diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-26 16:04:00 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-26 16:04:00 +0200 |
| commit | c81eaa0bd07e360ec03406dfc63267c3013a0319 (patch) | |
| tree | dbce9709954cd640a77356b3cd6b00b632b6023d /packages/meshbay-node/src/meshbay_node/indexer/enrich.py | |
| parent | b1543faad87fd865f8746711ab5f10ac4f331e8b (diff) | |
| download | meshbay-c81eaa0bd07e360ec03406dfc63267c3013a0319.tar.gz | |
fix(video): fix two remaining bugs in Specials numbering and long-season episodes
1. A season spanning more than one folder (a per-book Bonus folder nested
inside every numbered season) got the same synthetic episode numbers
handed out again in each folder independently — three unrelated
Specials all showing up as "S0E01". _synthetic_episode_number now ranks
across the whole show for the target season, not just one file's own
folder.
2. guessit reads a bare 3-digit leading episode number as a concatenated
season+episode guess rather than a plain episode number — "100" parses
as season=1, episode=0, not episode=100, with nothing in its output
distinguishing that from a real 2-digit episode. A season-like ancestor
already overrides guessit's season (previous commit); this applies the
same fix to episode via a direct regex on the leading number, capped at
3 digits so a leading year (4 digits) is never misread the same way.
Both confirmed against the real library this whole fix was found on:
per-book Bonus features across two books no longer collide, and a real
100th-episode file now resolves to episode 100 instead of 0.
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/enrich.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/indexer/enrich.py | 78 |
1 files changed, 56 insertions, 22 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py index 5535674..07f0be7 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py @@ -36,9 +36,14 @@ MAX_ANCESTOR_DEPTH = 4 # Bounds the "borrow a title from a sibling episode filename" scan (§3.4) so # a folder with thousands of files costs a fixed, small amount of work. MAX_SIBLINGS_CHECKED = 20 +# Bounds the whole-show scan _synthetic_episode_number uses to rank a +# season's files across more than one folder (§3.4c) — a show's total file +# count, not just one folder's, so this needs more headroom than +# MAX_SIBLINGS_CHECKED. +MAX_SEASON_FILES_CHECKED = 500 -def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, str] | None: +def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, Path] | None: """ §3.4/§3.4c: walk up ancestor folders for a season-like one (a numbered season, or Specials/Bonus/Extras -> season 0) — season from the first @@ -72,7 +77,7 @@ def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, str] | None: folder = folder.parent depth += 1 continue - return season, folder.name + return season, folder if season is None: season = this_season folder = folder.parent @@ -112,27 +117,45 @@ def _title_from_siblings(file_path: Path) -> str | None: return None -def _synthetic_episode_number(file_path: Path) -> int: +def _synthetic_episode_number(file_path: Path, show_root: Path, season: int) -> int: """ - §3.4b: a Specials/Bonus folder's files often carry no episode number at - all — each is just named after its own one-off title. The frontend - (video-app.js's buildSeasons) sorts within a season by this number but - only needs it to provide a stable order, not to mean anything beyond - that — alphabetical rank among the folder's video files is enough, and - deterministic across re-scans as long as the folder's contents don't - change. 1-based so it reads as "episode 1", not "episode 0", in a UI - that already uses season 0 for "Specials" itself. + §3.4b/§3.4c: a Specials/Bonus folder's files often carry no episode + number at all — each is just named after its own one-off title. The + frontend (video-app.js's buildSeasons) sorts within a season by this + number but only needs it to provide a stable order, not to mean + anything beyond that. + + Ranked across the *whole show*, not just this file's own folder: season + 0 routinely spans more than one folder under the show's root — a + Bonus folder nested inside every numbered season + (`Show/Season N/Bonus/...`) is exactly the shape that produces this — + and ranking within just this file's own folder hands out the same + "episode 1" again in every other one, found live as three unrelated + Specials all showing up as "S0E01". Deterministic across re-scans as + long as the show's contents don't change. 1-based so it reads as + "episode 1", not "episode 0", in a UI that already uses season 0 for + "Specials" itself. Bounded (MAX_SEASON_FILES_CHECKED) so a huge show + costs a fixed amount of work rather than scaling with its size. """ try: - names = sorted( - p.name for p in file_path.parent.iterdir() - if p.is_file() and p.suffix.lower() in MEDIA_EXTENSIONS["video"]) + candidates = sorted( + (p for p in show_root.rglob("*") + if p.is_file() and p.suffix.lower() in MEDIA_EXTENSIONS["video"]), + key=lambda p: (str(p.parent), p.name), + ) except OSError: return 1 - try: - return names.index(file_path.name) + 1 - except ValueError: - return 1 + rank = 0 + for i, candidate in enumerate(candidates): + if i >= MAX_SEASON_FILES_CHECKED: + break + ancestor = _season_and_show_from_ancestors(candidate) + if ancestor is None or ancestor[0] != season: + continue + rank += 1 + if candidate == file_path: + return rank + return 1 async def _make_thumbnail(file_path: Path, duration: float | None) -> bytes | None: @@ -242,11 +265,22 @@ class Enricher: # individual "movies" each searched against — and matched # to — an unrelated real film sharing that one-off title, # instead of anything grouped under the show at all. - season, show_title = ancestor - episode = ep.episode + season, show_folder = ancestor + # Not ep.episode: guessit reads a bare 3-digit leading + # number (routine once a season-like ancestor is already + # doing the real season/episode work) as a concatenated + # season+episode guess rather than a plain episode number + # — confirmed live, "100 Title.mkv" parsed as episode 0, + # not 100, with nothing in guessit's own output telling + # that apart from a real 2-digit episode. The direct regex + # reads the whole leading number as it is. + episode = title_parse.leading_episode_number(entry.name) + if episode is None: + episode = ep.episode if episode is None: - episode = await asyncio.to_thread(_synthetic_episode_number, file_path) - fields["display_title"] = show_title + episode = await asyncio.to_thread( + _synthetic_episode_number, file_path, show_folder, season) + fields["display_title"] = show_folder.name fields["season"] = season fields["episode"] = episode elif ep.episode is not None: |