--- type: reference title: Media Relayout description: "Census, plan and apply the movie library into Jellyfin format: the gates to read, and the rollback." tags: [dankserver, media-server, jellyfin, plex, procedure] timestamp: 2026-10-30T00:00:00Z --- # Media Relayout The movie library on [[Entropy Volume]] arrived as release folders — `Some.Movie.2019.1080p.WiKiTaLx.x265-FOO/` with the video inside named after the release. [[Jellyfin]] matches a movie by folder and file name, so the release noise cost posters, extras and versions. These three scripts turn that library into Jellyfin's own shape, and nothing else in the repo moves a media file. | Script | Touches media? | Output | |---|---|---| | `scripts/media-census.py` | reads headers only | `enum`, `probed`, `plex` TSVs | | `scripts/media-plan.py` | no — TSVs in, TSVs out | the plan + the exception report | | `scripts/media-apply.py` | **renames, never copies** | the moves, plus a journal | The seam is `tests/media_relayout.bats`; the target contract is quoted from [jellyfin.org/docs/general/server/media/movies](https://jellyfin.org/docs/general/server/media/movies) in `media-plan.py`'s docstring. ## Procedure Run the census on the machine that mounts the library. `probe` is the slow step (a header read per container) and is `nice`d — it is safe beside a live service because it never competes for the GPU and never writes inside the library. ```bash C=scripts/media-census.py $C enumerate --root /mnt/entropy/Movies --out /tmp/media-enum.tsv $C probe --enum /tmp/media-enum.tsv --out /tmp/media-probed.tsv --jobs 6 $C plex --root /mnt/entropy/Movies --out /tmp/plex-movies.tsv ``` `plex` reads the Plex metadata cache (`--db` defaults to the path the [[Plex]] container bind-mounts at `/var/lib/plexmediaserver`) with `mode=ro&immutable=1`: a live server is never locked or written. Then plan — this reads only the three TSVs, so it costs seconds and can run anywhere, including a laptop: ```bash scripts/media-plan.py --root /mnt/entropy/Movies \ --enum /tmp/media-enum.tsv --probed /tmp/media-probed.tsv \ --plex /tmp/plex-movies.tsv --out /tmp/jf-plan.tsv --exceptions /tmp/jf-exceptions.txt ``` **Read `/tmp/jf-exceptions.txt` before applying.** It is not a log: every line is a file the plan could not name a film for, a pile that needs a README, or a collision it refused. The printed gate lines are the go/no-go: `problems`, `bad-names`, `occupied`, `double-claim`, `left-unmoved` must all be 0 and `unplanned=0` must appear in the report. Any non-zero number means a file that would be filed wrong, and the plan is not run until it is understood. ```bash scripts/media-apply.py apply --plan /tmp/jf-plan.tsv --root /mnt/entropy/Movies # dry, writes nothing scripts/media-apply.py apply --plan /tmp/jf-plan.tsv --root /mnt/entropy/Movies --apply scripts/media-apply.py verify --plan /tmp/jf-plan.tsv --journal …/media-relayout.jsonl scripts/media-apply.py rollback --journal …/media-relayout.jsonl --apply ``` After it lands, both servers need a scan or the change is invisible: [[Jellyfin]] *Library → Scan All Libraries*, and Plex's library scan (`podman exec plex …` or the web UI). `verify` reporting empty folders is normal for the folders the piles own; they are left in place per the deletion policy. ## Laws - **Plex's matched title outranks the filename**, and a Plex year of `0` means *indexed but never matched* — trusting it named folders `Title (0)`, which is a Jellyfin item with no year, no poster and no identity. A year that is not a plausible year is no information. - **`<>:"/\|?*` never survive a name.** Plex's own title for *V/H/S/2* is `V/H/S/2`; kept verbatim it became `Movies/V/H/S/2 (2013)/` — a tree five levels deep that neither matcher reads as a movie. - **A folder inside a movie's folder is bonus territory even when its name is misspelled.** `Saboteur (1942)/Exrtas/Saboteur - A Closer Look` is a typo for `Extras`; the vocabulary rule never fired, the featurette became a movie folder, and its words then out-scored the real film and drew that box set's other extras into it. The rule that replaced it needs no metadata: the folder holding the longest film owns the shorter video filed under it, unless that file's own name carries a different year — which is another film speaking. - **Matching words are split on everything that is not a letter or a digit.** Splitting on separators but not on `/` left tokens like `(1960)/extras` that no other name could ever agree with, so a film could not be found by the folder holding it, and extras drifted into a neighbour. A bare number counts only when it could be a release year, so `1080` and `2160` never pose as agreement between two names. - **A file whose name carries no words is bonus material, not a folder.** `.Extra 1 ( 1H 27min multisub ).mkv` parses to `1`; a movie folder named `1` is not a movie. It becomes a review line telling a human to name the file. - **The executor renames and nothing else.** `rename(2)` cannot cross a filesystem, so a plan that would is refused rather than silently copying tens of terabytes. - **The journal is a write-ahead log: the intent is fsynced before the rename.** An interrupted run therefore leaves only two states — an intent whose move happened, and an intent whose move never ran — and the next run finishes the job without moving anything twice. A file that moved with no journal line did not move through this tool, and the tool refuses to guess on its behalf. - **The journal lives BESIDE the library root, never inside it** (default: `media-relayout.jsonl` next to the root): a log inside the library is a file Jellyfin indexes. - **Refusals are all-or-nothing.** An occupied target, a vanished source or a duplicate destination stops the run *before the first rename*, so the operator fixes one thing and re-runs instead of untangling a half-applied plan. `--force` relaxes only the opinion guards; it never permits overwriting. ## Rollback One command, from the journal of the run: `media-apply.py rollback --journal …/media-relayout.jsonl --apply`. It restores every source path, removes only the folders that run created *and that are now empty*, and refuses everything if any moved file cannot be accounted for. A rolled-back journal refuses to roll back twice. ## See Also [[Jellyfin]], [[Plex]], [[Entropy Volume]], [[Testing Policy]], [[Storage Policy]] ## Citations - Jellyfin Movie documentation — folder/file naming, extras subfolders, `Movie (2013) - 216p.mkv` version and `- 4K.mkv` handling: