TMDb has no TV side: a series, a season and an episode get nothing from it #106
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
crates/jellyfin-app/src/metadata/tmdb.rshas one entry point —movie()— plusimages(). Upstream has six more in-box providers against the same API:TmdbSeriesProvider,TmdbSeasonProvider,TmdbEpisodeProviderand the three image providers beside them, all underMediaBrowser.Providers/Plugins/Tmdb/TV/.So a TV library here fills from filename and NFO only: no overview on a series, no episode titles or air dates beyond what the file names, no season posters, no series backdrop, no cast.
The parts already built that this rides on: the
MetadataHttpport and its cassette (D27), the merge rule with its lock set, the download-and-file path for the pictures a film won, and the person rows a cast writes. What is new is the request shapes, the season/episode identification (an episode is looked up by series id plus season and episode number, not by an id of its own), and the fixture — the enriched library will need a series named by[tmdbid-…]the way the films are.TmdbMissingEpisodeProviderand itsTmdbRefreshUpcomingEpisodestask hang off the same client and belong here or immediately after: they are what fills/Shows/Upcoming, whichENDPOINTS.mdcurrently answers with what the fixture's missing premiere dates allow.Where to start
crates/jellyfin-app/src/metadata/tmdb.rshasmovie()andimages()and theclient machinery around them — the append-to-response list, the crew filter, the
cast ceiling, the id-then-external-id lookup order. Six more providers hang off
the same client:
MediaBrowser.Providers/Plugins/Tmdb/TV/{TmdbSeriesProvider,TmdbSeasonProvider,TmdbEpisodeProvider}.csand the three image providers beside them.
What is genuinely new, and worth reading upstream for rather than inferring:
season and episode number, which means the series has to have resolved first
and the ordering matters.
different language fallbacks.
TmdbMissingEpisodeProviderand itsTmdbRefreshUpcomingEpisodestask arethe same client again and are what fills
/Shows/Upcoming, which todayanswers what the fixture's absent premiere dates allow.
The fixture has to grow. M10's films are named
[tmdbid-…]so that a lookupis by id and the recording is stable; a series needs the same treatment before
any of this can be measured.
reference/README.mdsays how the enrichedsnapshot is made and re-provisioned, and the cassette is regenerated beside the
answer key rather than hand-written.
How to know it is done
A series, a season and an episode scanned here carry the same fields as the rows
upstream fetched into the enriched snapshot — every column, the
Datablobincluded — and the artwork matches on bytes. That is the same bar M10's films
met, applied to the TV side.
House rules
CLAUDE.mdis the short version and these are the parts that bite: tests rununder
cargo nextest; upstream's source is the checkout inupstream/at thepin (
v12.0-rc5,6d501ba4) and not the clone beside the repository;generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and
Dnreferences stay out of code and comments.ARCHITECTURE.mddecides which crate a thing goes in and which dependency edgesare illegal,
DECISIONS.mdsays why a rule exists.The reference pair is
cargo xtask reference up(andreference provision,which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,with
diff-dbfor the two databases.crates/jellyfin-harness/README.mdsayswhat a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.
Done, on
main. The six providers, the scan wiring, the fixture, and the answer key that measures them.What it answers now
A series, a season and an episode take from TMDb what upstream takes: name, original title, overview, official rating, original language, production year, premiere date, end date, run time, genres, studios, tags, community rating,
Statusin the blob, and all four provider ids at series level — measured against rows the pinned upstream image wrote forFirefly (2002) [tmdbid-1437]andTwin Peaks (1990) [imdbid-tt0098936], the second resolved through/findso both halves of how a lookup starts are covered.The fixture grew to carry them: three clips under
enriched/shows, a secondtvshowslibrary with fetchers on, and a regeneratedenrichedsnapshot. The answer key is 11 items, 99 persons, 104 images, 156 credits, 118 values, 125 recorded answers.JELLYFIN_RS_REQUIRE_FIXTURES=1 cargo nextest run --workspace: 2219 passed, 0 skipped.The cassette stopped being a liability
tmdb.jsonwas 13 MB of base64 rewritten wholesale on every regeneration, and adding TV would have made it ~40 MB. Bodies now live in content-addressed files beside it and the JSON is 1.1 MiB.Cassetteowns the layout so the generator and the replay cannot drift — they are in different crates, which is exactly how they would have.The conversion was proven rather than asserted: 46 keys before and after, zero semantic differences among the 42 unchanged, exactly four re-spelled for the
append_to_responsefix with byte-identical answers, and all 40 base64 bodies decoding to the file that now holds them. Three guards keep it that way — the recording is byte-for-byte what the writer emits, every named body is on disk and every body on disk is named, and each is filed under the digest of what it holds.Recorded::answernow fails on a missing key and on a named-but-absent file, where it used to answer an empty body.deny_unknown_fieldscloses the same hole one format-version away: an old-format recording is refused rather than answering200with nothing.Two things this got wrong before it got right, and both are method
A self-made answer key is not a check. The first measurement reported 13 of 13 pictures byte-identical — against a cassette the port had recorded itself, so it compared the port's choices to a recording of the port's own choices. The committed corpus was the first independent check, and it disagreed on seven pictures. That is M10's "a check on a projection is not a check" one level up: not a projection, a self-made key.
And then byte equality turned out to be unsound anyway. Fetching each disputed URL showed the port's chosen address serving exactly upstream's bytes, seven times out of seven — so the selection was right and the two halves of the key disagreed.
image.tmdb.orgserves two JPEG encodings of one picture: Tom Skerritt's portrait is baseline at 1,590,787 bytes and progressive at 1,121,471, same 2000x3000 pixels, andAccept: image/webpproves negotiation is live on that CDN. Upstream downloaded during provisioning and the generator recorded minutes later; they got one each.D30 is the rule that came out of it: the answer key records which picture upstream chose, discovered by walking the ranked candidates until one is the file the snapshot holds, and the test asserts the port's chosen path against it. The pairing is discovered from upstream's bytes rather than asserted from our ranking, so a ranking both sides share and both get wrong is still caught. A candidate list where nothing matches stops the cut and names the row. Bytes are still compared where that is sound — a picture we filed equals the picture we were handed — and never across two downloads.
Worth recording: progressive and baseline do not decode to the same blur hash. They differ in two characters, which is why the recording takes upstream's own file once the address is established.
What the review changed
/find, where upstream reads only the Tmdb id it was handed and answers nothing without it. A series carrying only a TVDb id got nothing from upstream and a full set from us. The series is now identified once per library, before any row is read.78874-en,series-firefly-en-001) and stayed — reproducing a transient upstream state as a permanent one.Knowncarries the persisted key now and a test asserts both passes.Tvdb 78874and70533." -en") written down, rather than a test asserting our rule as theirs.crew_ofcapped before the name check where the film path caps after; studios were not deduped;Statuswas read out of the blob and hardcodedNoneon the way in; episode trailers carried a name upstream does not write; episodepremiere_dateis upstream's one local-time date. All fixed.Left open, named
TmdbSeasonProvider,AddPerson,UpdatePeopleorMergePeople, and the only.Reverse()in four assemblies being on episodes. Against the regenerated corpus the two now agree, which suggests the earlier reversal was TMDb drift in a scout container rather than a rule.scan_enrichedcompares crew elementwise byListOrder, so if it is a rule it will fail there and say so.TmdbMissingEpisodeProviderstays out: 663 lines plus a scheduled task, sharing only the client, and it is what fills/Shows/Upcoming.EnableAutomaticSeriesGroupingis unmodelled — default true, and when off upstream keys a seriesId.ToString("N").#105 stays open: OMDb, MusicBrainz and AudioDb, the person provider and the generated images are still unwritten.
One thing this turned up that belongs to the whole workspace rather than here, filed as #137: thirty-five tests skip themselves by printing a note and returning, which nextest reports as passed. Fifteen were vacuous in the worktree this was developed in, including the one test that checks the presentation-key rule against real rows.