diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-29 15:43:38 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-29 15:43:38 +0200 |
| commit | 5d28d0c96cc8489645178b483835489779cb3887 (patch) | |
| tree | 90248f1c3211ca37495a774d0bc140109c299abc /packages/meshbay-node/src/meshbay_node/tmdb.py | |
| parent | 236e5e811355212945b89c4f7a99df5c837e97c7 (diff) | |
| download | meshbay-5d28d0c96cc8489645178b483835489779cb3887.tar.gz | |
feat(node): V8–V11 — show-branch ladder, year-aware _best_match, wider sequel_variants
V8: the TV/show branch of _tmdb_search used the old "first candidate over
0.6 wins" shape. It now shares one _tmdb_ladder helper with the movie
branch — score every candidate query, keep the best, fast-path a
confident primary hit. A year lifted off the show's folder name
(title_parse.year_in, e.g. "Some.Show.2022.S01") rescues a sub-0.6 hit
that lands on the exact year. title_parse.clean_query de-dots a
folder-derived title without naive_title's extension-stripping trap.
V9: _best_match gains an optional `year`. When the top result is not a
confident textual hit (ratio < 0.6) and a year was requested, a
different result of that exact release year is preferred — TMDB already
year-filtered the search, so this is a hard corroboration, not the fuzzy
re-rank §3.3 warns against. A confident top hit is never overridden.
search_movie/search_tv forward the year.
V10: sequel_variants widened — trailing Roman→digit as well as
digit→Roman, spelled-out indices (one..twelve / un..douze / ordinals),
and a "Part N" / "Chapitre N" wrapper. Still empty for a trailing word
that is not an index or a 4-digit year.
V11: when the primary hit is already decent (>= 0.6) and there is nothing
more specific to try (no alternative_title, no sequel variant — only a
punctuation restatement left), the ladder returns without the extra
requests. The clean-title common case is back to one call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/tmdb.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/tmdb.py | 58 |
1 files changed, 42 insertions, 16 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/tmdb.py b/packages/meshbay-node/src/meshbay_node/tmdb.py index a530660..c9623df 100644 --- a/packages/meshbay-node/src/meshbay_node/tmdb.py +++ b/packages/meshbay-node/src/meshbay_node/tmdb.py @@ -51,24 +51,47 @@ def _normalize(s: str) -> str: return re.sub(r"\s+", " ", s).strip() -def _best_match(query_title: str, results: list[dict], keys: tuple[str, ...]) -> tuple[dict | None, float]: +def _release_year_of(item: dict) -> int | None: + d = str(item.get("release_date") or item.get("first_air_date") or "") + return int(d[:4]) if d[:4].isdigit() else None + + +def _ratio_against(qn: str, item: dict, keys: tuple[str, ...]) -> float: + best = 0.0 + for k in keys: + val = item.get(k) + if val: + best = max(best, difflib.SequenceMatcher(None, qn, _normalize(str(val))).ratio()) + return best + + +def _best_match( + query_title: str, results: list[dict], keys: tuple[str, ...], + year: int | None = None, +) -> tuple[dict | None, float]: """ Trusts TMDB's own ranking (§3.3's last row — a locally-recomputed - re-rank picked a coincidentally-closer-looking wrong show once): only - the top result is considered. The similarity ratio is returned purely - as a confidence signal for the caller's fallback decision, never used - to pick a different candidate. + re-rank picked a coincidentally-closer-looking wrong show once): the + top result is what's returned. The similarity ratio rides along purely + as a confidence signal for the caller's fallback decision. + + One narrow exception (§10.1/V9): when the top result is *not* a + confident textual hit (ratio < 0.6) and a `year` was requested, a + different result of that **exact** release year is preferred. TMDB + already year-filtered the search, so an entry landing on the requested + year is a hard corroboration, not the fuzzy string re-rank §3.3 warns + against — and this never overrides a confident top hit. """ if not results: return None, 0.0 - top = results[0] qn = _normalize(query_title) - best_ratio = 0.0 - for k in keys: - val = top.get(k) - if val: - best_ratio = max(best_ratio, difflib.SequenceMatcher(None, qn, _normalize(str(val))).ratio()) - return top, best_ratio + top = results[0] + top_ratio = _ratio_against(qn, top, keys) + if year is not None and top_ratio < 0.6: + for item in results: + if _release_year_of(item) == year: + return item, _ratio_against(qn, item, keys) + return top, top_ratio class TmdbClient: @@ -138,12 +161,15 @@ class TmdbClient: params["year"] = year data = await self._get("search/movie", params) results = (data or {}).get("results", []) - return _best_match(title, results, ("title", "original_title")) + return _best_match(title, results, ("title", "original_title"), year=year) - async def search_tv(self, title: str) -> tuple[dict | None, float]: - data = await self._get("search/tv", {"query": title}) + async def search_tv(self, title: str, year: int | None = None) -> tuple[dict | None, float]: + params = {"query": title} + if year: + params["first_air_date_year"] = year + data = await self._get("search/tv", params) results = (data or {}).get("results", []) - return _best_match(title, results, ("name", "original_name")) + return _best_match(title, results, ("name", "original_name"), year=year) async def search_movie_results(self, title: str) -> list[dict]: """ |