A season keeps a series key its series has moved off, and /Shows/{id}/Seasons answers nothing #177

Closed
opened 2026-08-22 17:01:17 +02:00 by Claude · 1 comment
Collaborator

Found by clicking through the throwaway instance in TESTCONTAINER/ — a real
library of 294 series and 717 seasons, scanned by the image built at 33a1bdd.

What it looks like

A series page renders a Seasons heading with nothing under it, and no
Next Up. There is no way to reach an episode from the series page at all.

What answers

GET /Shows/{seriesId}/Seasons           -> TotalRecordCount 0
GET /Shows/{seriesId}/Episodes          -> TotalRecordCount 0     (no seasonId)
GET /Shows/{seriesId}/Episodes?seasonId -> TotalRecordCount 10    (fine)
GET /Items?ParentId={seriesId}          -> the four seasons       (fine)

Why

Both empty answers query on series_presentation_unique_key
(crates/ephyra-http/src/routes/library/tv_shows.rs:266 and :351), and across
this library 704 of 717 seasons carry a SeriesPresentationUniqueKey that
their series' own PresentationUniqueKey does not equal:

Series  Ted Lasso   PresentationUniqueKey        = tt10986410-en
Season  Season 1    SeriesPresentationUniqueKey  = series-ted lasso-en

series_presentation_key keys on Custom, then Tvdb, then Imdb, and falls
back to series-<folded name> (crates/ephyra-core/src/item/keys.rs:120). The
seasons were written while the series had no id, and the series was re-keyed
when its Imdb id landed — see #176 for where that id comes from — without
the rows below it being re-keyed with it.

The 13 that match are the 9 series that never resolved an id at all, so both
sides kept the name-derived key.

season_presentation_key's doc comment names this order dependence and says the
next pass makes the season catch up. It does — a per-item Replace all metadata on Ted Lasso re-keyed its four seasons and the page filled in — but
a library rescan does not: RefreshLibrary completed at 13:27 and again at
14:15 with the mismatch untouched, because a pass that finds no changed file
rewrites no row. A library scanned once and left alone stays broken.

Upstream

Season.FindSeriesPresentationUniqueKey() resolves the live series' current key
at save time (upstream/MediaBrowser.Controller/Entities/TV/Season.cs:237), so
a child cannot hold a key its parent has moved off — the two converge inside one
pass rather than on the next one.

Where to start

Either re-key the children when a series' PresentationUniqueKey changes, or
resolve the key at write time from the series row the way upstream does rather
than carrying whatever the child was created with.

How to know it is done

SELECT over a scanned fixture returns no season or episode whose
SeriesPresentationUniqueKey differs from its series' PresentationUniqueKey,
after one pass; /Shows/{id}/Seasons answers with the seasons for a series
that acquired an id during that same pass.

House rules

Tests run under cargo nextest; upstream's source is the checkout in
upstream/ at the pin. Generated artifacts are regenerated, never edited.

Found by clicking through the throwaway instance in `TESTCONTAINER/` — a real library of 294 series and 717 seasons, scanned by the image built at `33a1bdd`. ## What it looks like A series page renders a `Seasons` heading with nothing under it, and no `Next Up`. There is no way to reach an episode from the series page at all. ## What answers ``` GET /Shows/{seriesId}/Seasons -> TotalRecordCount 0 GET /Shows/{seriesId}/Episodes -> TotalRecordCount 0 (no seasonId) GET /Shows/{seriesId}/Episodes?seasonId -> TotalRecordCount 10 (fine) GET /Items?ParentId={seriesId} -> the four seasons (fine) ``` ## Why Both empty answers query on `series_presentation_unique_key` (`crates/ephyra-http/src/routes/library/tv_shows.rs:266` and `:351`), and across this library **704 of 717** seasons carry a `SeriesPresentationUniqueKey` that their series' own `PresentationUniqueKey` does not equal: ``` Series Ted Lasso PresentationUniqueKey = tt10986410-en Season Season 1 SeriesPresentationUniqueKey = series-ted lasso-en ``` `series_presentation_key` keys on `Custom`, then `Tvdb`, then `Imdb`, and falls back to `series-<folded name>` (`crates/ephyra-core/src/item/keys.rs:120`). The seasons were written while the series had no id, and the series was re-keyed when its `Imdb` id landed — see #176 for where that id comes from — without the rows below it being re-keyed with it. The 13 that match are the 9 series that never resolved an id at all, so both sides kept the name-derived key. `season_presentation_key`'s doc comment names this order dependence and says the next pass makes the season catch up. It does — a per-item `Replace all metadata` on *Ted Lasso* re-keyed its four seasons and the page filled in — but **a library rescan does not**: `RefreshLibrary` completed at 13:27 and again at 14:15 with the mismatch untouched, because a pass that finds no changed file rewrites no row. A library scanned once and left alone stays broken. ## Upstream `Season.FindSeriesPresentationUniqueKey()` resolves the live series' current key at save time (`upstream/MediaBrowser.Controller/Entities/TV/Season.cs:237`), so a child cannot hold a key its parent has moved off — the two converge inside one pass rather than on the next one. ## Where to start Either re-key the children when a series' `PresentationUniqueKey` changes, or resolve the key at write time from the series row the way upstream does rather than carrying whatever the child was created with. ## How to know it is done `SELECT` over a scanned fixture returns no season or episode whose `SeriesPresentationUniqueKey` differs from its series' `PresentationUniqueKey`, after **one** pass; `/Shows/{id}/Seasons` answers with the seasons for a series that acquired an id during that same pass. ### House rules Tests run under `cargo nextest`; upstream's source is the checkout in `upstream/` **at the pin**. Generated artifacts are regenerated, never edited.
Author
Collaborator

Fixed in de308ac, with the review's findings in 113caba.

Verified end to end on the throwaway instance's real library — 294 series, 717
seasons, 7,975 episodes. Before, 704 of 717 seasons carried a key their series
had moved off and /Shows/{id}/Seasons answered nothing. After one scan on the
fixed build:

matching  mismatched
     717           0

and the listings answer:

2 Broke Girls     6 seasons  137 episodes
3 Body Problem    1           8
12 Monkeys        4          46
24                9         204

The series page renders its seasons, with the posters the seasons carry.

The repair runs at the end of every pass rather than only where a row is
rewritten, because that is the case this was about: a library scanned once and
left alone never rewrites the children, so two scheduled RefreshLibrary runs
had left all 704 behind.

Two things the review corrected: a season's own PresentationUniqueKey is
derived from its series' and moves with it — upstream recomputes it on every
save, and a season left on the old one groups with nothing — and only a season
and an episode are touched, because a book carries that column as its own
rather than its parent's.

Fixed in `de308ac`, with the review's findings in `113caba`. Verified end to end on the throwaway instance's real library — 294 series, 717 seasons, 7,975 episodes. Before, 704 of 717 seasons carried a key their series had moved off and `/Shows/{id}/Seasons` answered nothing. After one scan on the fixed build: ``` matching mismatched 717 0 ``` and the listings answer: ``` 2 Broke Girls 6 seasons 137 episodes 3 Body Problem 1 8 12 Monkeys 4 46 24 9 204 ``` The series page renders its seasons, with the posters the seasons carry. The repair runs at the end of every pass rather than only where a row is rewritten, because that is the case this was about: a library scanned once and left alone never rewrites the children, so two scheduled `RefreshLibrary` runs had left all 704 behind. Two things the review corrected: a season's own `PresentationUniqueKey` is derived from its series' and moves with it — upstream recomputes it on every save, and a season left on the old one groups with nothing — and only a season and an episode are touched, because a book carries that column as its own rather than its parent's.
Sign in to join this conversation.
No description provided.