Keep sidecar paths in step when a video moves #234

Closed
opened 2026-08-25 08:36:54 +01:00 by naps62-yolo · 2 comments
Owner

subtitle_files.path is the row's identity on disk, and nothing keeps it in
step when the video moves. §7.4 renames on import, and a library move rewrites
media_files.path; the sidecar rows keep pointing at the old folder.

Consequences, all quiet:

  • The sidecars are still on disk under the old path and still read by Jellyfin
    if the folder survives, or orphaned if it does not.
  • claim_path and #222's one-sidecar invariant both reason about a path that
    no longer exists, so arr can write a second sidecar beside the moved video
    while believing the language is already satisfied.
  • #218's delete unlinks a path that is not there any more, and reports success.

Rewrite subtitle_files.path wherever media_files.path is rewritten, in the
same transaction, and move the files with the video. A sidecar lives inside the
§7.4 title folder precisely so that a folder-level move takes it along — the
database is the part that does not follow.

Pre-existing; found while implementing #222 and not caused by it.

Depends on: #186, #195

`subtitle_files.path` is the row's identity on disk, and nothing keeps it in step when the video moves. §7.4 renames on import, and a library move rewrites `media_files.path`; the sidecar rows keep pointing at the old folder. Consequences, all quiet: - The sidecars are still on disk under the old path and still read by Jellyfin if the folder survives, or orphaned if it does not. - `claim_path` and #222's one-sidecar invariant both reason about a path that no longer exists, so arr can write a second sidecar beside the moved video while believing the language is already satisfied. - #218's delete unlinks a path that is not there any more, and reports success. Rewrite `subtitle_files.path` wherever `media_files.path` is rewritten, in the same transaction, and move the files with the video. A sidecar lives inside the §7.4 title folder precisely so that a folder-level move takes it along — the database is the part that does not follow. Pre-existing; found while implementing #222 and not caused by it. Depends on: #186, #195
naps62-yolo added this to the Subtitles milestone 2026-08-25 08:36:54 +01:00
naps62-yolo added the area/dbdifficulty/moderatetype/bugphase/9-subtitles labels 2026-08-25 08:36:54 +01:00
Author
Owner

Investigated before implementing. This issue is blocked: there is currently no code path anywhere in the repo that rewrites an existing media_files.path to a different value, so there is no "video move" for subtitle_files.path to fall out of step with.

Checked:

  • Every INSERT/UPDATE touching media_files (arr-api, arr-daemon, arr-compat). The only path-changing statement is the crash-recovery upsert in record_import/record_episode_import (ON CONFLICT (path) DO UPDATE), which only fires when the same path is re-imported after a crash — it never changes the stored path to something new.
  • Every impl Action for ... registered with the reconcile loop (import, grab, tv_grab, series_refresh, reaper, broken, subtitles, attention, rss). None relocates an already-imported file.
  • PUT /api/roots/{root_id} reconfigures a root's path (a settings correction) but never touches media_files rows or moves files.
  • movies::update accepts root_id, and the web UI exposes a root-select dropdown on the movie detail page (web/src/main.ts ~2358) that calls it — but the handler only writes the movies.root_id column. It doesn't move the file on disk or rewrite media_files.path. Changing a movie's root today silently desyncs the file from its new root/audience with no relocation at all.
  • DESIGN.md §7.4/§7.5 describe import-time placement only; no move/relocate mechanism is specified anywhere in the document.

Since nothing moves a video's media_files.path yet, there's no transaction to hook subtitle_files.path into, and building a trigger or helper against a caller that doesn't exist would be speculative infrastructure the project's conventions explicitly avoid. The move feature itself needs its own issue: physically relocate the file, rewrite media_files.path, move the sidecars, and rewrite subtitle_files.path, all together. This issue can be re-scoped to be part of that work (or reopened against it) once the move exists.

Branch: subtitles/234-sidecar-on-move (no code changes — nothing to implement against). just ci not run since nothing changed.

Findings

  • movies::update accepts root_id and the web UI lets an operator change a movie's root/audience from the detail page, but the file is never moved and media_files.path is never rewritten to match the new root. The movie silently ends up tracked under a root/audience its file doesn't physically live under. This is the real gap #234 is downstream of — worth its own issue (implement the actual move: relocate the file, update media_files.path, move sidecars, update subtitle_files.path, in one transaction).
Investigated before implementing. This issue is blocked: there is currently no code path anywhere in the repo that rewrites an existing `media_files.path` to a different value, so there is no "video move" for `subtitle_files.path` to fall out of step with. Checked: - Every `INSERT`/`UPDATE` touching `media_files` (`arr-api`, `arr-daemon`, `arr-compat`). The only path-changing statement is the crash-recovery upsert in `record_import`/`record_episode_import` (`ON CONFLICT (path) DO UPDATE`), which only fires when the *same* path is re-imported after a crash — it never changes the stored path to something new. - Every `impl Action for ...` registered with the reconcile loop (import, grab, tv_grab, series_refresh, reaper, broken, subtitles, attention, rss). None relocates an already-imported file. - `PUT /api/roots/{root_id}` reconfigures a root's `path` (a settings correction) but never touches `media_files` rows or moves files. - `movies::update` accepts `root_id`, and the web UI exposes a root-select dropdown on the movie detail page (`web/src/main.ts` ~2358) that calls it — but the handler only writes the `movies.root_id` column. It doesn't move the file on disk or rewrite `media_files.path`. Changing a movie's root today silently desyncs the file from its new root/audience with no relocation at all. - DESIGN.md §7.4/§7.5 describe import-time placement only; no move/relocate mechanism is specified anywhere in the document. Since nothing moves a video's `media_files.path` yet, there's no transaction to hook `subtitle_files.path` into, and building a trigger or helper against a caller that doesn't exist would be speculative infrastructure the project's conventions explicitly avoid. The move feature itself needs its own issue: physically relocate the file, rewrite `media_files.path`, move the sidecars, and rewrite `subtitle_files.path`, all together. This issue can be re-scoped to be part of that work (or reopened against it) once the move exists. Branch: `subtitles/234-sidecar-on-move` (no code changes — nothing to implement against). `just ci` not run since nothing changed. ## Findings - `movies::update` accepts `root_id` and the web UI lets an operator change a movie's root/audience from the detail page, but the file is never moved and `media_files.path` is never rewritten to match the new root. The movie silently ends up tracked under a root/audience its file doesn't physically live under. This is the real gap #234 is downstream of — worth its own issue (implement the actual move: relocate the file, update `media_files.path`, move sidecars, update `subtitle_files.path`, in one transaction).
Author
Owner

Closing: the premise of this issue was wrong, and the worker was right to push no code.

I wrote that "§7.4 renames on import and library moves rewrite media_files.path". The second half is not true — nothing in the codebase relocates an imported file. The reconcile loop's nine actions all leave placement alone, and PUT /api/roots/{id} rewrites a root's path without touching files. So there is no transaction for subtitle_files.path to hook into, and adding a helper against a caller that does not exist would be speculative infrastructure.

The real gap is upstream and now filed as #235: PATCH /api/movies/{id} accepts root_id and the UI offers a root dropdown, but the handler only writes the column — the file is never moved. Keeping sidecar paths in step is one step of implementing that move, not a separate piece of work, so it belongs in #235 rather than here.

Reopen against #235 if the move lands and the sidecar half gets skipped.

Closing: the premise of this issue was wrong, and the worker was right to push no code. I wrote that "§7.4 renames on import and library moves rewrite `media_files.path`". The second half is not true — **nothing in the codebase relocates an imported file**. The reconcile loop's nine actions all leave placement alone, and `PUT /api/roots/{id}` rewrites a root's path without touching files. So there is no transaction for `subtitle_files.path` to hook into, and adding a helper against a caller that does not exist would be speculative infrastructure. The real gap is upstream and now filed as #235: `PATCH /api/movies/{id}` accepts `root_id` and the UI offers a root dropdown, but the handler only writes the column — the file is never moved. Keeping sidecar paths in step is one step of implementing that move, not a separate piece of work, so it belongs in #235 rather than here. Reopen against #235 if the move lands and the sidecar half gets skipped.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#234