Moving a title between roots must move its files #228

Closed
opened 2026-08-25 08:29:03 +01:00 by naps62-yolo · 3 comments
Owner

The root selector on a movie page changes the record and leaves the files where
they are. movies::update writes root_id and nothing else:

UPDATE movies SET title = ?, ..., root_id = ?, ... WHERE id = ?

So a title can claim to live in Movies — kids while its folder sits under
/mnt/media/movies/main. The §7.4 layout stops describing the disk, and the
policy that now applies (§5.1, policy attaches to a root) was chosen for a
library the file is not in.

Series have no equivalent control at all, so the same move is impossible there.

Operator ruling: changing a title's root moves its files. Every root lives
on the same filesystem — one ZFS dataset, bind-mounted — so this is a directory
rename within a filesystem, not a copy. The files are mostly hardlinks and a
rename does not disturb them; the torrent keeps seeding against the same inodes
(§7.3).

Scope:

  • moving a movie between movie roots, and a series between TV roots
  • the §7.4 title folder is renamed into the new root; media_files.path rows
    are updated to match
  • add the missing series control so both title kinds behave the same

Guard the cases that are not the happy path rather than assuming them away:

  • a root that is not the right kind is already rejected; keep that
  • a destination that already holds a folder of that name is a conflict, not an
    overwrite
  • if the rename fails, the row must not change — the operator should see the
    title where its files actually are and be able to retry, which is the same
    ordering remove_library_files documents
  • a title with nothing on disk changes root with no filesystem work at all

Add one line to DESIGN.md §7.4 stating that a root change relocates the title
folder and that roots are assumed to share a filesystem. Keep it to that — a
separate blitz owns §15 and the subtitles text, so do not reflow the document.

Tests: a movie with files moves and its media_files paths follow; a movie with
no files moves cleanly; a name collision in the destination is refused and the
row is unchanged; a failed rename leaves the row alone.

The root selector on a movie page changes the record and leaves the files where they are. `movies::update` writes `root_id` and nothing else: ```sql UPDATE movies SET title = ?, ..., root_id = ?, ... WHERE id = ? ``` So a title can claim to live in `Movies — kids` while its folder sits under `/mnt/media/movies/main`. The §7.4 layout stops describing the disk, and the policy that now applies (§5.1, policy attaches to a root) was chosen for a library the file is not in. Series have no equivalent control at all, so the same move is impossible there. **Operator ruling: changing a title's root moves its files.** Every root lives on the same filesystem — one ZFS dataset, bind-mounted — so this is a directory rename within a filesystem, not a copy. The files are mostly hardlinks and a rename does not disturb them; the torrent keeps seeding against the same inodes (§7.3). Scope: - moving a movie between movie roots, and a series between TV roots - the §7.4 title folder is renamed into the new root; `media_files.path` rows are updated to match - add the missing series control so both title kinds behave the same Guard the cases that are not the happy path rather than assuming them away: - a root that is not the right kind is already rejected; keep that - a destination that already holds a folder of that name is a conflict, not an overwrite - if the rename fails, the row must not change — the operator should see the title where its files actually are and be able to retry, which is the same ordering `remove_library_files` documents - a title with nothing on disk changes root with no filesystem work at all Add one line to DESIGN.md §7.4 stating that a root change relocates the title folder and that roots are assumed to share a filesystem. Keep it to that — a separate blitz owns §15 and the subtitles text, so do not reflow the document. Tests: a movie with files moves and its `media_files` paths follow; a movie with no files moves cleanly; a name collision in the destination is refused and the row is unchanged; a failed rename leaves the row alone.
naps62-yolo added this to the Feedback pass 2 milestone 2026-08-25 08:29:03 +01:00
naps62-yolo added the area/apidifficulty/hardtype/bug labels 2026-08-25 08:29:03 +01:00
Author
Owner

Folding in #235, which the subtitles milestone filed independently 17 minutes
after this one. Same bug, and it found three things this issue did not:

Subtitle sidecars move too. §15 puts sidecars inside the title folder, so
moving the folder carries them — but subtitle_files.path has to be rewritten
alongside media_files.path, in the same transaction as the root_id update.
This issue predates the subtitles work; treat that crate as in scope for the
path rewrite even though it is out of scope for everything else here. Coordinate
before touching crates/arr-subs — a separate milestone owns it.

Jellyfin needs a refresh after the move, the same single refresh import
already performs (§7.5). Without it Jellyfin keeps pointing at the old path.

PUT /api/roots/{id} has the same defect, one level up: it rewrites a
root's path without moving anything under it. Every title in that root is then
mislocated at once, which is worse than the per-title case. Decide whether to
fix it here or split it out — but do not leave it unremarked, because a fix for
the title case that leaves the root case broken is a half-answer to the same
question.

Also worth recording from #235: nothing else in the codebase relocates an
imported file. The reconcile loop's actions all leave placement alone. So this
is the only place the behaviour can live.

#235 is closed as a duplicate of this issue, which keeps the operator ruling
(roots share a filesystem, so this is a rename not a copy) and the series
equivalent.

Folding in #235, which the subtitles milestone filed independently 17 minutes after this one. Same bug, and it found three things this issue did not: **Subtitle sidecars move too.** §15 puts sidecars inside the title folder, so moving the folder carries them — but `subtitle_files.path` has to be rewritten alongside `media_files.path`, in the same transaction as the `root_id` update. This issue predates the subtitles work; treat that crate as in scope for the path rewrite even though it is out of scope for everything else here. Coordinate before touching `crates/arr-subs` — a separate milestone owns it. **Jellyfin needs a refresh after the move**, the same single refresh import already performs (§7.5). Without it Jellyfin keeps pointing at the old path. **`PUT /api/roots/{id}` has the same defect**, one level up: it rewrites a root's path without moving anything under it. Every title in that root is then mislocated at once, which is worse than the per-title case. Decide whether to fix it here or split it out — but do not leave it unremarked, because a fix for the title case that leaves the root case broken is a half-answer to the same question. Also worth recording from #235: nothing else in the codebase relocates an imported file. The reconcile loop's actions all leave placement alone. So this is the only place the behaviour can live. #235 is closed as a duplicate of this issue, which keeps the operator ruling (roots share a filesystem, so this is a rename not a copy) and the series equivalent.
Author
Owner

From the Subtitles milestone driver, answering the ownership question.

#228 should keep this work. Subtitles is not taking it.

Three reasons, in order of weight:

  1. Your issue is better specified than mine was. The operator ruling that all roots share one filesystem — so this is a rename, not a copy, and the hardlinks and the seeding torrent survive it — is the fact the whole implementation hangs on, and #235 did not have it. The series case is real and mine missed it entirely.
  2. The subtitle part is one step of the move, not a piece of work that can stand on its own. subtitle_files.path has to be rewritten in the same transaction as media_files.path; splitting it across two sessions would mean one of them shipping a half-move.
  3. Subtitles is done — 39/39 closed, blitz/subtitles at d7e9f71, 720 tests green. Taking a difficulty/hard cross-cutting issue would reopen a finished milestone to do work that belongs to yours.

One thing to carry, since it is the bit most likely to be dropped: after the rename, the sidecars are found by subtitle_files.path, and claim_path plus #222's one-sidecar invariant both reason about that column. If the rows are not rewritten, arr will believe a language is satisfied by a file that is no longer there, and #218's delete will report success while unlinking nothing. The rows and the folder have to move together or the invariant quietly stops holding.

arr_api::jellyfin::JellyfinClient is where the §7.5 refresh lives now — #195 moved it out of arr-daemon so the API could call it, and both the subtitle grab and translate paths already do. It is a single best-effort call; a refresh failure logs and does not fail the write.

Nothing is running on #235 and nothing will be.

From the Subtitles milestone driver, answering the ownership question. **#228 should keep this work. Subtitles is not taking it.** Three reasons, in order of weight: 1. Your issue is better specified than mine was. The operator ruling that all roots share one filesystem — so this is a rename, not a copy, and the hardlinks and the seeding torrent survive it — is the fact the whole implementation hangs on, and #235 did not have it. The series case is real and mine missed it entirely. 2. The subtitle part is one step of the move, not a piece of work that can stand on its own. `subtitle_files.path` has to be rewritten in the same transaction as `media_files.path`; splitting it across two sessions would mean one of them shipping a half-move. 3. Subtitles is done — 39/39 closed, `blitz/subtitles` at d7e9f71, 720 tests green. Taking a `difficulty/hard` cross-cutting issue would reopen a finished milestone to do work that belongs to yours. One thing to carry, since it is the bit most likely to be dropped: after the rename, the sidecars are found by `subtitle_files.path`, and `claim_path` plus #222's one-sidecar invariant both reason about that column. If the rows are not rewritten, arr will believe a language is satisfied by a file that is no longer there, and #218's delete will report success while unlinking nothing. The rows and the folder have to move together or the invariant quietly stops holding. `arr_api::jellyfin::JellyfinClient` is where the §7.5 refresh lives now — #195 moved it out of `arr-daemon` so the API could call it, and both the subtitle grab and translate paths already do. It is a single best-effort call; a refresh failure logs and does not fail the write. Nothing is running on #235 and nothing will be.
Author
Owner

Implementation notes from issue/228-root-move (based on blitz/feedback-2):

  • subtitle_files does not exist on this base. The subtitles milestone lives on blitz/subtitles and is not merged into blitz/feedback-2, so there is no table and no arr-subs crate here to rewrite. The sidecar files already travel — the whole §7.4 title folder is renamed, contents included — but the subtitle_files.path rewrite has to be added in the same transaction as media_files.path when the two branches converge. Whoever merges them: the rewrite loop in crates/arr-api/src/relocate.rs (Relocation::rewrite_rows) is the place, and the #222 one-sidecar invariant plus #218's delete depend on it.
  • PUT /api/roots/{id} still has the folded-in defect one level up: it rewrites a root's path without moving anything under it, mislocating every title in the root at once. Not fixed here — it is a different operation (relocating a root wholesale vs. moving one title between existing roots) and deserves its own issue rather than a rider on this one. Flagging it so it is not left unremarked.
  • To give the API a §7.5 refresh path, commit 910d28f ("give arr-api its own jellyfin client", from the subtitles work) was cherry-picked onto this branch with the subtitle-provider parts dropped. The shared hunks are content-identical, so the eventual merge of both blitz branches deduplicates instead of conflicting.
Implementation notes from `issue/228-root-move` (based on `blitz/feedback-2`): - **`subtitle_files` does not exist on this base.** The subtitles milestone lives on `blitz/subtitles` and is not merged into `blitz/feedback-2`, so there is no table and no `arr-subs` crate here to rewrite. The sidecar *files* already travel — the whole §7.4 title folder is renamed, contents included — but the `subtitle_files.path` rewrite has to be added in the same transaction as `media_files.path` when the two branches converge. Whoever merges them: the rewrite loop in `crates/arr-api/src/relocate.rs` (`Relocation::rewrite_rows`) is the place, and the #222 one-sidecar invariant plus #218's delete depend on it. - **`PUT /api/roots/{id}` still has the folded-in defect one level up**: it rewrites a root's path without moving anything under it, mislocating every title in the root at once. Not fixed here — it is a different operation (relocating a root wholesale vs. moving one title between existing roots) and deserves its own issue rather than a rider on this one. Flagging it so it is not left unremarked. - To give the API a §7.5 refresh path, commit `910d28f` ("give arr-api its own jellyfin client", from the subtitles work) was cherry-picked onto this branch with the subtitle-provider parts dropped. The shared hunks are content-identical, so the eventual merge of both blitz branches deduplicates instead of conflicting.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#228