Files
serverAppConfigs/serverAppConfigs-docs/Dankserver/Media TV Relayout.md
T
Frank 8f0b1982c2 docs(tv): the parked-folder merge, and the laws that gate it
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.
2026-11-02 13:56:03 -08:00

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.
dankserver
media-server
jellyfin
plex
procedure
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 — S03E2019 and S04E47370355 are 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 NN folder it sits in and the number its name leads with inside it: Season 19/01 Charles Barkley, Nirvana.avi is 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_MAX makes it film-shaped, and it leaves for --movies only 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 what title_of salvaged 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.mkv into a root series called 01 NCOP.
  • A scraped row does not outrank the filesystem. Jellyfin numbers multi-season packs badly: every file under SNL 16-20/Season 16..19 carries 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 00 IS 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 S05E01 in 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 Series row 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-11 of 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: mkdir the Season 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 .srt has 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=title names the series each file belongs to. Measured 2026-11-02 on COW and CHICKEN … Complete Animated TV Series, a bundle whose name pairs Cow and Chicken with I Am Weasel: 103 of 104 files carry a Cow 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 Sxx matches the Season NN folder holding them, per-season counts equal, and no duplicate SxxEyy key 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 Series rows, 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 Library is 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. ScheduledTasks is the read (Jellyfin); wait for a Running sighting and then two consecutive Idle polls, 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