fix(media): a number too long to be an episode is not one

The Attack on Titan box set on the array names two files S03E2019 and
S04E47370355, and the provider rows the metadata server recorded for them carry
the same junk. EP_RANGE capped its first episode group at three digits without
guarding the run, so the pass-2 plan filed both under E201 and E473 — slots
that belong to real episodes — inside 進撃の巨人 (2013) [tvdbid-267440].

- guard the first episode group the way the second was already guarded, so a
  longer digit run states no marker at all; a glued resolution stays legal,
  because S01E01-1080p is still ONE episode
- cap a provider row's episode at EP_MAX: such a row is renumbered from tvdb's
  title when the series' id answers, and parks with a reason that names the
  junk when nothing answers
- split the operator-map exceptions into the two things they are: a folder that
  IS on the array whose files never became a series (bonus material with no
  owner: Fairy Tail, Kimetsu no Yaiba, Dorohedoro) is no longer reported as a
  folder the array no longer holds, and the missing-folder hint says the review
  pile counts as moved because the map reads top-level folders only
- the vault's evidence list carries the law; the four tests were read red
  against the shipping planner before any of it changed

Replanning the two real files now parks them under their own names with the
reasons "a provider row of season 3 episode 2019" and
"… season 4 episode 47370355".
This commit is contained in:
Frank committed 2026-10-31 14:49:12 -07:00
1 parent 9c25b1cccb
commit 2625ad1083
3 files changed
+91 -11

No files matched your search

+33 -10
View File
@@ -168,12 +168,16 @@ SEASON_META = re.compile(r"^season\.nfo$", re.I)
FOLDER_IMAGE = re.compile(r"^folder\.(jpg|jpeg|png|webp|bmp)$", re.I)
SEASON_DIR_RE = re.compile(r"(?i)^\s*(?:season[\s._-]*(\d{1,3})|s(\d{1,2}))\s*$")
BAD_CHARS = re.compile(r'[<>:"/|?*]')
# SxxEyy with an optional second episode, plus the 1x04 form. The second number
# is guarded so a resolution never reads as an episode: "S01E01-1080p" is ONE
# episode, and the longest-match backtracking of \d{1,3} is what the (?![\dpP])
# after it closes off.
# SxxEyy with an optional second episode, plus the 1x04 form. Both episode numbers
# are guarded so a longer digit run never reads as an episode. A resolution glued
# on stays legal — "S01E01-1080p" is ONE episode — while "S03E2019" and
# "S04E47370355" (both real names on the array, from a box set whose provider rows
# say the same junk) state no episode at all: the run is an airdate or a pack
# index, and the first three digits of it would steal E201 and E473 from the
# episodes that own them. No marker means the file is numbered by its provider
# row, or by tvdb's title, or it parks.
EP_RANGE = re.compile(r"(?i)(?<![a-z0-9])s(?:eason[\s._-]*)?(\d{1,2})[\s._-]*"
r"e(?:p(?:isode)[\s._-]*)?(\d{1,3})"
r"e(?:p(?:isode)[\s._-]*)?(\d{1,3})(?![\dpP])"
r"(?:[\s._-]*(?:-|to[\s._-]*|ep)[\s._-]*(?:s\d{1,2}[\s._-]*)?e?"
r"(\d{1,3})(?![\dpP]))?")
NX_RE = re.compile(r"(?i)(?<![a-z0-9])(\d{1,2})x(\d{2,3})(?![a-z0-9])")
@@ -201,6 +205,11 @@ EXTRA_KIND = {"trailer": "trailers", "tmbd": "trailers", "intro": "other",
"deleted": "deleted scenes", "other": "other", "epilogue": "other",
"opening": "other", "featurette": "featurettes", "short": "shorts"}
SHORT_MAX = 45 * 60 # over this, a file with no episode evidence is film-shaped
# An episode number above this is not an episode number: the array's box sets
# carry 2019 (an airdate fragment) and 47370355 (a pack index) in the column the
# metadata server recorded as the match. tvdb's episode list is the only door on
# such a row; nothing files at E1000 or beyond.
EP_MAX = 999
def rel(p):
@@ -748,10 +757,11 @@ for r in video:
else:
season = fs if fs is not None else num(t["season"])
ep = leading_index(r["base"]) or num(t["episode"])
if not 0 <= season <= 200 or ep <= 0:
if not 0 <= season <= 200 or not 1 <= ep <= EP_MAX:
# Season 0 IS a season: specials live in Season 00 by Jellyfin's own
# 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.
# contract. A season that is a YEAR is the date matcher's doing and an
# episode above EP_MAX is a box set's airdate or pack index; neither is
# a number, and the only authority on either is tvdb's episode list.
recovered = tvdb_number(t)
if not recovered:
unknown.append(
@@ -1199,7 +1209,9 @@ def write_exceptions():
f"{n} S{'/'.join(f'{x:02d}' for x in ss)}"
for n, ss in clash[:300]) + "\n")
map_ids = {e["id"] for e in MAP.values()}
unseen = sorted(f for f in MAP if f not in MAP_SEEN)
on_array = {rel(p).split("/")[0] for p in held}
stalled = sorted(f for f in MAP if f not in MAP_SEEN and f in on_array)
unseen = sorted(f for f in MAP if f not in MAP_SEEN and f not in on_array)
unanswered = sorted({f"{identity(s)[0]} ({s['tvdb']})"
for s in canon.values()
if s.get("tvdb") in map_ids and not s.get("canon")})
@@ -1211,9 +1223,20 @@ def write_exceptions():
" and re-plan, because a rename is idempotent.\n")
fh.write(" " + "; ".join(
f"{f} -> {MAP[f]['id']}" for f in sorted(MAP_SEEN)) + "\n")
if stalled:
fh.write(f"\n{len(stalled):>6} map rows name a folder on the array that\n")
fh.write(" never became a series: every file in it parked, so the id\n"
" had nothing to name. The map names a RECORD — it numbers\n"
" no episodes and owns no bonus material, so read the park\n"
" reasons above this line: a folder of openings, endings and\n"
" promos with no show to claim them is the usual shape.\n")
fh.write(" " + "; ".join(
f"{f} -> {MAP[f]['id']}" for f in stalled[:300]) + "\n")
if unseen:
fh.write(f"\n{len(unseen):>6} map rows name no folder on the array: the\n")
fh.write(" pack moved, or the row carries a name the array never held.\n")
fh.write(" pack moved (into the review pile counts: the map reads\n"
" top-level folders only), or the row carries a name the\n"
" array never held.\n")
fh.write(" " + "; ".join(unseen[:300]) + "\n")
if MAP_CLASH:
fh.write(f"\n{len(MAP_CLASH):>6} folders carry a provider tvdb id the map\n")