feat(media): tvdb names the show — episode titles, id-backed folders, park outside
The TV relayout that landed copied Jellyfin's Series names verbatim, and the
naming census showed what that launders in: one Series item per release folder
(1,107 items for 1,107 top-level dirs), the same show spelled six ways, and
box sets living in 346 separate folders for 821 identities. Jellyfin names a
Series after the folder it scanned, so `24 (2001) Season 1` is a folder echo,
not a show name.
The name authority is now tvdb, reached through the id the server matched:
* media-census.py jellyfin emits two more columns — `title` (the episode row's
own Name) and `tvdb` (the series id, read from BaseItemProviders where
Jellyfin 12 keeps provider ids and from the row's Data blob where older
builds do, so the census does not care which shape the live DB has);
* media-shows.py resolves each series to tvdb — the row's tvdb id, else its
TMDb id through TMDb's external ids, else an exact tvdb name search — and
writes tvdb's name and year. That also collapses the provider's spellings
into ONE identity, which is what turns a box set split over five release
folders into one folder and what makes two files claiming one episode MEET
in the duplicate rule;
* the episode file becomes `<Series> (<year>) - SxxEyy - <Title>`: the title
comes from tvdb's episode list, else the row's own title, else nowhere — no
title means no separator, never an invented one.
Two refusals come with it. A row whose series is the hidden top-level folder the
file itself sits in is NOT evidence: the review pile parked inside the library
was indexed as one show named `_unsorted`, and its rows would have renamed real
shows to it. And inside such a folder a file's own words may MATCH a registered
show but never INVENT one — invention is what filled the pile. `--park`
therefore belongs outside the library, which is also what makes the next plan
re-file the pile instead of skipping it.
Both providers answer offline-reproducibly: every answer is cached in one JSON
file, so a plan can be re-planned without a key. Lookups stay behind --tmdb and
--tvdb, and the tvdb token is read from a mode-600 file, never argv.
tests/media_shows.bats is 23 tests, none of which touch the network — the
fixture Jellyfin DB carries the providers table and the name cache is an input
file. ./tests/run.sh: 753 green.
This commit is contained in:
1 parent
5dbbfcb8a0
commit
c3ab565d9b
4 files changed
+463
-96
No files matched your search
@@ -187,12 +187,16 @@ show. A name whose marker is `Part 2` is a film and stays put.
|
||||
|
||||
`/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 same shape, and it never guesses: every series name and every
|
||||
episode number it writes traces to an authority, and what has none is parked for
|
||||
a human to read.
|
||||
library into the shape Jellyfin reads, with **tvdb as the name authority**:
|
||||
|
||||
<Series> (<year>)/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.
|
||||
|
||||
```bash
|
||||
# 1. The authority: Jellyfin's own matched rows (the DB opens read-only).
|
||||
# 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.
|
||||
@@ -201,10 +205,15 @@ python3 scripts/media-census.py enumerate --root /mnt/entropy/TVShows \
|
||||
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.
|
||||
# 3. The plan. --tmdb needs TMDB_API_KEY; without it a series no provider dated
|
||||
# is simply named without a year, which Jellyfin accepts.
|
||||
# 3. The plan. --tmdb needs TMDB_API_KEY, --tvdb needs ~/.config/secrets/tvdb
|
||||
# (`api_key=`, never argv); both write into the SAME cache file, so the plan is
|
||||
# reproducible offline afterwards. Run it once to fill the cache, then again:
|
||||
# the second run is the one whose numbers are quoted.
|
||||
python3 scripts/media-shows.py --root /mnt/entropy/TVShows --enum /tmp/tv_enum.tsv \
|
||||
--titles /tmp/tv_titles.tsv --probed /tmp/tv_probed.tsv --out /mnt/entropy/media-relayout-shows-plan.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-shows-plan.tsv \
|
||||
--exceptions /mnt/entropy/media-relayout-shows-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: a shared journal cannot be unwound.
|
||||
@@ -217,8 +226,11 @@ Evidence, strongest first — the order the planner believes things:
|
||||
- the **episode marker the file's own name states** (`S01E02-E03`, `1x04`);
|
||||
- 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**: identity (the series name and its year), and the
|
||||
numbering of a file whose own name says nothing;
|
||||
- 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;
|
||||
- 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`
|
||||
@@ -237,11 +249,22 @@ Two refusals carry the design, and a dry run on the real library found both:
|
||||
the file's own leading number, win; a row that still insists on season 1989
|
||||
parks its file instead of creating `Season 1989`.
|
||||
|
||||
The provider's name is copied verbatim, warts included: Jellyfin carries a Series
|
||||
named `Black Clover 001` (92 episodes, no year) because it matched a pack off a
|
||||
filename. Renaming a show is Jellyfin's work — a folder rename never touches its
|
||||
provider ids — so the mover keeps the name it is handed and strips only the pack
|
||||
index a release leads with (`4a. Seed DESTINY` → `Seed DESTINY`).
|
||||
**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 `--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 music library's own relayout
|
||||
|
||||
|
||||
Reference in new issue
Block a user