Skip to content

Don't move ebook files when no colocated destination applies - #127

Open
jbob06 wants to merge 1 commit into
Chaptarr:developfrom
jbob06:fix/colocation-skip-when-not-applicable
Open

Don't move ebook files when no colocated destination applies#127
jbob06 wants to merge 1 commit into
Chaptarr:developfrom
jbob06:fix/colocation-skip-when-not-applicable

Conversation

@jbob06

@jbob06 jbob06 commented Sep 4, 2026

Copy link
Copy Markdown

Fixes #126.

EbookColocateOnAudiobookImportHandler exists solely to place ebook files alongside a just-imported audiobook of the same work. It moved a file whenever GetOrganizeDestination returned CanOrganize, but that is true whether or not the colocation planner actually applied: when the planner declines, the plan still carries the plain naming destination, and the handler relocated the ebook to it. That is a full re-organize of a file that was never part of the triggering import.

The two gates disagree by construction. The handler resolves its root from the audiobook's quality, the planner resolves its root from the ebook's quality, and Author.GetRootFolderForQuality returns a different root per media type. AuthorService.IsCompatibleRootFolder allows an author to have a Mixed audiobook root and a separate ebook only root, and RootFolderResource forces PlaceEbooksWithAudiobooks = false on any non Mixed root, so that configuration is guaranteed to pass the handler's gate and fail the planner's. NoAudiobookFolders is reachable the same way.

In that state MoveBookFile also took its non colocation branch, whose unconditional TransferFile throws SameFilenameException for a file already at its canonical path. The handler's try/catch sits outside the per file loop and nothing on this path catches that exception, so one already correct ebook aborted the whole batch under the generic "Failed to colocate ebooks after audiobook import" warning.

Change

Surface whether colocation was applied on BookFileMovePlan and skip the move when it was not.

ColocationApplied is also now the single source of truth for "this plan is a colocation" and selects the colocating branch in MoveBookFile, replacing the previous plan.ReplicaPaths != null check that encoded the same fact implicitly. Those two were provably equivalent for every plan reaching MoveBookFile in production (two construction sites, two callers, and the planner's single Applies = true site always assigns a non null list), so this is behavior preserving and removes an unsynchronized second encoding rather than adding one. The colocating branch is null tolerant either way, since ReconcileReplicaFiles null coalesces its argument.

RenameBookFileService is unaffected: it reads only CanOrganize, DestinationPath, SkipReason, the author folder paths and ShouldUpdateStoredAuthorPath.

Tests

should_not_move_ebook_when_no_colocated_destination_applies hands the handler a plan where colocation did not apply and asserts no move, no Update, and no events. It fails on current develop and passes with the change.

should_not_flag_colocation_when_planner_declines pins the flag on the real BookFileMovingService with a declining planner, asserting CanOrganize stays true while ColocationApplied is false, so RenameBookFileService's behavior is explicitly unchanged. The existing colocation test gained the positive assertion.

Full Chaptarr.Core.Test suite: 3010 passed, 0 failed.

Known deferral

Because the handler now returns before MoveBookFile, it no longer triggers the ShouldCleanupReplicas path in the declined case, so an ebook carrying stale ReplicaPaths from an earlier colocation is no longer pruned opportunistically during an unrelated audiobook import. That cleanup still happens via RenameBookFileService and via GetImportDestinationPath on the next import of that file. This seemed like correct scoping for a handler whose mandate is colocation rather than general organizing, but happy to restore an explicit cleanup only path here if you would rather keep it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MfnN7ewDrDq99e4LvzCzaW

EbookColocateOnAudiobookImportHandler exists solely to place ebook files
alongside a just-imported audiobook of the same work. It moved a file
whenever GetOrganizeDestination returned CanOrganize, but that is true
whether or not the colocation planner actually applied: when the planner
declines, the plan still carries the plain naming destination.

The handler's gate reads the audiobook's root folder while the planner's
gate reads the ebook's root folder, and Author.GetRootFolderForQuality
returns different roots per media type. An author with a Mixed audiobook
root and a separate ebook-only root therefore passes the handler's gate
and fails the planner's, so every audiobook import re-organized that
author's ebook files to their plain naming path even though nothing was
being colocated.

In that state MoveBookFile also took its non-colocation branch, whose
unconditional TransferFile throws SameFilenameException for a file that
is already at its canonical path. The handler's try/catch sits outside
the per-file loop, so one already-correct ebook aborted the whole batch.

Surface whether colocation was applied on BookFileMovePlan and skip the
move when it was not. The flag is now the single source of truth for
"this plan is a colocation" and selects the colocating branch in
MoveBookFile, replacing the previous implicit ReplicaPaths != null check
that encoded the same fact.

Fixes Chaptarr#126

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MfnN7ewDrDq99e4LvzCzaW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ebook colocation handler moves ebook files even when the colocation planner declines

1 participant