summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-26 15:51:10 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-26 15:51:10 +0200
commitb1543faad87fd865f8746711ab5f10ac4f331e8b (patch)
tree37bca053cc875973c27dcdbbfed9bfb65f50c2f4 /packages/meshbay-node/src/meshbay_node/indexer/enrich.py
parent84aa73e89cfe486c62317f761dcf87d7845eb4f7 (diff)
downloadmeshbay-b1543faad87fd865f8746711ab5f10ac4f331e8b.tar.gz
fix(video): group every episode under the show's own folder, not a per-file guessit title
The mechanism itself was wrong, not just the TMDB matching: a bare episode numbering convention with no show name in the filename at all (`001 Episode's Own Title.ext`, no SxxExx, no show prefix — entirely ordinary on its own) makes guessit invent a "title" from whatever text follows the number. That text is the individual episode's own name, and differs for every episode in the folder — trusting it, as the code did, groups nothing together at all: every episode became its own single- episode "show", searched against TMDB by that one-off title alone. Whenever a season-like ancestor folder exists (a numbered season, or Specials/Bonus/Extras -> season 0), its own root folder now names the show unconditionally — never a per-file guessit title, which cannot tell a show's real name from an individual episode's one-off name when the filename carries no reliable ShowName/SxxExx structure. The walk continues past *every* consecutive season-like ancestor, not just the first: a per-season Bonus folder (Show/Season N/Bonus/file.ext) is nested two levels inside the show, both "Bonus" and "Season N" season-like on their own, and stopping at the first would hand back "Season N" as the show's name instead of "Show". Also adds "livre" ("book") to the season-word vocabulary (§3.4) — some shows number their seasons that way (Roman numerals) rather than "season"/"saison". Supersedes _title_from_show_siblings from the previous commit (removed): that fallback assumed a per-file title could still be trusted often enough to be worth borrowing from a sibling season folder — this fix means it never needs trusting in the first place once a season-like ancestor exists. Verified directly against the real library this was found on, not just synthetic tests: every sample file (a numbered episode, a Book/Bonus feature, a Book-root "making of") now resolves to the show's real name.
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/enrich.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich.py143
1 files changed, 66 insertions, 77 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
index fc4b059..5535674 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
@@ -38,15 +38,45 @@ MAX_ANCESTOR_DEPTH = 4
MAX_SIBLINGS_CHECKED = 20
-def _season_from_ancestors(file_path: Path) -> int | None:
+def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, str] | 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
+ (innermost) match, but the show's own name from *above every
+ consecutive season-like ancestor*, not just the first one. A
+ per-season Bonus folder (`Show/Season N/Bonus/file.ext`) is nested two
+ levels inside the show, both of them season-like on their own
+ ("Bonus" and "Season N") — stopping at the first would hand back
+ "Season N" as the show's name instead of "Show".
+
+ Trusted over any per-file guessit title once found: a bare episode
+ numbering convention with no show name embedded at all
+ (`001 Episode's Own Title.mkv`, no SxxExx, no show prefix) is
+ completely ordinary and makes guessit invent a title from whatever
+ text follows the number — that text is the individual episode's own
+ name, never the show's, and every episode in the folder produces a
+ different one. None of that ambiguity exists for the folder structure
+ itself: the show's own root folder names it once, not per file.
+
+ None if no ancestor looks like a season folder at all — a flat
+ library has nothing to borrow a show name from this way, and
+ _title_from_siblings' per-file logic is what applies instead.
+ """
folder = file_path.parent
- for _ in range(MAX_ANCESTOR_DEPTH):
- if folder is None or folder == folder.parent:
- break
- season = title_parse.season_from_folder_name(folder.name)
- if season is not None:
- return season
+ season: int | None = None
+ depth = 0
+ while folder is not None and folder.parent != folder and depth < MAX_ANCESTOR_DEPTH:
+ this_season = title_parse.season_from_folder_name(folder.name)
+ if this_season is None:
+ if season is None:
+ folder = folder.parent
+ depth += 1
+ continue
+ return season, folder.name
+ if season is None:
+ season = this_season
folder = folder.parent
+ depth += 1
return None
@@ -82,48 +112,6 @@ def _title_from_siblings(file_path: Path) -> str | None:
return None
-def _title_from_show_siblings(file_path: Path) -> str | None:
- """
- §3.4b: a Specials/Bonus folder's files are routinely named after their
- own one-off joke or theme rather than the show at all — every one of
- them parses as a standalone "movie" (guessit has no season/episode
- grammar to find), and by coincidence a good few of those one-off
- titles collide with real, unrelated films: found live, a real show's
- Specials folder matched several bonus episodes to real, unrelated
- movies sharing those one-off titles, instead of anything to do with
- the show. _title_from_siblings can't help — every sibling *here* has the same
- problem. What the ordinary season folders next to this one hold does
- not: `Show.S01E01.mkv`-shaped filenames name the show properly, so
- borrowing from the first one found there is the same trick as
- _title_from_siblings, aimed one level higher — at the show's other
- seasons rather than this file's own folder.
- """
- season_folder = file_path.parent
- show_folder = season_folder.parent
- try:
- sibling_folders = sorted(
- (d for d in show_folder.iterdir() if d.is_dir() and d != season_folder),
- key=lambda d: d.name)
- except OSError:
- return None
- for folder in sibling_folders:
- try:
- names = sorted(p.name for p in folder.iterdir() if p.is_file())
- except OSError:
- continue
- checked = 0
- for name in names:
- if Path(name).suffix.lower() not in MEDIA_EXTENSIONS["video"]:
- continue
- checked += 1
- if checked > MAX_SIBLINGS_CHECKED:
- break
- parsed = title_parse.parse_episode_filename(name)
- if parsed.display_title and parsed.episode is not None:
- return parsed.display_title
- return None
-
-
def _synthetic_episode_number(file_path: Path) -> int:
"""
§3.4b: a Specials/Bonus folder's files often carry no episode number at
@@ -237,37 +225,38 @@ class Enricher:
entry.id, fields.get("duration"), fields.get("width"), fields.get("height"))
ep = title_parse.parse_episode_filename(entry.name)
- season = ep.season
- if season is None:
- season = await asyncio.to_thread(_season_from_ancestors, file_path)
+ ancestor = await asyncio.to_thread(_season_and_show_from_ancestors, file_path)
- # §3.4b: an ancestor folder saying this is part of a show (a
- # numbered season, or Specials/Bonus/Extras -> season 0) is
- # trusted over the filename having no SxxExx of its own —
- # otherwise a Specials folder's one-off-named files (no episode
- # grammar for guessit to find at all) fall to the movie branch
- # below and get searched against TMDB as unrelated standalone
- # films, one per Special. Found live: a real show's Specials
- # folder matched several bonus episodes to real, unrelated
- # movies sharing those one-off titles.
- if ep.episode is not None or season is not None:
- if ep.episode is not None:
- title = ep.display_title or await asyncio.to_thread(
- _title_from_siblings, file_path)
- episode = ep.episode
- else:
- # This file's own name carries no episode grammar at
- # all — every sibling in *this* folder has the same gap
- # (that is what makes it a Specials-shaped folder), so
- # borrowing from them (_title_from_siblings) would just
- # hand back another Special's own one-off title. The
- # show's ordinary season folders, next to this one,
- # don't have that gap.
- title = await asyncio.to_thread(_title_from_show_siblings, file_path)
+ if ancestor is not None:
+ # A season-like ancestor folder exists — the show's own
+ # root folder names it, trusted over any per-file guessit
+ # title. This is what actually groups every episode under
+ # one show: a bare numbering convention with no show name
+ # in the filename at all (`001 Episode's Own Title.mkv`,
+ # ordinary enough on its own) makes guessit invent a title
+ # from whatever text follows the number, which is that
+ # episode's own name, never the show's — and differs for
+ # every episode, so nothing would ever group together.
+ # Found live: a real show's episodes and its Specials
+ # folder alike, both named this way, showed up as
+ # 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
+ if episode is None:
episode = await asyncio.to_thread(_synthetic_episode_number, file_path)
- fields["display_title"] = title or title_parse.naive_title(entry.name)
- fields["season"] = season if season is not None else 0
+ fields["display_title"] = show_title
+ fields["season"] = season
fields["episode"] = episode
+ elif ep.episode is not None:
+ # No season-like ancestor at all (a flat library) but the
+ # filename itself carries season+episode (§3.4).
+ title = ep.display_title or await asyncio.to_thread(
+ _title_from_siblings, file_path)
+ fields["display_title"] = title or title_parse.naive_title(entry.name)
+ fields["season"] = ep.season
+ fields["episode"] = ep.episode
else:
mv = title_parse.parse_movie_filename(entry.name)
fields["display_title"] = mv.display_title or mv.naive_title