The movie library on the entropy volume is release-folded: the video inside
`Some.Movie.2019.1080p…-FOO/` is named after the release, and Jellyfin matches
a movie by folder and file name, so the noise cost posters, extras and versions.
Three scripts own the fix, and nothing else in the repo moves a media file.
* media-census.py the read-only inputs: a stat pass, an ffprobe header pass
(nice'd, so it is safe beside a live endpoint), and the
Plex metadata cache read that outranks any filename guess
* media-plan.py the Jellyfin-format planner: one folder per movie, the
video named exactly like the folder, extras in the
canonical subfolders, copies of one film as versions. It
opens no media file, so a whole-library plan costs seconds,
and it ends in gates — bad-names, occupied, double-claim,
left-unmoved, unplanned — that must all read zero before a
human points the executor at 24 TB
* media-apply.py the executor: rename(2) only, the intent fsynced to a
write-ahead journal BEFORE the move it authorises, so an
interrupted run is finishable and every run is undoable.
Refusals are all-or-nothing and it never overwrites a file
Laws the fixture pins (tests/media_relayout.bats, 16 tests, each proven red
against a reverted copy of the rule it guards):
* a Plex year of 0 means "indexed, never matched" — trusting it produced
folders "Title (0)": items with no year, no poster, no identity
* a Plex title carrying "/" is sanitised, or "V/H/S/2" became a five-level
tree neither matcher reads as a movie
* a folder inside a movie's folder is bonus territory even when its name is
misspelled ("Exrtas"), unless the file's own name carries another year
* matching words split on everything but a letter or digit — the earlier
tokenizer left "(1960)/extras" glued, so a film could not be found by the
folder holding it and one box set's featurettes drifted into a neighbour
* a name with no words in it is bonus material, never a movie folder named 1
Docs: the runbook page owns the procedure, the gates and the rollback; AGENTS.md
links it from the runbook index and the summary table.
6.6 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| reference | Media Relayout | Census, plan and apply the movie library into Jellyfin format: the gates to read, and the rollback. |
|
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
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 niced — it is safe beside a live service
because it never competes for the GPU and never writes inside the library.
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:
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.
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
0means indexed but never matched — trusting it named foldersTitle (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 isV/H/S/2; kept verbatim it becameMovies/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 Lookis a typo forExtras; 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)/extrasthat 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, so1080and2160never pose as agreement between two names. - A file whose name carries no words is bonus material, not a folder.
.Extra 1 ( 1H 27min multisub ).mkvparses to1; a movie folder named1is 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.jsonlnext 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.
--forcerelaxes 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.mkvversion and- 4K.mkvhandling: https://jellyfin.org/docs/general/server/media/movies