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
No Branch/Tag Specified
main
ui-mobile-first
codex/fix-action-button-reload
translation
qbit-2
docker
bugs
blitz/subtitles
fix/255-provider-reorder
blitz/feedback-3
subtitles/251-icon-vocab
subtitles/237-relocate-subs
fix/248-title-move-creates-root
blitz/feedback-2
issue/227-abandoned-pack-visible
issue/245-backoff-anchor
issue/240-design-coherence
issue/211-waived-rule
issue/232-plainer-words
issue/246-policy-reclassify
issue/244-root-slash-stored
issue/231-settings-rows
issue/241-reclassify-on-root-change
issue/239-failure-window
issue/230-icon-only
issue/243-root-path-edges
issue/238-attention-liveness
issue/229-back-button
issue/236-root-path-move
issue/226-attention-threshold
issue/228-root-move
blitz/size-bands
size-bands/209-runtime-scale
size-bands/210-size-waiver
size-bands/208-runtime-design
blitz/reliability
reliability/155-sqlx-migrations
reliability/176-refresh-on-add
removal-nav/175-removal-controls
removal-nav/174-season-file-removal
removal-nav/172-search-titles
removal-nav/173-homepage-library
removal-nav/171-untrack-clears
removal-nav/170-chip-align
removal-nav/169-design-amendment
feedback/167-deck-autosearch
feedback/165-chip-colour
feedback/166-header-rail
feedback/163-drop-cast
feedback/164-control-geometry
feedback/162-season-ordering
feedback/161-search-episodes
feedback/160-autotrack-seed
feedback/159-design-amend
blitz/rich-metadata
rich/151-grid
rich/148-search-rows
rich/150-series-meta
rich/149-movie-page
rich/147-search-art
rich/156-rating-null
rich/146-meta-api
rich/145-poster-cols
rich/144-trailer
rich/143-meta-detail
rich/142-design-amend
blitz/153-empty-episode-titles
blitz/152-deck-routes
blitz/141-season-vanished-api
blitz/129-series-detail-view
blitz/140-attention-episode-numbers
blitz/137-season-removals
blitz/133-attention-tv-lanes
blitz/139-movie-id-episode-guard
blitz/134-stale-attention-queue
blitz/124-rss-episode-matching
blitz/136-imdb-series-lookup
blitz/132-manual-commands
blitz/128-series-delete-files
blitz/131-status-seasons-input
blitz/138-plural-season-range
blitz/135-pack-name-parse
blitz/122-upstream-removals
blitz/130-add-series-search
blitz/121-series-refresh
blitz/127-unified-search-tv
blitz/123-episode-matching
blitz/126-tv-attention
blitz/119-tracked-rule
blitz/125-season-deck
blitz/120-tvdb-id
blitz/118-season-zero
blitz/117-design-amend
issue/115-avail-search
issue/112-name-truncate
issue/111-score-colors
No results found.
Labels
Clear labels
area/api
area/ci
area/compat
area/core
area/daemon
area/db
area/dl
area/indexer
area/infra
area/meta
area/parse
area/probe
area/subs
area/web
difficulty/easy
difficulty/hard
difficulty/moderate
difficulty/trivial
phase/1-skeleton
phase/2-logic
phase/3-sourcing
phase/4-movies
phase/5-ui
phase/6-tv
phase/7-people
phase/8-compat
phase/9-subtitles
type/bug
type/chore
type/feature
type/test
arr-api
CI and test harness
arr-compat
arr-core
arr-daemon
arr-db
arr-dl
arr-indexer
workspace and tooling
arr-meta
arr-parse
arr-probe
arr-subs crate: providers, extraction, translation, sync
web/
bounded, obvious approach, few files
subtle correctness or cross-cutting
multiple files, judgement, an interface to design
one file, mechanical, no design decisions
workspace, CI, config, database, empty API and SPA
parsing and policy engine, pure, no network
TMDB and Prowlarr, read-only
movies end to end
search, buckets, library views, queues
seasons, episodes, tracking, derived status
owner tags and notifications
Jellyseerr shim
build order: subtitles, DESIGN.md §15
Milestone
No items
No Milestone
Feedback pass 2
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: yolo/arr#228
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The root selector on a movie page changes the record and leaves the files where
they are.
movies::updatewritesroot_idand nothing else:So a title can claim to live in
Movies — kidswhile its folder sits under/mnt/media/movies/main. The §7.4 layout stops describing the disk, and thepolicy 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:
media_files.pathrowsare updated to match
Guard the cases that are not the happy path rather than assuming them away:
overwrite
title where its files actually are and be able to retry, which is the same
ordering
remove_library_filesdocumentsAdd 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_filespaths follow; a movie withno files moves cleanly; a name collision in the destination is refused and the
row is unchanged; a failed rename leaves the row alone.
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.pathhas to be rewrittenalongside
media_files.path, in the same transaction as theroot_idupdate.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 aroot'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.
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:
subtitle_files.pathhas to be rewritten in the same transaction asmedia_files.path; splitting it across two sessions would mean one of them shipping a half-move.blitz/subtitlesatd7e9f71, 720 tests green. Taking adifficulty/hardcross-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, andclaim_pathplus #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::JellyfinClientis where the §7.5 refresh lives now — #195 moved it out ofarr-daemonso 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.
Implementation notes from
issue/228-root-move(based onblitz/feedback-2):subtitle_filesdoes not exist on this base. The subtitles milestone lives onblitz/subtitlesand is not merged intoblitz/feedback-2, so there is no table and noarr-subscrate here to rewrite. The sidecar files already travel — the whole §7.4 title folder is renamed, contents included — but thesubtitle_files.pathrewrite has to be added in the same transaction asmedia_files.pathwhen the two branches converge. Whoever merges them: the rewrite loop incrates/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.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.