Skip to content

feat(library): move files to another audiobook record#719

Draft
kevinheneveld wants to merge 1 commit into
Listenarrs:canaryfrom
kevinheneveld:feat/transfer-files
Draft

feat(library): move files to another audiobook record#719
kevinheneveld wants to merge 1 commit into
Listenarrs:canaryfrom
kevinheneveld:feat/transfer-files

Conversation

@kevinheneveld

Copy link
Copy Markdown
Contributor

What

Adds a "Move Files to Another Book" action to the audiobook detail page: move audio files from one library record to another existing record, for when files actually belong to a different book the library already tracks — two books' tracks imported onto one record, or a collection that needs splitting into its real books.

  • POST /library/{id}/files/transfer (new LibraryTransferFilesWorkflow): DB ownership reassigns always; the physical file moves into the destination folder best-effort — a failed disk move leaves the file in place with a warning (Organize can relocate it later) and never aborts the transfer.
  • Collision safety: if the target already owns a row at the path a file would occupy (duplicate from a prior partial transfer, or a dual-referenced file), the file is skipped with a clear warning before any disk move — moving first would orphan the source row. Any other per-file DB failure also degrades to a warning instead of a 500.
  • Bookkeeping: a source left without audio gets its legacy single-file columns (FilePath/FileSize) reset, gated on what actually reassigned; history entries land on both records ("Files Transferred" / "Files Received").
  • IAudiobookFileRepository.ReassignAsync: targeted column update via a detached stub (only AudiobookId/Path marked modified, nothing tracked) — a full-entity Update on rows sharing navigation graphs trips EF's identity map on the second file of a bulk transfer. Implemented without ExecuteUpdate so the InMemory test provider exercises the same code path production runs.
  • UI: transfer modal with per-file checkboxes and a library search ranked by token overlap (so a filename-ish query still surfaces the right record), a merge warning + confirm when the destination already has files, and a detail-page action next to Organize.
  • Toasts: warning/error toasts are now sticky by default (dismissed via the close button) — transfer warnings carry information the user needs to read and act on. Info/success keep the 5s auto-dismiss; callers can still pass an explicit timeout to opt back in.

Why

Live case: a dramatized adaptation's parts were sitting on the wrong record while the book's own entry sat wanted with zero files. Today the only remedies are delete-and-rescan or manual filesystem surgery; this makes the fix a two-click operation that keeps history and never loses a file.

Tests

9 new tests in LibraryController_TransferFilesTests (validation, full + subset transfer, legacy-column gating, missing-on-disk, DB-row collision skip, on-disk collision reassign-in-place, history). Full suite: 1024/1024 passing. vue-tsc + eslint + prettier clean.

🤖 Generated with Claude Code

When files actually belong to a different book the library already
tracks (two books' tracks imported onto one record, or a collection
being split into its real books), a "Move Files to Another Book" action
on the detail page opens a transfer dialog with per-file checkboxes and
a library search ranked by token overlap.

POST /library/{id}/files/transfer: DB ownership reassigns always; the
physical file moves into the destination folder best-effort (failures
leave it in place with a warning — Organize can relocate later).
Collisions with a row the target already owns at the same path are
detected BEFORE any disk move and skipped with a warning, and any
per-file DB failure becomes a warning rather than aborting the whole
transfer. A source left without audio gets its legacy single-file
columns reset; history entries land on both records.

Row reassignment uses a targeted IAudiobookFileRepository.ReassignAsync
(detached stub, only the reassigned columns marked modified) so bulk
transfers can't trip EF identity-map conflicts on overlapping
navigation graphs.

Warning/error toasts are now sticky by default (dismissed via the close
button) — transfer warnings carry information the user needs to read
and act on; info/success keep the 5s auto-dismiss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant