aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/enrich.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich.py112
1 files changed, 103 insertions, 9 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
index ae3f3dc..fc4b059 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
@@ -55,6 +55,13 @@ def _title_from_siblings(file_path: Path) -> str | None:
§3.4: an episode filename with no show name in it borrows the title from
a representative sibling in the same folder, never from the folder name
alone (an acronym-named show folder is a real, observed case).
+
+ Requires the sibling to carry its own episode number too, not just a
+ title — a folder where every file is a one-off-named Special (§3.4b)
+ has plenty of `display_title`s (guessit reads *a* title off nearly
+ anything) but none of them name the show; requiring a real episode
+ number alongside is what tells apart a genuinely representative sibling
+ from another Special just like this one.
"""
try:
names = sorted(p.name for p in file_path.parent.iterdir() if p.is_file())
@@ -70,11 +77,76 @@ def _title_from_siblings(file_path: Path) -> str | None:
if checked > MAX_SIBLINGS_CHECKED:
break
parsed = title_parse.parse_episode_filename(name)
- if parsed.display_title:
+ if parsed.display_title and parsed.episode is not None:
return parsed.display_title
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
+ 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.
+ """
+ try:
+ names = sorted(
+ p.name for p in file_path.parent.iterdir()
+ if p.is_file() and p.suffix.lower() in MEDIA_EXTENSIONS["video"])
+ except OSError:
+ return 1
+ try:
+ return names.index(file_path.name) + 1
+ except ValueError:
+ return 1
+
+
async def _make_thumbnail(file_path: Path, duration: float | None) -> bytes | None:
"""One ffmpeg frame grab at ~10% of duration (or 5s if unknown), scaled down."""
seek = max(0.0, (duration or 50.0) * 0.1)
@@ -165,15 +237,37 @@ class Enricher:
entry.id, fields.get("duration"), fields.get("width"), fields.get("height"))
ep = title_parse.parse_episode_filename(entry.name)
- if ep.episode is not None:
- title = ep.display_title or await asyncio.to_thread(
- _title_from_siblings, file_path)
- season = ep.season
- if season is None:
- season = await asyncio.to_thread(_season_from_ancestors, file_path)
+ season = ep.season
+ if season is None:
+ season = await asyncio.to_thread(_season_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)
+ episode = await asyncio.to_thread(_synthetic_episode_number, file_path)
fields["display_title"] = title or title_parse.naive_title(entry.name)
- fields["season"] = season
- fields["episode"] = ep.episode
+ fields["season"] = season if season is not None else 0
+ fields["episode"] = episode
else:
mv = title_parse.parse_movie_filename(entry.name)
fields["display_title"] = mv.display_title or mv.naive_title