feat(media): the folder carries its tvdb id; specials and year-seasons resolve #105

Merged
dank merged 2 commits from feat/shows-ids-and-specials into main 2026-10-31 12:56:35 -07:00
3 changed files with 281 additions and 27 deletions

No files matched your search

+137 -16
View File
@@ -404,19 +404,79 @@ 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.
_TITLE_IDX = {}
def tvdb_title_index(sid):
"""tvdb's episode list as `title -> (season, episode)`. A title two episodes
share names no episode: both halves are dropped, because a file that parks is
honest and a file filed at the wrong number is a lie the server believes."""
if sid in _TITLE_IDX:
return _TITLE_IDX[sid]
index, tie = {}, set()
for key, name in (tvdb_episodes(sid) or {}).items():
season, _, ep = key.partition("/")
t = eng.norm(name)
if not t or t in tie:
continue
if t in index:
del index[t]
tie.add(t)
continue
try:
n_season, n_ep = int(season), int(ep)
except ValueError:
continue # half-numbers ("12.5") state no slot
index[t] = (n_season, n_ep)
_TITLE_IDX[sid] = index
return index
def tvdb_number(t):
"""The (season, episode) tvdb states for a provider row whose own numbering is
not a season at all. Jellyfin's date matcher files specials by AIRDATE, so the
row says season 2002 — a YEAR — and only the title the provider recorded says
which episode it is. No id, no title, or a title tvdb gives twice: no number,
and the file parks with the reason it always carried."""
sid = (t.get("tvdb") or "").strip()
title = (t.get("title") or "").strip()
if not sid or not title:
return None
return tvdb_title_index(sid).get(eng.norm(title))
# ------------------------------------------------------------------- the inputs # ------------------------------------------------------------------- the inputs
rows = [] rows = []
for line in open(ARGS.enum): for line in open(ARGS.enum):
@@ -613,9 +673,16 @@ for r in video:
else: else:
season = fs if fs is not None else num(t["season"]) season = fs if fs is not None else num(t["season"])
ep = leading_index(r["base"]) or num(t["episode"]) ep = leading_index(r["base"]) or num(t["episode"])
if not 0 < season <= 200 or ep <= 0: if not 0 <= season <= 200 or ep <= 0:
unknown.append((r, f"a provider row of season {season} episode {ep}")) # Season 0 IS a season: specials live in Season 00 by Jellyfin's own
continue # contract. A season that is a YEAR is the date matcher's doing, and
# the only authority on it is tvdb's episode list for the series.
recovered = tvdb_number(t)
if not recovered:
unknown.append(
(r, f"a provider row of season {season} episode {ep}"))
continue
season, ep = recovered
eps = [ep] eps = [ep]
claimants.append((r, owner, season, eps)) claimants.append((r, owner, season, eps))
note(owner, t["year"] if t else "", t["tmdb"] if t else "", note(owner, t["year"] if t else "", t["tmdb"] if t else "",
@@ -705,11 +772,41 @@ def label_of(s):
return f"{nm} ({yr})" return f"{nm} ({yr})"
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 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 = 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:
season, _, _ep = k.partition("/")
if season.isdigit():
have[int(season)] += 1
booked = collections.defaultdict(set)
for _r, name, season, claimed in claimants:
if canon.get(series_key(name)) is s:
booked[season].update(claimed)
bad = {n for n, seen in booked.items() if len(seen) > have.get(n, 0)}
s["contradicted"] = bad
return bad
def episode_title(r, s, season, ep): def episode_title(r, s, season, ep):
"""The title this episode carries: tvdb's for the id the series resolved to, """The title this episode carries: tvdb's for the id the series resolved to,
else the title the matched row states, else nothing at all — a dangling ` - ` else the title the matched row states, else nothing at all — a dangling ` - ` is
is not a title, and an invented one is worse than none.""" not a title, and an invented one is worse than none. A season tvdb's own list
if s["tvdb"]: contradicts gets no tvdb title: its number points somewhere else, and a wrong
title is worse than a bare one."""
if s["tvdb"] and season not in contradicted_seasons(s):
t = ((tvdb_episodes(s["tvdb"]) or {}).get(f"{season}/{ep}") or "").strip() t = ((tvdb_episodes(s["tvdb"]) or {}).get(f"{season}/{ep}") or "").strip()
if t: if t:
return clean(t) return clean(t)
@@ -718,8 +815,17 @@ def episode_title(r, s, season, ep):
def series_dir(name): def series_dir(name):
"""One series, one folder — and the folder CARRIES the tvdb id. Both matchers
read `[tvdbid-NNNN]` (Jellyfin's Tvdb agent, Plex's scanner), so a show whose
title is ambiguous resolves by number instead of by a human guessing which
folder is which. The id rides the FOLDER only: Jellyfin gives a file name 180
characters and the episode title needs the room. A series nothing resolved
keeps the provider's words with no tag at all."""
s = canon.get(series_key(name)) s = canon.get(series_key(name))
return f"{ROOT}/{label_of(s)}" if s else f"{ROOT}/{clean(series_name(name))}" if not s:
return f"{ROOT}/{clean(series_name(name))}"
tag = f" [tvdbid-{s['tvdb']}]" if s["tvdb"] else ""
return f"{ROOT}/{label_of(s)}{tag}"
# ------------------------------------------------------------------- the plan # ------------------------------------------------------------------- the plan
@@ -973,6 +1079,8 @@ def write_plan():
def write_exceptions(): def write_exceptions():
no_year = sorted({s["name"] for s in canon.values() if not s["year"]}) no_year = sorted({s["name"] for s in canon.values() if not s["year"]})
no_tvdb = sorted({identity(s)[0] for s in canon.values() if not s.get("canon")}) no_tvdb = sorted({identity(s)[0] for s in canon.values() if not s.get("canon")})
clash = sorted({(identity(s)[0], tuple(sorted(contradicted_seasons(s))))
for s in canon.values() if contradicted_seasons(s)})
with open(ARGS.exceptions, "w") as fh: with open(ARGS.exceptions, "w") as fh:
fh.write(f"TV relayout exceptions for {ROOT}\n\n") fh.write(f"TV relayout exceptions for {ROOT}\n\n")
fh.write(f"Everything below is parked under {PARK}, keeping the relative path it\n") fh.write(f"Everything below is parked under {PARK}, keeping the relative path it\n")
@@ -1002,6 +1110,19 @@ def write_exceptions():
fh.write(" title was taken. Nothing was invented: give these a resolvable\n") fh.write(" title was taken. Nothing was invented: give these a resolvable\n")
fh.write(" id (--tvdb) and re-plan; a rename is idempotent.\n") fh.write(" id (--tvdb) and re-plan; a rename is idempotent.\n")
fh.write(" " + ", ".join(no_tvdb[:300]) + "\n") fh.write(" " + ", ".join(no_tvdb[:300]) + "\n")
if clash:
fh.write(f"\n{len(clash):>6} series carry seasons whose numbering tvdb"
" contradicts:\n")
fh.write(" a pack books more episodes in a season than tvdb holds"
" there — an anime\n pack numbered by its release group is"
" the usual shape. Those files still\n rename to the"
" standard shape, but they do NOT ride with tvdb's titles:"
"\n the slot tvdb answers for that number is a different"
" episode. Correct the\n numbering and re-plan; a rename"
" is idempotent.\n")
fh.write(" " + "; ".join(
f"{n} S{'/'.join(f'{x:02d}' for x in ss)}"
for n, ss in clash[:300]) + "\n")
print(f"exceptions -> {ARGS.exceptions}") print(f"exceptions -> {ARGS.exceptions}")
@@ -181,7 +181,8 @@ show. A name whose marker is `Part 2` is a film and stays put.
already matched and numbered most of it. `scripts/media-shows.py` re-files that already matched and numbered most of it. `scripts/media-shows.py` re-files that
library into the shape Jellyfin reads, with **tvdb as the name authority**: library into the shape Jellyfin reads, with **tvdb as the name authority**:
<Series> (<year>)/Season NN/<Series> (<year>) - SxxEyy - <Episode Title>.ext <Series> (<year>) [tvdbid-NNNN]/Season NN/
<Series> (<year>) - SxxEyy - <Episode Title>.ext
It never guesses: every series name, episode number and title it writes traces to 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. an authority, and what has none is parked for a human to read.
@@ -234,7 +235,11 @@ Evidence, strongest first — the order the planner believes things:
whose own name says nothing; whose own name says nothing;
- the **row's tvdb id, else its TMDb id carried through TMDb's external ids, else - 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 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; episode title, because a name the server read off a folder is not a show name.
The folder CARRIES the id: `[tvdbid-NNNN]` is what Jellyfin's Tvdb agent and
Plex's scanner both read, so a show whose title is ambiguous resolves by number
instead of by a human guessing which folder is which. The id rides the folder
only — a file name gets 180 characters and the episode title needs the room.
- a **bonus folder** anywhere above the file, or Jellyfin's `ExtraType` → the - a **bonus folder** anywhere above the file, or Jellyfin's `ExtraType` → the
series' canonical extra folder; series' canonical extra folder;
- a runtime over `SHORT_MAX` makes it film-shaped, and it leaves for `--movies` only - a runtime over `SHORT_MAX` makes it film-shaped, and it leaves for `--movies` only
@@ -244,7 +249,7 @@ Evidence, strongest first — the order the planner believes things:
for the word "russia", *Special Edition I* for a file saying *Edition III*. Else it for the word "russia", *Special Edition I* for a file saying *Edition III*. Else it
parks. parks.
Two refusals carry the design, and a dry run on the real library found both: Four refusals carry the design: what the planner will NOT do is the point of it.
- **Folders resolve a series; they never invent one.** A folder's words may - **Folders resolve a series; they never invent one.** A folder's words may
resolve a file to a series a provider has already named — `(CBB) Tengen Toppa resolve a file to a series a provider has already named — `(CBB) Tengen Toppa
@@ -254,8 +259,27 @@ Two refusals carry the design, and a dry run on the real library found both:
- **A scraped row does not outrank the filesystem.** Jellyfin numbers - **A scraped row does not outrank the filesystem.** Jellyfin numbers
multi-season packs badly: every file under `SNL 16-20/Season 16..19` carries multi-season packs badly: every file under `SNL 16-20/Season 16..19` carries
season 16 episode 20, and one Gundam row says season 2004. The folder, then season 16 episode 20, and one Gundam row says season 2004. The folder, then
the file's own leading number, win; a row that still insists on season 1989 the file's own leading number, win.
parks its file instead of creating `Season 1989`. - **A season that is a YEAR is the date matcher, not a season.** Jellyfin files
specials by airdate, so a row can arrive saying `season 2002`; that is not a
season and it never becomes a folder. The one authority on such a row is tvdb's
episode list read backwards: the title the row carries names the (season,
episode) tvdb gives it. A title tvdb gives to two episodes names nothing, and
the file parks with the reason it always carried. `Season 00` IS a legal
season — specials live there — so a row that really says 0 files there instead
of parking.
- **A season tvdb's own list contradicts gets no tvdb title.** 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: the slot its number points at is a
different episode, and dressing it in that episode's title is a confident lie.
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. 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
+115 -6
View File
@@ -11,7 +11,11 @@
# the name caches are input files, so no test touches the network. # the name caches are input files, so no test touches the network.
# #
# The target contract is tvdb-grade naming: # The target contract is tvdb-grade naming:
# <Series> (<year>)/Season NN/<Series> (<year>) - SxxEyy - <Episode Title>.<ext> # <Series> (<year>) [tvdbid-NNNN]/Season NN/<Series> (<year>) - SxxEyy - <Title>.<ext>
# The id rides the FOLDER only: it is what both matchers read the series by, and a
# 180-character ceiling on a file name is not the place to spend it. Season 00
# holds specials; a provider row whose "season" is a YEAR is renumbered by the
# episode title tvdb states for the series' id, never by a guess.
# tvdb is the IDENTITY authority: a provider name a server derived from a folder # tvdb is the IDENTITY authority: a provider name a server derived from a folder
# is not a series name, so a polluted name is undone through the series' tvdb id. # is not a series name, so a polluted name is undone through the series' tvdb id.
# The review pile parks OUTSIDE the library — a parked folder inside it indexes # The review pile parks OUTSIDE the library — a parked folder inside it indexes
@@ -39,6 +43,15 @@ title_row() {
"${5:-}" "${6:-}" "${7:-}" "${8:-}" "${9:-}" >> "$TITLES" "${5:-}" "${6:-}" "${7:-}" "${8:-}" "${9:-}" >> "$TITLES"
} }
# file PATH SIZE — `at` plus the row the enum census would have carried had the
# file existed when setup walked the tree. A test that needs a shape the fixture
# does not ship adds it here, or the planner never sees it.
file() {
at "$1" "$2"
local b="${1##*/}"
printf '%s\t.%s\t%s\n' "$2" "${b##*.}" "$1" >> "$ENUM"
}
# dst SRC — the destination the plan gives SRC, or nothing when it does not move. # dst SRC — the destination the plan gives SRC, or nothing when it does not move.
dst() { dst() {
awk -F '\t' -v s="$1" '$1 == "move" && $2 == s {print $3}' "$OUT" awk -F '\t' -v s="$1" '$1 == "move" && $2 == s {print $3}' "$OUT"
@@ -220,11 +233,12 @@ PY
= "$LIB/The Examples (2010)/Season 01/The Examples (2010) - S01E01 - Pilot.mkv" ] = "$LIB/The Examples (2010)/Season 01/The Examples (2010) - S01E01 - Pilot.mkv" ]
} }
@test "the tvdb identity names the series folder, not the name a folder left behind" { @test "the tvdb identity names the series folder, id and all, not the folder's echo" {
printf '{"tvdb:series:78850": {"name": "Show Box", "year": "2016"}}\n' > "$CACHE" printf '{"tvdb:series:78850": {"name": "Show Box", "year": "2016"}}\n' > "$CACHE"
plan plan
[ "$(dst "$BOX/Show.Box.S01E01.1080p.WEB-DL.mkv")" \ want="$LIB/Show Box (2016) [tvdbid-78850]/Season 01"
= "$LIB/Show Box (2016)/Season 01/Show Box (2016) - S01E01 - Pilot.mkv" ] want+="/Show Box (2016) - S01E01 - Pilot.mkv"
[ "$(dst "$BOX/Show.Box.S01E01.1080p.WEB-DL.mkv")" = "$want" ]
} }
@test "a provider name that already carries the year is not given it twice" { @test "a provider name that already carries the year is not given it twice" {
@@ -232,8 +246,9 @@ PY
# and `Acapulco (2021) (2021)` is a folder neither matcher nor human reuses. # and `Acapulco (2021) (2021)` is a folder neither matcher nor human reuses.
printf '{"tvdb:series:78850": {"name": "Show Box (2016)", "year": "2016"}}\n' > "$CACHE" printf '{"tvdb:series:78850": {"name": "Show Box (2016)", "year": "2016"}}\n' > "$CACHE"
plan plan
[ "$(dst "$BOX/Show.Box.S01E01.1080p.WEB-DL.mkv")" \ want="$LIB/Show Box (2016) [tvdbid-78850]/Season 01"
= "$LIB/Show Box (2016)/Season 01/Show Box (2016) - S01E01 - Pilot.mkv" ] want+="/Show Box (2016) - S01E01 - Pilot.mkv"
[ "$(dst "$BOX/Show.Box.S01E01.1080p.WEB-DL.mkv")" = "$want" ]
run grep -F '(2016) (2016)' "$OUT" run grep -F '(2016) (2016)' "$OUT"
[ -z "$output" ] [ -z "$output" ]
} }
@@ -457,3 +472,97 @@ PY
plan plan
[ -z "$(awk -F '\t' '$1 == "move" {print $2}' "$OUT")" ] [ -z "$(awk -F '\t' '$1 == "move" {print $2}' "$OUT")" ]
} }
# Specials and the year-shaped season. Jellyfin's contract puts specials in
# Season 00, and its date matcher hands back rows whose "season" is the AIRDATE
# YEAR. Both used to park — 1,412 and 1,033 files of one 52k plan — although one
# is a legal season and the other is renumberable from the title tvdb states.
@test "a provider's season 0 is a special, so it files under Season 00" {
file "$LIB/Show Bumps/Best Of The Examples.mkv" 5000
title_row "$LIB/Show Bumps/Best Of The Examples.mkv" "The Examples" 2010 0 2
plan
[ "$(dst "$LIB/Show Bumps/Best Of The Examples.mkv")" \
= "$LIB/The Examples (2010)/Season 00/The Examples (2010) - S00E02.mkv" ]
}
# These three tests carry a tvdb id of their own. The fixture's box set is 78850 and
# ONE id means ONE show: reusing it here would merge these files into that series and
# the duplicate rule would park them before any numbering question is asked.
@test "a row whose season is a year is renumbered by the title tvdb states" {
printf '{"tvdb:series:4242": %s, "tvdb:episodes:4242": %s}\n' \
'{"name": "The Examples", "year": "2010"}' \
'{"1/7": "Best Of"}' > "$CACHE"
file "$LIB/Show Bumps/Best Of The Examples.mkv" 5000
title_row "$LIB/Show Bumps/Best Of The Examples.mkv" \
"The Examples" 2010 2010 11 "" "" "Best Of" "4242"
plan
want="$LIB/The Examples (2010) [tvdbid-4242]/Season 01"
want+="/The Examples (2010) - S01E07 - Best Of.mkv"
[ "$(dst "$LIB/Show Bumps/Best Of The Examples.mkv")" = "$want" ]
}
@test "a title tvdb gives twice never renumbers an episode" {
printf '{"tvdb:series:4242": %s, "tvdb:episodes:4242": %s}\n' \
'{"name": "The Examples", "year": "2010"}' \
'{"1/7": "Best Of", "2/3": "Best Of"}' > "$CACHE"
file "$LIB/Show Bumps/Best Of The Examples.mkv" 5000
title_row "$LIB/Show Bumps/Best Of The Examples.mkv" \
"The Examples" 2010 2010 11 "" "" "Best Of" "4242"
plan_park
run awk -F '\t' -v p="$OUTSIDE/" '$1 == "move" && index($3, p) == 1 {print $3}' "$OUT"
[[ "$output" == *"Best Of The Examples.mkv"* ]]
}
# Anime numbering is the one place a tvdb title can be confidently wrong: a pack
# numbered by its release group still names S05E01, and tvdb answers that slot with
# a different episode. A season the authority does not hold, or holds fewer of than
# the pack books, is CONTRADICTED: the rename still happens, the title does not.
@test "a season tvdb contradicts keeps the rename but never its titles" {
printf '{"tvdb:series:4242": %s, "tvdb:episodes:4242": %s}\n' \
'{"name": "Anime Gate", "year": "2010"}' \
'{"1/1": "Pilot"}' > "$CACHE"
file "$LIB/Gate Pack/Season 01/one S01E01.mkv" 5000
file "$LIB/Gate Pack/Season 05/two S05E01.mkv" 5000
file "$LIB/Gate Pack/Season 05/three S05E02.mkv" 5000
title_row "$LIB/Gate Pack/Season 01/one S01E01.mkv" \
"Anime Gate" 2010 1 1 "" "" "" "4242"
title_row "$LIB/Gate Pack/Season 05/two S05E01.mkv" \
"Anime Gate" 2010 5 1 "" "" "" "4242"
title_row "$LIB/Gate Pack/Season 05/three S05E02.mkv" \
"Anime Gate" 2010 5 2 "" "" "" "4242"
plan
want="$LIB/Anime Gate (2010) [tvdbid-4242]/Season 01"
want+="/Anime Gate (2010) - S01E01 - Pilot.mkv"
[ "$(dst "$LIB/Gate Pack/Season 01/one S01E01.mkv")" = "$want" ]
want="$LIB/Anime Gate (2010) [tvdbid-4242]/Season 05"
want+="/Anime Gate (2010) - S05E01.mkv"
[ "$(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 ]
}