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:<id>`, 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.
This commit is contained in:
Frank committed 2026-10-31 12:40:49 -07:00
1 parent acab4cef22
commit e4656cfd7a
3 files changed
+66 -15

No files matched your search

+34 -14
View File
@@ -404,20 +404,37 @@ def tvdb_search(name):
return None 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): 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}" ck = f"tvdb:episodes:{sid}"
if ck in CACHE: if ck not in CACHE:
return CACHE[ck] data = tvdb(f"/series/{sid}/episodes/default")
data = tvdb(f"/series/{sid}/episodes/default") if not data:
if not data: return None
return None rows = data.get("episodes") or []
CACHE[ck] = {f"{e.get('seasonNumber')}/{e.get('number')}": CACHE[ck] = {f"{e.get('seasonNumber')}/{e.get('number')}":
(e.get("name") or "").strip() (e.get("name") or "").strip() for e in rows}
for e in data.get("episodes") or []} CACHE[f"tvdb:capped:{sid}"] = len(rows) >= EPISODE_PAGE_CAP
return CACHE[ck] 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. # tvdb's titles read backwards, for the rows that carry a title and no numbering.
_TITLE_IDX = {} _TITLE_IDX = {}
@@ -757,14 +774,17 @@ def label_of(s):
def contradicted_seasons(s): def contradicted_seasons(s):
"""Seasons whose numbering tvdb's own episode list contradicts — the one place """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 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 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 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.""" 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: if "contradicted" in s:
return s["contradicted"] return s["contradicted"]
bad = set() 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: if eps:
have = collections.Counter() have = collections.Counter()
for k in eps: for k in eps:
@@ -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 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 series is listed under *seasons whose numbering tvdb contradicts* in the
exceptions. A season holding FEWER files than tvdb lists stays plausible — a 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 **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 after the folder it scanned, so a library filed by release folders ends up with
+27
View File
@@ -539,3 +539,30 @@ PY
[ "$(dst "$LIB/Gate Pack/Season 05/two S05E01.mkv")" = "$want" ] [ "$(dst "$LIB/Gate Pack/Season 05/two S05E01.mkv")" = "$want" ]
grep -A6 -i "numbering tvdb contradicts" "$EXC" | grep -q "Anime Gate" 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 ]
}