From e4656cfd7a64f0955a55825c24b78a47a61ea17e Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 31 Oct 2026 12:40:49 -0700 Subject: [PATCH] fix(media): a tvdb episode page is not the episode list Tvdb's default episode query answers at most 500 rows and states no total. One Piece (1999), Crayon Shin-chan (1992) and Detective Conan (1996) each answer exactly 500 while holding over a thousand, so the contradiction rule added in the previous commit read every long-running show as contradicted: the pack books more episodes than "the authority holds", and the rule that exists to stop a wrong anime title would have stripped the right one from every file of the season. A capped list now records that it is capped (`tvdb:capped:`, so a warm cache answers the same question a cold one did) and contradicts nothing; titles for the episodes the page does hold still ride. Test: a 500-row page with a 501-episode pack keeps ` - Episode 1` and lists no contradiction. Red before the change, proved in a copy of the tree with the cap raised past the fixture. --- scripts/media-shows.py | 48 +++++++++++++------ .../Dankserver/Media Relayout.md | 6 ++- tests/media_shows.bats | 27 +++++++++++ 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/scripts/media-shows.py b/scripts/media-shows.py index a07222b..4dff323 100755 --- a/scripts/media-shows.py +++ b/scripts/media-shows.py @@ -404,20 +404,37 @@ def tvdb_search(name): return None +# Tvdb's default episode query answers AT MOST this many rows and states no +# total: One Piece (1999), Crayon Shin-chan (1992) and Detective Conan (1996) +# each answer exactly 500 while holding over a thousand. A list that long bounds +# nothing above itself, so it must never be read as "the series holds 500". +EPISODE_PAGE_CAP = 500 + + def tvdb_episodes(sid): - """`season/episode` -> title for one series, in the one call tvdb answers.""" + """`season/episode` -> title for one series, in the one call tvdb answers. + Whether that call came back a cut-off page is recorded in the cache beside + it, so a warm cache answers the same question a cold one did.""" ck = f"tvdb:episodes:{sid}" - if ck in CACHE: - return CACHE[ck] - data = tvdb(f"/series/{sid}/episodes/default") - if not data: - return None - CACHE[ck] = {f"{e.get('seasonNumber')}/{e.get('number')}": - (e.get("name") or "").strip() - for e in data.get("episodes") or []} + if ck not in CACHE: + data = tvdb(f"/series/{sid}/episodes/default") + if not data: + return None + rows = data.get("episodes") or [] + CACHE[ck] = {f"{e.get('seasonNumber')}/{e.get('number')}": + (e.get("name") or "").strip() for e in rows} + CACHE[f"tvdb:capped:{sid}"] = len(rows) >= EPISODE_PAGE_CAP return CACHE[ck] +def tvdb_list_capped(sid): + """True when tvdb's episode answer for this series is a page, not a list.""" + rows = tvdb_episodes(sid) + if not rows: + return False + return CACHE.get(f"tvdb:capped:{sid}", len(rows) >= EPISODE_PAGE_CAP) + + # tvdb's titles read backwards, for the rows that carry a title and no numbering. _TITLE_IDX = {} @@ -757,14 +774,17 @@ def label_of(s): def contradicted_seasons(s): """Seasons whose numbering tvdb's own episode list contradicts — the one place - a tvdb title can be a confident lie. 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, and the slot its number points at is a DIFFERENT episode. Books <= - tvdb's count stays plausible: a partial pack is the normal shape.""" + a tvdb title can be a confident lie. 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, and the slot its number points at is a DIFFERENT episode. Books <= + tvdb's count stays plausible: a partial pack is the normal shape. A list the + authority cut off at its page limit (EPISODE_PAGE_CAP) contradicts nothing — + it says nothing about the episodes it never sent.""" if "contradicted" in s: return s["contradicted"] bad = set() - eps = tvdb_episodes(s["tvdb"]) if s["tvdb"] else None + eps = None if not s["tvdb"] or tvdb_list_capped(s["tvdb"]) \ + else tvdb_episodes(s["tvdb"]) if eps: have = collections.Counter() for k in eps: diff --git a/serverAppConfigs-docs/Dankserver/Media Relayout.md b/serverAppConfigs-docs/Dankserver/Media Relayout.md index c7c3fb6..a1ad5f1 100644 --- a/serverAppConfigs-docs/Dankserver/Media Relayout.md +++ b/serverAppConfigs-docs/Dankserver/Media Relayout.md @@ -275,7 +275,11 @@ Four refusals carry the design: what the planner will NOT do is the point of it. 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. + 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 diff --git a/tests/media_shows.bats b/tests/media_shows.bats index 1ef2f42..e78bf1d 100755 --- a/tests/media_shows.bats +++ b/tests/media_shows.bats @@ -539,3 +539,30 @@ PY [ "$(dst "$LIB/Gate Pack/Season 05/two S05E01.mkv")" = "$want" ] grep -A6 -i "numbering tvdb contradicts" "$EXC" | grep -q "Anime Gate" } + +# Tvdb's default episode query answers at most 500 rows and states no total, so a +# list that long is a PAGE, not a list: One Piece (1999), Crayon Shin-chan (1992) +# and Detective Conan (1996) each answer exactly 500 while holding over a thousand. +# Read as the whole truth, that page contradicts every long-running show's pack +# and strips the tvdb title from all of it. +@test "an episode list the authority paginates never contradicts the pack" { + python3 - "$CACHE" <<'PY' +import json, sys +json.dump({"tvdb:series:4242": {"name": "Long Run", "year": "1999"}, + "tvdb:episodes:4242": {f"1/{n}": f"Episode {n}" + for n in range(1, 501)}}, + open(sys.argv[1], "w")) +PY + for n in $(seq 1 501); do + ep=$(printf '%02d' "$n") + file "$LIB/Long Pack/Season 01/long $ep.mkv" 1000 + title_row "$LIB/Long Pack/Season 01/long $ep.mkv" \ + "Long Run" 1999 1 "$n" "" "" "" "4242" + done + plan + want="$LIB/Long Run (1999) [tvdbid-4242]/Season 01" + want+="/Long Run (1999) - S01E01 - Episode 1.mkv" + [ "$(dst "$LIB/Long Pack/Season 01/long 01.mkv")" = "$want" ] + run grep -i "numbering tvdb contradicts" "$EXC" + [ "$status" -ne 0 ] +}