diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 103 |
1 files changed, 84 insertions, 19 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index dd68e18..788a8f1 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -3076,11 +3076,13 @@ class WebRTCPeerSession: video_root/tmdb_config: it replaces what every member sees for a show/movie, node-wide (media_cache is shared, not per-viewer). - Applied to every entry sharing the representative file's - display_title — the same grouping the poster grid itself uses - (§3.4/§V6) — not just the one file the operator happened to be - looking at, so the correction actually sticks regardless of which - episode a future render picks as representative. + For a **show**, applied to every entry sharing the representative + file's display_title — the same grouping the poster grid uses + (§3.4/§V6) — so the correction sticks regardless of which episode a + future render picks as representative. For a **movie** it is applied + to that one file only: guessit gives a whole franchise the same + display_title, and a fan-out there corrected the wrong films (found + live, 2026-08-29). See `_admin_exec_tmdb_override`. Keyed by `file_id`, not `path` — see `_do_media_meta_request`'s docstring for why a folder-level path cannot name one file. @@ -3140,11 +3142,25 @@ class WebRTCPeerSession: if tmdb_client is not None: meta = await self._tmdb_build_meta(tmdb_client, tmdb_id, media_type, {}) await media_cache.set_tmdb_meta(tmdb_id, media_type, meta) - target_title = entry.display_title or entry.name - matched = [e for e in ctx["index"].entries - if e.type == "video" and (e.display_title or e.name) == target_title] + # A show's episodes are many files that legitimately share one match, + # and which episode a render picks as representative rotates — so a + # show override fans out across every entry with the same + # display_title. A *movie* is one file: fanning out by display_title + # there is a bug — guessit gives every + # "<franchise> - <year> - <subtitle>.mkv" the same display_title, so + # "Fix match" on one entry rewrote the whole franchise (found live, + # 2026-08-29). Each corrected file is also marked as a manual + # override so ops.rematch_video / a rename never wipe it. + is_show = entry.season is not None and entry.episode is not None + if is_show: + target_title = entry.display_title or entry.name + matched = [e for e in ctx["index"].entries + if e.type == "video" and (e.display_title or e.name) == target_title] + else: + matched = [entry] for e in matched: await media_cache.set_file_tmdb(e.id, tmdb_id, media_type) + await media_cache.mark_tmdb_override(e.id) self._audit("tmdb_override", subject) notice = {"type": MNP.TMDB_OVERRIDE_ACK, "v": MNP_VERSION, "file_id": file_id, @@ -3157,9 +3173,22 @@ class WebRTCPeerSession: async def _tmdb_search(self, tmdb_client, entry, is_show: bool): """ - §3.3's retry ladder: the parsed title first, then a couple of - generic, non-per-title fallbacks — never re-ranking TMDB's own - top result locally (§3.3's last row). + §3.3's retry ladder. TMDB's own top result is still trusted per + query (§3.3's last row — no local re-ranking of *its* list); what + changed is that the ladder now *scores every candidate query* and + keeps the best, instead of returning the first that merely clears + 0.6. + + The bare parsed title is the weakest query: guessit drops a + "Volume 2", strips a real subtitle into `alternative_title`, and + renders a sequel number where TMDB uses a Roman numeral. A wrong + film that happened to score ~0.7 against that weak query — a + same-year making-of documentary, or a franchise entry whose + localized TMDB title *is* the franchise name — used to win outright + before `alternative_title` or the Roman-numeral variant was ever + tried. Found live (2026-08-29): a numbered sequel matched a + same-year documentary; a two-volume film's second part matched the + first; several franchise entries matched one early entry. """ from meshbay_node.indexer import title_parse @@ -3172,21 +3201,57 @@ class WebRTCPeerSession: result, ratio = await tmdb_client.search_tv(naive) return result, ratio + def _release_year(res: dict) -> int | None: + d = str(res.get("release_date") or res.get("first_air_date") or "") + return int(d[:4]) if d[:4].isdigit() else None + parsed = title_parse.parse_movie_filename(entry.name) title = entry.display_title or parsed.display_title or parsed.naive_title + + # Fast path, unchanged in effect: a strong direct hit still returns + # on the first call, so the common case costs exactly one request + # and the new ladder below only engages in the ambiguous 0<ratio<0.85 + # zone where every one of the live bugs lived. result, ratio = await tmdb_client.search_movie(title, parsed.year) - if result is not None and ratio >= 0.6: + if result is not None and ratio >= 0.85: return result, ratio - for candidate in filter(None, [parsed.alt_title, parsed.naive_title, - *title_parse.sequel_variants(title)]): + + best_result, best_score = (result, ratio) if result is not None else (None, 0.0) + + def _apply_year_rescue(res: dict, r: float) -> float: + # Only for a query whose own top hit is weak on its face + # (ratio < 0.6): TMDB already year-filtered the search, so its + # top result landing exactly on the filename's year is a hard + # corroborating signal that the low string ratio is a + # localized/rearranged title, not a wrong film. Never lets year + # equality outrank a genuinely strong textual match elsewhere. + if r < 0.6 and parsed.year and _release_year(res) == parsed.year: + return max(r, 0.6) + return r + + if best_result is not None: + best_score = _apply_year_rescue(best_result, best_score) + + for candidate in filter(None, [parsed.alt_title, + *title_parse.sequel_variants(title), + parsed.naive_title]): if candidate == title: continue - result2, ratio2 = await tmdb_client.search_movie(candidate, parsed.year) - if result2 is not None and ratio2 > ratio: - result, ratio = result2, ratio2 - if ratio >= 0.6: + r2, ratio2 = await tmdb_client.search_movie(candidate, parsed.year) + if r2 is None and parsed.year: + # A year-filtered search that finds nothing: the filename's + # year tag may be an edition/regional year TMDB doesn't + # carry. Retry the same candidate unconstrained before + # dropping it. + r2, ratio2 = await tmdb_client.search_movie(candidate) + if r2 is None: + continue + score2 = _apply_year_rescue(r2, ratio2) + if score2 > best_score: + best_result, best_score = r2, score2 + if best_score >= 0.85: break - return result, ratio + return best_result, best_score @staticmethod async def _tmdb_build_meta(tmdb_client, tmdb_id: str, media_type: str, result: dict) -> dict: |