A parked folder whose name parses to a registered show merges into the row that owns the show. The proof is the episode rows standing under the owner folder — never the id-less folder list, because one tvdb id can carry two `Series` rows and an id-less folder that parses gets identified on its own and keeps its own folder, offering the show twice. No library file moves while `Scan Media Library` is Running: wait for a Running sighting and then two consecutive Idle polls, because a gap between sub-tasks reads as finished. Both sides speak different path dialects (the server records the container coordinate, the journal speaks the array), so a join that forgets the translation reads as an empty library. Lands on [[Media TV Relayout]] — the page that owns the TV arm — so `Media Relayout.md` stays under the OKF ceiling.
15 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| reference | Media TV Relayout | Re-file TVShows from scene packs into the shape Jellyfin reads: authority order, refusals, the two-series split test. |
|
2026-11-02T11:00:00Z |
Media TV Relayout
The /mnt/entropy/TVShows share arrived as scene packs — release folders whose
names are the release, not the show. This arm re-files it into the shape
Jellyfin reads, with tvdb as the name authority. The executor, the journal,
the sweep and the rollback belong to Media Relayout; this page owns the TV
authority order, the refusals, and the test that decides a split.
/mnt/entropy/TVShows is scene packs rather than a library, and Jellyfin has
already matched and numbered most of it. scripts/media-shows.py re-files that
library into the shape Jellyfin reads, with tvdb as the name authority:
<Series> (<year>) [tvdbid-NNNN]/Season NN/
<Series> (<year>) - SxxEyy - <Episode Title>.ext
It never guesses: every series name, episode number and title it writes traces to an authority, and what has none is parked for a human to read.
# 1. The authority: Jellyfin's own matched rows — each carries the series' TMDb
# and Tvdb ids and the title the row holds (the DB opens read-only).
python3 scripts/media-census.py jellyfin --root /mnt/entropy/TVShows \
--out /tmp/tv_titles.tsv
# 2. What is on disk (stat only), and the runtimes the film arm needs.
python3 scripts/media-census.py enumerate --root /mnt/entropy/TVShows \
--out /tmp/tv_enum.tsv
python3 scripts/media-census.py probe --root /mnt/entropy/TVShows \
--enum /tmp/tv_enum.tsv --jobs 6 --out /tmp/tv_probed.tsv
# The probe is the slow step: ffprobe reads both ends of every file. So re-use
# the last one — a duration belongs to the file, not to its name: re-key the
# previous census through that pass's own journal (`src` -> `dst`) instead of
# seeking the array again. It is also the only column the film arm reads, and
# only for a file with no marker and no provider row (4,794 of 58,551 today,
# all of them covered by a re-key that costs seconds, not an hour).
# 3. The plan. --tmdb needs TMDB_API_KEY — the v3 key in ~/.config/secrets/tmdb,
# which travels as the `api_key` query parameter. Only a v4 request token is
# accepted as a Bearer header, and the wrong shape answers 401 on EVERY call,
# which the summary line flags as NOT provider-complete. --tvdb reads its
# `api_key=` line from ~/.config/secrets/tvdb, never argv. Both write the SAME
# cache file, so the plan is reproducible offline afterwards: run it once to
# fill the cache, and quote the second run.
# Paths are only strings to a dry run, so this step can run on the workstation
# against scp'd TSVs — which is where the two provider secrets live.
python3 scripts/media-shows.py --root /mnt/entropy/TVShows --enum /tmp/tv_enum.tsv \
--titles /tmp/tv_titles.tsv --probed /tmp/tv_probed.tsv \
--tmdb-cache /mnt/entropy/media-tv-names.json --tmdb --tvdb \
--park /mnt/entropy/media-park/TVShows \
--out /mnt/entropy/media-relayout-shows2-plan.tsv \
--exceptions /mnt/entropy/media-relayout-shows2-exceptions.txt
# 4. Read the exceptions file FIRST — it is the review, not a log. Same executor
# as above, but on this arm's OWN journal, NAMED PER PASS: the journal of an
# applied pass is its unwind record, so a second pass that reuses the name
# destroys the only map back.
scripts/media-apply.py apply --plan /mnt/entropy/media-relayout-shows2-plan.tsv \
--root /mnt/entropy --journal /mnt/entropy/media-relayout-shows2.jsonl --apply
Evidence, strongest first — the order the planner believes things:
- the episode marker the file's own name states (
S01E02-E03,1x04); a digit run longer than three at that position is NOT a number —S03E2019andS04E47370355are one box set's airdate fragment and pack index — so such a file is renumbered by tvdb's title or parks, and is never truncated down to fit; - the
Season NNfolder it sits in and the number its name leads with inside it:Season 19/01 Charles Barkley, Nirvana.aviis S19E01; - the provider row: which episode the file IS, and the numbering of a file whose own name says nothing;
- the row's tvdb id, else its TMDb id carried through TMDb's external ids, else
an exact tvdb name search: that id names the SERIES FOLDER and supplies the
episode title, because a name the server read off a folder is not a show name.
The folder CARRIES the id:
[tvdbid-NNNN]is what Jellyfin's Tvdb agent and Plex's scanner both read, so a show whose title is ambiguous resolves by number instead of by a human guessing which folder is which. The id rides the folder only — a file name gets 180 characters and the episode title needs the room. - a bonus folder anywhere above the file, or Jellyfin's
ExtraType→ the series' canonical extra folder; - a runtime over
SHORT_MAXmakes it film-shaped, and it leaves for--moviesonly when the movie provider's title is a run of words the FILE itself states and its year agrees with the file's: the query is only whattitle_ofsalvaged from a release name, and the first hit for a short one is a near miss — a 1963 Bond film for the word "russia", Special Edition I for a file saying Edition III. Else it parks.
Four refusals carry the design: what the planner will NOT do is the point of it.
- Folders resolve a series; they never invent one. A folder's words may
resolve a file to a series a provider has already named —
(CBB) Tengen Toppa Gurren Lagann - The Complete Series (BD 1080p)names the registered Tengen Toppa Gurren Lagann — but a bonus subfolder is not a show. An early run made…/03. Extra/01. NCOP/NCOP 01.mkvinto a root series called01 NCOP. - A scraped row does not outrank the filesystem. Jellyfin numbers
multi-season packs badly: every file under
SNL 16-20/Season 16..19carries season 16 episode 20, and one Gundam row says season 2004. The folder, then the file's own leading number, win. - A season that is a YEAR is the date matcher, not a season. Jellyfin files
specials by airdate, so a row can arrive saying
season 2002; that is not a season and it never becomes a folder. The one authority on such a row is tvdb's episode list read backwards: the title the row carries names the (season, episode) tvdb gives it. A title tvdb gives to two episodes names nothing, and the file parks with the reason it always carried.Season 00IS a legal season — specials live there — so a row that really says 0 files there instead of parking. - A season tvdb's own list contradicts gets no tvdb title. An anime pack
numbered by its release group books
S05E01in a season tvdb does not hold, or books more episodes there than tvdb lists: the slot its number points at is a different episode, and dressing it in that episode's title is a confident lie. The file still renames to the standard shape; the title does not ride, and the series is listed under seasons whose numbering tvdb contradicts in the exceptions. A season holding FEWER files than tvdb lists stays plausible — a partial pack is the normal shape. Neither reading holds for a list tvdb cut off at its own page limit: the default episode query answers at most 500 rows and states no total, and One Piece, Crayon Shin-chan and Detective Conan each answer exactly 500 while holding over a thousand — a page bounds nothing above itself, so it contradicts nothing.
A provider name is evidence only when an id backs it. Jellyfin names a Series
after the folder it scanned, so a library filed by release folders ends up with
one Series item per box set — 24 (2001) Season 1, Modern Family S05 — and
copying those names verbatim launders folder junk into the contract. The planner
resolves each series to its tvdb id and writes tvdb's name and year; the merged
identity is also what turns a box set split across five release folders into ONE
folder, and what makes two files claiming one episode MEET in the duplicate rule.
A series no id resolves keeps the provider's words and is listed under no tvdb
identity in the exceptions — that list, not a rename, is the review.
The review pile lives outside the library. Files parked inside
/mnt/entropy/TVShows were indexed as one show named _unsorted, and that
show's rows then looked like evidence. So a row whose series is the hidden
top-level folder the file itself sits in is dropped — and the folder is matched
with its leading underscore stripped, because the server displays _unsorted as
unsorted: compare the spelled names and the pile survives the relayout as a
visible series folder that real files get renamed into. --park outside the root
makes the next plan re-file the pile instead of skipping it. Inside a hidden
top-level folder a file's own words may MATCH a registered show, never invent one.
The operator's id map is the door for a name tvdb cannot be searched by.
scripts/media-shows.py reads scripts/media-shows-tvdb-ids.tsv by default: one
line per pack folder, folder <TAB> tvdb id <TAB> note. The file exists because
tvdb titles a record in the show's own language and the English name rides in
aliases the name search never reads: ブラッククローバー (331753) answers to no
spelling of "Black Clover", and the bare alias One Piece belongs to 392276, the
2023 live action, not to the 1999 record (81797) the pack here is. The map
outranks a provider id — a box set's provider id is the server's own guess at a
folder name — and it merges the release folders that are one show. Everything it
touched is written to the exceptions: every folder it named, every row that
matched no folder, every disagreement with a provider id, every id tvdb never
answered. A line whose id is not a number stops the plan, because a typo renames
a whole pack to the wrong show. Only folders the search CANNOT reach belong in
it; a row for a folder the search would resolve hides a live lookup behind a
number nobody re-checks.
scripts/media-shows-tvdb.py makes the sheet the map is built from. It reads a
plan's mkdir rows, asks tvdb about every destination folder carrying no
[tvdbid-, and writes one row per folder with the verdict that says how much was
guessed to get there: EXACT (tvdb's own spelling), PUNCT (equal only after
release junk is stripped from the folder's words), NEAR (tvdb answered
something else — the best hit is printed for a human and nothing applies it),
NONE. Acceptance never moved across the passes that built this: a hit counts
only when tvdb's name equals the query with every non-alphanumeric dropped from
both sides, and the record's year still has to fall inside any year span the
folder names. That gate is what caught three confident picks — Fairy Tail's 2024
sequel (410031), the 2026 Hokuto no Ken remake (465216) and Mugyutto! Black Clover (378762) offered for Black Clover. It is why the map is approved by hand
and the sheet is never fed to the planner.
A parked folder merges into the row that owns the show. The folder that never
got an id usually holds episodes the identified folder for that show lacks, and
the server shows the audience neither: the parked copy is invisible and the owner
reads as a short series. One media-apply.py plan per show, built from the
server's own rows:
- the owner is the folder whose
Seriesrow carries the tvdb id; the parked side is the folder with the same words and no id (or the same id under a second row — see the law below); - the comparison is episode identity, not filename: which (season, episode)
the provider row gives each file, read straight out of the server. Release
folders renumber — one park carried
S01E01-11of a show tvdb books as seasons 2 and 3, and every episode runs the same eleven minutes, so neither the marker nor the duration says what one query into the rows says; - a season the owner has no file in moves whole:
mkdirtheSeason NN, then one row per file with its name unchanged — the folder shape is the contract, the file name is not; - a subtitle pack is re-keyed, never just moved: a sidecar attaches only when
its stem equals the video's, so each
.srthas to land on the OWNER's stem with the language suffix kept (… - The Cat and the Claw (1).en.srt). Moved under its release-group name it stays a folder of 88 subtitles no player ever offers; - the same episode on both sides is the operator's call, never a plan row: which copy survives is a quality judgment — the parked copy is usually the bigger one, and a re-cut is not a worse one — and the parked file often carries no title to align by. Those pairs are listed and left alone.
The two sides speak different path dialects, since the server records the container coordinate (Jellyfin) and the journal speaks the array; a join that forgets the translation returns zero rows and looks like an empty library.
Laws of this arm
- A folder that names two series is not evidence of two series — the files
answer.
ffprobe -show_entries format_tag=titlenames the series each file belongs to. Measured 2026-11-02 onCOW and CHICKEN … Complete Animated TV Series, a bundle whose name pairs Cow and Chicken with I Am Weasel: 103 of 104 files carry aCow and Chicken - SxxEyy - <title>tag and none carries an I Am Weasel one. The 7-minute duration class is a half-episode segment, not a second show, and a file holding both segments needs a remux, which this executor will never do. Split only when the tags (or a provider episode list) name both series per file; everything else is a rename. - When the numbering is right and only the name is wrong, rename the folder
and nothing else. Tags whose
Sxxmatches theSeason NNfolder holding them, per-season counts equal, and no duplicateSxxEyykey anywhere prove the layout is already correct. Renumbering files under that evidence buys nothing and can silently mislabel an episode. - One tvdb id can carry two
Seriesrows, so the id-less folder list proves nothing about a merge. A parked folder whose words parse to a registered show gets identified on its own and keeps its own folder: the count of id-less folders drops, and the library now offers the same show twice. Prove a merge by the episode rows standing under the owner folder, never by that list. - Move library files only while
Scan Media Libraryis Idle. A rename that lands under a running scan is a row the server read a moment ago pointing at a path that no longer exists.ScheduledTasksis the read (Jellyfin); wait for aRunningsighting and then two consecutiveIdlepolls, because a gap between sub-tasks reads as finished.
See Also
Media Relayout — the executor, the journal, the sweep and the rollback this arm shares. Jellyfin — the reader of the resulting shape. Plex — the second reader. Media Metadata View — where a rename's sidecars strand, or do not.
Citations
- Jellyfin Shows documentation — series, season and episode folder/file naming: https://jellyfin.org/docs/general/server/media/shows