Files
serverAppConfigs/serverAppConfigs-docs/Dankserver/Media TV Relayout.md
T
Frank b232033b11 docs(tv): split the TV arm into its own page, add the folder-identity laws
Media Relayout.md crossed the OKF 500-line ceiling with the 2026-11-02 TV
laws, and the ceiling was right: the TV arm reads a different authority and
refuses for different reasons, like the music and audiobook arms that already
have pages. The arm moves verbatim to Media TV Relayout.md and gains two laws
from the TVShows pass: a folder naming two series is not evidence of two
series (ffprobe's embedded title tag answers per file), and right numbering
with a wrong name means rename the folder only. The executor page keeps the
executor laws and gains the hold-outside---root/--force law; Jellyfin.md gains
the settle read (ScheduledTasks State, since /Library/ScanStatus is a 404 on
12.4). Index regenerated with gen-vault-index.py.

cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
2026-11-02 09:06:22 -08:00

12 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.

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.

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