From f6790ee37614cc67f3a2424102d12c0a81ee888d Mon Sep 17 00:00:00 2001 From: Sebastiaan Jacobs Date: Sun, 19 Jul 2026 23:30:38 +0200 Subject: [PATCH 1/2] feat(file-management): add symbolic link import action Add a configurable symbolic link file action for virtual and FUSE-backed storage workflows. Preserve existing symlink targets during import, avoid copying file contents, and keep source files under downloader control. Expose the new action in the frontend and add coverage for regular files, absolute links, relative links, and failed imports. Supports use cases discussed in Listenarr Discussion #334, including NZBDav, rclone, FUSE, and Real-Debrid mounted storage. Parts of this implementation were developed with the assistance of AI. All generated changes were reviewed, adjusted, and tested before committing. --- README.md | 20 ++ .../domain/audiobook/LibraryImportFooter.vue | 1 + .../components/feedback/ManualImportModal.vue | 3 +- .../feedback/UnmatchedFilesModal.vue | 16 +- .../settings/FileManagementSection.vue | 3 +- fe/src/stores/libraryImport.ts | 2 +- fe/src/types/index.ts | 4 +- .../Downloads/Import/DownloadImportService.cs | 6 +- .../Audiobooks/Enumerations/FileAction.cs | 4 +- .../FileSystem/FileMover.Copying.cs | 106 ++++++++ .../Api/Services/FileMoverSymlinkTests.cs | 250 ++++++++++++++++++ 11 files changed, 401 insertions(+), 14 deletions(-) create mode 100644 tests/Features/Api/Services/FileMoverSymlinkTests.cs diff --git a/README.md b/README.md index 0811c7c1f..70e193ef5 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,26 @@ Supported download clients: - Automatic metadata fetching - Library management options +#### Completed File Action + +Choose how completed downloads are placed into the library (Settings → File Management → *Completed File Action*): + +- **Move** – move the file into the library and remove it from the client's folder. +- **Copy** – copy the file into the library and leave the original in place. +- **Hardlink/Copy** – create a hardlink into the library (falls back to a copy when source and destination are on different filesystems). +- **Symbolic Link** (`symlink`) – create a symbolic link in the library instead of copying the file. The source content is never read or copied and the original source is left untouched. This is primarily useful with virtual filesystems such as **NZBDav**, **rclone** and other WebDAV/FUSE mounts, where a hardlink is impossible (cross-filesystem) and a copy would download the entire file. + + When the source is itself a symlink, Listenarr reads its target and links the library entry **directly** to the final target, avoiding a symlink chain. Both absolute and relative link targets are supported (relative targets are resolved against the source symlink's directory). + + > **Important:** the source and destination paths must stay reachable for the link to work. Because absolute link targets are preserved as-is, the underlying path (e.g. the NZBDav/rclone mount) must be mounted at the **same absolute path** in every container that reads the library — both Listenarr and your audiobook player (e.g. Audiobookshelf). The link will break whenever the source path (mount) is unavailable. + > + > Example — both containers must expose identical absolute paths: + > + > ```text + > Listenarr: /mnt/nzbdav + /data/media/audiobooks + > Audiobookshelf: /mnt/nzbdav + /data/media/audiobooks + > ``` + ## API Endpoints ### Search diff --git a/fe/src/components/domain/audiobook/LibraryImportFooter.vue b/fe/src/components/domain/audiobook/LibraryImportFooter.vue index 4b296227b..6d9e38c91 100644 --- a/fe/src/components/domain/audiobook/LibraryImportFooter.vue +++ b/fe/src/components/domain/audiobook/LibraryImportFooter.vue @@ -31,6 +31,7 @@ +