Skip to content

fix(paths): harden imports, moves, and root relocation#717

Open
therobbiedavis wants to merge 120 commits into
canaryfrom
bugfix/unix-folder-name-space
Open

fix(paths): harden imports, moves, and root relocation#717
therobbiedavis wants to merge 120 commits into
canaryfrom
bugfix/unix-folder-name-space

Conversation

@therobbiedavis

@therobbiedavis therobbiedavis commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR hardens Listenarr's filesystem path handling across download imports, manual/library destinations, durable audiobook moves, root-folder relocation, metadata-only path repair, scanning, and frontend destination editing.

The main rule this branch enforces is:

  • Download-client-reported source paths are external filesystem identities and must preserve legal whitespace, case, and client-reported shape.
  • Listenarr-owned destination paths are user input and must be validated against explicit filesystem boundaries before any filesystem mutation or metadata rewrite.

It also replaces fragile BasePath pre-save behavior with durable move orchestration, adds root-folder relocation workflows, restores EF migration discoverability/schema coverage from PR #727, and adds guardrails so future path/migration regressions fail tests instead of silently shipping.

Added

  • Filesystem path identity helpers for syntax-aware, case-sensitive, and case-insensitive equality/containment checks.
  • Filesystem semantics resolution for configured roots, output paths, download mappings, scan paths, move source/target paths, and metadata rewrites.
  • Strict user-provided path validation for root folders, library destinations, manual imports, move targets, and frontend path editing.
  • Durable audiobook content moves with persisted source paths, manifests, recovery markers, lease fencing, retry state, phase tracking, source cleanup safeguards, and terminal/attention statuses.
  • Root-folder relocation APIs, frontend flow, active-boundary protection, retry support, metadata-only relocation handling, skipped rewrite tracking, and historical relocation preservation.
  • A shared filesystem mutation coordinator to serialize high-risk move, relocation, retry, and metadata-rewrite operations.
  • Centralized metadata-only destination rewrite service used by both the move endpoint and legacy PUT /library/{id} compatibility path.
  • Background move-job status tracking store on the frontend so queued move updates survive modal close.
  • SQLite migration/schema tests that migrate a real database and verify the resulting schema matches the EF model.
  • Architecture/guardrail tests for path semantics, migration metadata, hosted workers, and backend layering.

Changed

  • Preserves exact download-client source paths for torrent import, remote-path mapping, queue translation, and case-distinct qBittorrent source files.
  • Separates source-path identity handling from user-destination validation instead of routing both through the same normalization rules.
  • Queues physical library moves instead of pre-saving BasePath, so metadata is updated only after move completion or through explicit metadata-only rewrite paths.
  • Allows custom physical move destinations outside configured roots when the nearest existing parent is valid, while keeping metadata-only moves restricted to configured root/output boundaries.
  • Routes legacy PUT /library/{id} BasePath changes through the same destination rewrite service and prevents stale full-object payloads from undoing rewritten path fields.
  • Builds sparse frontend update payloads so destination changes do not resend unchanged basePath, imageUrl, or file-path fields.
  • Applies filesystem-aware comparisons for same-path checks, overlapping roots, active move conflicts, root-folder reassignment/deletion guards, scan dedupe, unmatched filtering, and rename/organize flows.
  • Uses explicit root-folder case-sensitivity settings ahead of legacy output-path auto behavior when both point to the same boundary.
  • Updates frontend root-folder and audiobook destination editing to use the new validation, relocation, and move queue workflows.
  • Keeps retry/requeue state lease-aware so stale workers cannot mutate attempts, overwrite terminal state, or broadcast stale failures.
  • Makes move queue legacy identity reconciliation tolerant of malformed active jobs by marking only the bad job NeedsAttention and continuing recovery.

Fixed

  • Imports from torrent/download folders with legal leading or trailing whitespace on Unix-like filesystems.
  • Torrent child path escape risks from rooted-looking or parent-segment client paths.
  • Invalid custom add-to-library and move destinations silently falling back to generated paths.
  • Absolute library move destinations bypassing configured root/output boundaries.
  • Physical custom move destinations being incorrectly rejected by metadata-only configured-root validation.
  • Case-only library moves being blocked on roots configured as case-sensitive.
  • Move retry failures caused by stale temp directories, stale leases, or incomplete durable move state.
  • Nested target-inside-source cleanup/recursive copy hazards so unrelated sibling content is not removed or merged.
  • Root-folder relocation and standalone move conflicts racing each other or crossing active boundaries.
  • Root-folder deletion/reassignment returning misleading raw failures instead of conflict responses.
  • Metadata-only path repair requiring source filesystem access or deleting tracked files when BasePath is unavailable.
  • Saved unmatched scan results showing already-tracked files because equivalent paths used different slash/canonical forms.
  • Durable move UI status mismatch (Processing vs Running/NeedsAttention/Superseded) and modal-local subscriptions being torn down before background updates arrive.
  • EF migration drift by restoring the AddMoveJobSourcePath designer, adding the missing ProcessExecutionLogs migration, regenerating durable move migrations via EF tooling, and adding schema coverage.

Removed / replaced

  • Removed the fragile BasePath pre-save pattern for physical moves.
  • Removed modal-owned long-lived move-job SignalR handling in favor of an app-level move-job store.
  • Removed/debug-cleaned the temporary frontend debug test placeholder.
  • Removed additional host-default path comparison fallbacks from user-library path identity sites, replacing them with explicit resolved semantics.
  • Removed the need for runtime schema repair around missing move-job columns by making migrations discoverable and schema-tested.

Testing

Validated locally on the final branch state with:

  • cd fe && npx vitest run EditAudiobookModal.moveOptions.spec.ts moveJobs.store.spec.ts --reporter=dot --silent=true
  • cd fe && npm run test:unit
  • cd fe && npm run build
  • dotnet test tests/Listenarr.Tests.csproj --no-restore --verbosity minimal --filter "FullyQualifiedName~EfMoveQueuePersistenceTests|FullyQualifiedName~MoveQueueServiceTests|FullyQualifiedName~LibraryController_MoveTests" -p:OutDir=C:/Users/local_oqoz17v/AppData/Local/Temp/listenarr-test-out/
  • dotnet test tests/Listenarr.Tests.csproj --no-build --verbosity minimal --filter "FullyQualifiedName~BackendArchitectureTests|FullyQualifiedName~HostedServicesRegistrationTests.BackgroundWorkerOwnership_DocumentsEveryHostedService"
  • cd fe && npx prettier --check src/__tests__/EditAudiobookModal.moveOptions.spec.ts src/__tests__/moveJobs.store.spec.ts src/components/domain/audiobook/EditAudiobookModal.vue src/stores/moveJobs.ts
  • git diff --check origin/canary...HEAD

Also previously validated the broader backend suite during review; the only temp-output failures were repo-root fixture/doc lookup tests, which passed when rerun from the normal repository output.

Issues and related PRs

Notes

  • Future EF migrations should be generated with dotnet ef migrations add; do not hand-author or hand-split migration files.
  • Future EF migrations must use IDs later than 20260702200000.
  • Download-client source paths remain external filesystem identities and are not routed through user destination validation.
  • Metadata-only moves remain restricted to configured root/output boundaries; physical moves may use a validated custom destination.
  • Source-inside-target moves into a parent containing unrelated sibling content remain intentionally blocked to avoid merging an audiobook into a non-empty unrelated library folder.
  • Docker compatibility follows the Linux container path model, so host paths should still be mounted and referenced as container paths such as /books or /downloads.

Preserve download-client reported path whitespace for torrent source/content paths while keeping destination validation separate. Add user-provided library destination validation for add, move, and root-folder workflows, allowing filesystem roots for root folders while rejecting parent traversal for concrete destinations.
Allow root-folder configuration to accept filesystem roots, including Windows current-drive roots, while keeping concrete destination paths strict against root-only and parent-traversal targets. Add cross-platform root-folder validation coverage and standardize the new torrent path mapper test header.
@therobbiedavis therobbiedavis added the patch patch version bump - backward compatible bug fixes label Jul 1, 2026
@therobbiedavis
therobbiedavis marked this pull request as ready for review July 1, 2026 21:04
@therobbiedavis
therobbiedavis requested review from a team and Copilot July 1, 2026 21:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes download/import path handling so client-reported torrent paths preserve meaningful leading/trailing whitespace (important on Unix-like filesystems), while adding stricter, OS-aware validation for Listenarr-owned destination paths (root folders, add-to-library, and move operations).

Changes:

  • Preserve whitespace in torrent client path mapping and remote path translation by avoiding whitespace-trimming/whitespace-only rejection in key path plumbing.
  • Add OS-aware normalization/validation for user-provided destination directories, including explicit support for filesystem roots in root-folder configuration and blocking parent traversal for concrete destinations.
  • Add regression tests covering whitespace-bearing torrent folders (Transmission/qBittorrent), root-folder root paths, and invalid destination rejection in library add/move workflows.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Mocks/Api/TransmissionApiMock.cs Adds a canned Transmission torrent response with whitespace-bearing folder names for regression coverage.
tests/Features/Infrastructure/DownloadClients/Common/TorrentClientPathMapperTests.cs New tests asserting whitespace preservation and rooted-child handling for torrent file mapping.
tests/Features/Infrastructure/DownloadClients/Common/DownloadClientAdapterTests.cs Adds Transmission adapter regression asserting whitespace-bearing ContentPath/SourceFiles are preserved.
tests/Features/Domain/Utils/FileUtilsTests.cs Adds tests for whitespace-preserving combining/normalization and new destination-path validation rules.
tests/Features/Application/Downloads/Common/DownloadClientGatewayTests.cs Adds tests ensuring remote path mapping and directory expansion keep whitespace-bearing paths intact.
tests/Features/Application/Audiobooks/RootFolders/RootFolderServiceTests.cs Adds coverage for allowing filesystem roots and validating/normalizing root folder paths.
tests/Features/Api/Features/Library/LibraryController_MoveTests.cs Adds API test ensuring invalid move destinations are rejected with 400.
tests/Features/Api/Features/Library/LibraryController_AddToLibraryTests.cs Updates custom path test to be OS-safe and adds parent-traversal/invalid-path rejection tests.
listenarr.infrastructure/DownloadClients/Transmission/TransmissionImportPathResolver.cs Stops treating whitespace-bearing strings as “empty” when building content/source paths.
listenarr.infrastructure/DownloadClients/Qbittorrent/QbittorrentImportPathResolver.cs Same as above for qBittorrent source file translation.
listenarr.infrastructure/DownloadClients/Common/TorrentClientPathMapper.cs Reworks torrent path mapping to avoid trimming path-segment whitespace and better handle rooted-looking child paths.
listenarr.infrastructure/Configuration/Paths/RemotePathMappingService.cs Adjusts empty-path detection to avoid rejecting whitespace-bearing remote paths.
listenarr.domain/Common/FileUtils.UserProvidedPaths.cs New OS-aware user-provided directory path validation/normalization helper with root/traversal options.
listenarr.domain/Common/FileUtils.PathCombining.cs Documents CombineWithOptionalBase’s whitespace-preserving intent and relaxes base-path emptiness check.
listenarr.application/Downloads/Common/DownloadClientGateway.cs Preserves whitespace-bearing paths during mapping/translation and directory-expansion flows.
listenarr.application/Audiobooks/RootFolders/RootFolderService.cs Root folder create/update now uses OS-aware normalization and allows filesystem root paths.
listenarr.application/Audiobooks/Contracts/ILibraryAddService.cs Extends result contract to surface validation failures cleanly to API workflows.
listenarr.application/Audiobooks/Catalog/LibraryAddService.cs Validates custom/generated destination paths with OS-aware normalization and blocks parent traversal.
listenarr.api/Features/Library/LibraryMoveWorkflow.cs Validates and normalizes move destinations (blocking traversal) instead of silently normalizing invalid targets.
listenarr.api/Features/Library/LibraryAddWorkflow.cs Returns 400 for validation failures and validates custom destination paths via OS-aware normalization.

Comment thread listenarr.infrastructure/DownloadClients/Common/TorrentClientPathMapper.cs Outdated
Comment thread tests/Features/Application/Downloads/Common/DownloadClientGatewayTests.cs Outdated
Comment thread tests/Features/Application/Downloads/Common/DownloadClientGatewayTests.cs Outdated
therobbiedavis and others added 6 commits July 1, 2026 18:29
Cherry-picked PR #727 from 3103996 and adapted the current branch by removing the now-redundant MoveJobs runtime schema repair.

Co-authored-by: Kevin Heneveld <1192102+kevinheneveld@users.noreply.github.com>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Comment thread fe/src/utils/path.ts Fixed
Comment thread fe/src/utils/path.ts Fixed
Comment thread listenarr.infrastructure/Library/Moving/MoveBackgroundService.cs Fixed
Make root-folder operations semantics-aware by threading FileSystemPathSemantics through root path checks, repository migrations, deletion safeguards, and unmatched-file filtering, and block new roots that overlap active relocation boundaries.

Improve relocation/move reliability with atomic-rename recovery markers, stricter cleanup reconciliation (including ambiguous quarantine detection), resilient heartbeat/broadcast error handling, and retry/reconcile support for superseded move jobs. Remove the temporary frontend debug test placeholder.
Adds lease-generation fencing for move jobs so only the active claimant can heartbeat, update status/phase, and persist move-manifest cleanup state. Move processing now cancels when a lease is lost, and persistence now increments/stores LeaseGeneration with a migration.

Path handling was also made semantics-driven across backend and frontend: remote/client path mapping, scan/planning, metadata parsing, and destination validation now honor explicit syntax/case-sensitivity instead of host defaults or path-shape guesses. Tests were updated to cover lease loss behavior, cross-platform path rules, and new architecture boundaries.
Refactor UpdateJobStatusAsync to isolate persistence, relocation, and broadcast into separate try/catch blocks so persistence failures propagate without broadcasting. Add specific catch blocks in MoveBackgroundService and MoveJobProcessor for MoveLeaseLostException and PersistenceException to prevent lease-lost scenarios from marking jobs as Failed. Update root folder change toast messages for clarity. Add tests covering persistence failure propagation, post-commit relocation failures, and lease-loss handling.
Diagnosis: LibraryMoveWorkflow compares user-library source and destination paths, not internal system paths. The relevant semantics are the destination root/output path boundary semantics already resolved from configured root folders or output path. This changes no public API surface; LibraryMoveWorkflow is DI-constructed and callers continue through LibraryController.
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Temporary verbose Windows test diagnostic superseded by compact output.

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Application.Audiobooks.RootFolders.RootFolderServiceTests.Delete_ReassignsFilesystemRootChildAudiobookPreservingRelativePath
System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See https://go.microsoft.com/fwlink/?LinkId=800142 This exception can be suppressed or logged by passing event ID 'InMemoryEventId.TransactionIgnoredWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.InMemory.Internal.InMemoryLoggerExtensions.TransactionIgnoredWarning(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTransactionManager.BeginTransactionAsync(CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.BeginTransactionAsync(CancellationToken cancellationToken)
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 190
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 251
   at Listenarr.Application.Audiobooks.RootFolders.RootFolderService.DeleteCoreAsync(Int32 id, Nullable`1 reassignRootId) in D:\a\Listenarr\Listenarr\listenarr.application\Audiobooks\RootFolders\RootFolderService.cs:line 115
   at Listenarr.Application.Common.FilesystemMutationCoordinator.ExecuteExclusiveAsync(Func`2 operation, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.application\Common\FilesystemMutationCoordinator.cs:line 20
   at Listenarr.Tests.Features.Application.Audiobooks.RootFolders.RootFolderServiceTests.Delete_ReassignsFilesystemRootChildAudiobookPreservingRelativePath() in D:\a\Listenarr\Listenarr\tests\Features\Application\Audiobooks\RootFolders\RootFolderServiceTests.cs:line 560
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove
Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException : Destination contains an unowned directory: nested
at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.ValidateExistingDestinationContents(String source, String destinationRoot, IReadOnlyCollection`1 manifest, Guid jobId, FileSystemPathSemantics targetSemantics) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 128
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.CopySourceContentsAsync(String source, String copyDestination, IReadOnlyList`1 manifest, Guid jobId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 17
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.MoveContentsAsync(AudiobookContentMoveRequest request, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.cs:line 169
   at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 583
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot
System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See https://go.microsoft.com/fwlink/?LinkId=800142 This exception can be suppressed or logged by passing event ID 'InMemoryEventId.TransactionIgnoredWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.InMemory.Internal.InMemoryLoggerExtensions.TransactionIgnoredWarning(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTransactionManager.BeginTransactionAsync(CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.BeginTransactionAsync(CancellationToken cancellationToken)
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 190
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 251
   at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 43
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: Exception type was not an exact match
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
Actual:   typeof(System.InvalidOperationException)
---- System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See https://go.microsoft.com/fwlink/?LinkId=800142 This exception can be suppressed or logged by passing event ID 'InMemoryEventId.TransactionIgnoredWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 95
--- End of stack trace from previous location ---
----- Inner Stack Trace -----
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.InMemory.Internal.InMemoryLoggerExtensions.TransactionIgnoredWarning(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTransactionManager.BeginTransactionAsync(CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.BeginTransactionAsync(CancellationToken cancellationToken)
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 190
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 251

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot
Assert.Equal() Failure: Strings differ
                                     ↓ (pos 91)
Expected: ···"547ad4a\\root-reassign-target\\Author\\Title"
Actual:   ···"547ad4a\\root-reassign-source\\Author\\Title"
                                     ↑ (pos 91)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 53
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove
Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException : Destination contains an unowned directory: nested
at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.ValidateExistingDestinationContents(String source, String destinationRoot, IReadOnlyCollection`1 manifest, Guid jobId, FileSystemPathSemantics targetSemantics) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 128
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.CopySourceContentsAsync(String source, String copyDestination, IReadOnlyList`1 manifest, Guid jobId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 17
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.MoveContentsAsync(AudiobookContentMoveRequest request, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.cs:line 169
   at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 583
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 95
--- End of stack trace from previous location ---

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot
Assert.Equal() Failure: Strings differ
                                     ↓ (pos 91)
Expected: ···"7e36cc8\\root-reassign-target\\Author\\Title"
Actual:   ···"7e36cc8\\root-reassign-source\\Author\\Title"
                                     ↑ (pos 91)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 53
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 95
--- End of stack trace from previous location ---

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 143
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

@m4bard

m4bard commented Jul 16, 2026

Copy link
Copy Markdown

Bit awkward to raise this mid-PR — apologies if it's noise while you're heads-down. But I ran a reproduction harness against this branch and it turned up something directly relevant to what you're hardening, so here seemed better than a separate issue.

ScanFileDiscovery.Matches still attributes a file by author-in-path (filePathMatchesAuthor, line ~140). In any {Author}/… layout every file under an author's folder satisfies that, so scanning one book claims every book by that author, and CalculateBasePath collapses the result to the author folder.

Reproduced on this branch with Listenarr's own default layout {Author}/{Series}/{Title}, BasePath cleared so the scan falls back to the library root:

scan two books, each with a same-author sibling present:
  BasePath '/audiobooks/Arthur Conan Doyle' — shared by 2 books, swallowed a sibling
  BasePath '/audiobooks/Alexandre Dumas'    — shared by 2 books, same

Both end up with the author folder as their BasePath — which matters more here, since BasePath now feeds your move orchestration: a book whose BasePath is its author's shelf is a book whose move operates on the whole shelf.

The one-line fix (drop the author clause; require the title in the filename or folder) plus tests are up as #766, but it belongs on this branch since you're rewriting that file. It does mean layouts carrying neither title nor author in the path go from wrongly-matched to unmatched — the right direction, and what a tag pass would then claim.

The harness runs against a Linux container — which is where a large share of users will probably run Listenarr, and where this branch's cross-platform path handling lands in practice — so it exercises the deployment target rather than a dev box. Happy to run whatever path shapes would help and report back, which is zero setup on your end. Or run it yourself: it's public (https://github.com/m4bard/listenarr-testdata), with a build-and-run-against-a-branch recipe in the README (Linux/macOS or WSL2). There's a small backlog of harness improvements tracked there too, if any of this is useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch patch version bump - backward compatible bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Linux/OSX] Folder names with leading/trailing space

5 participants