From 0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 24 Aug 2026 14:33:20 +0200 Subject: feat(node,hub): season-specific overviews, manual TMDB match correction, and wizard polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two operator-facing fixes for a real 3-season show whose automatic TMDB match was wrong at the show level: per-season overview/air_date tabs in the detail modal (falling back to the show-level text when a season's own is empty), and a "Fix match…" search-and-correct affordance that re-resolves every file sharing the corrected show's display_title. New signed op OP_TMDB_OVERRIDE and two read-only pairs (season_meta_req/resp, tmdb_search_req/resp), MNP_VERSION 0.5 -> 0.6. Also: the create-group wizard gets a spinning indexing indicator and an app-selection step, group settings default the TMDB language to the operator's own locale (never as a global default), and a file renamed mid-session now re-triggers title parsing instead of being silently skipped by the enrichment dedup guard. Fixes two bugs found during this work: the search overlay's z-index lost to the base video-overlay class and rendered invisibly, and season_meta's own empty overview didn't fall back to the show-level one. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY --- .../tests/test_season_and_search_requests.py | 187 +++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 packages/meshbay-node/tests/test_season_and_search_requests.py (limited to 'packages/meshbay-node/tests/test_season_and_search_requests.py') diff --git a/packages/meshbay-node/tests/test_season_and_search_requests.py b/packages/meshbay-node/tests/test_season_and_search_requests.py new file mode 100644 index 0000000..8ba55fb --- /dev/null +++ b/packages/meshbay-node/tests/test_season_and_search_requests.py @@ -0,0 +1,187 @@ +""" +`_do_season_meta_request` (per-season TMDB overview/poster/air_date, for the +season-tab view — docs/mediacenter.md §5.4's fix for a 3-season show whose +overview read as season-3-specific for every season) and +`_do_tmdb_search_request` (raw TMDB candidates for an operator correcting a +wrong automatic match). Neither is a signed admin op — see each handler's own +docstring for why — so these tests only exercise the read path, unlike +test_tmdb_override_policy.py. +""" + +import pytest + +from meshbay_common.protocol import MNP +from meshbay_node.media_cache import MediaCache +from meshbay_node.transport.webrtc_server import WebRTCPeerSession + +pytestmark = pytest.mark.asyncio + + +def _session(media_cache=None, tmdb_client=None) -> WebRTCPeerSession: + session = WebRTCPeerSession.__new__(WebRTCPeerSession) + session._ctx = {"media_cache": media_cache, "tmdb_client": tmdb_client} + session.sent = [] + session._send = session.sent.append + return session + + +@pytest.fixture +async def media_cache(tmp_path): + c = MediaCache(db_path=tmp_path / "media_cache.db") + await c.open() + yield c + await c.close() + + +class FakeTmdbClient: + def __init__(self, season_json=None): + self.season_json = season_json + self.tv_season_calls = [] + self.movie_search_calls = [] + self.tv_search_calls = [] + + @staticmethod + def poster_url(path): + return f"https://image.tmdb.org/t/p/w500{path}" + + async def fetch_image(self, url): + return b"jpeg-bytes-for-" + url.encode() + + async def tv_season(self, tmdb_id, season, language=None): + self.tv_season_calls.append((tmdb_id, season, language)) + return self.season_json + + async def search_movie_results(self, title): + self.movie_search_calls.append(title) + return [{"id": 111, "title": title, "release_date": "2019-05-01", "poster_path": "/m.jpg"}] + + async def search_tv_results(self, title): + self.tv_search_calls.append(title) + return [{"id": 222, "name": title, "first_air_date": "2021-03-01", "poster_path": "/t.jpg"}] + + +# ── season_meta_req ────────────────────────────────────────────────────────── + +async def test_season_meta_missing_tmdb_id_is_refused(): + session = _session() + await session._do_season_meta_request({"season": 1}) + assert session.sent == [{"type": "error", "detail": "Missing tmdb_id or season"}] + + +async def test_season_meta_non_int_season_is_refused(): + session = _session() + await session._do_season_meta_request({"tmdb_id": "42", "season": "1"}) + assert session.sent == [{"type": "error", "detail": "Missing tmdb_id or season"}] + + +async def test_season_meta_with_no_cache_or_client_reports_zero_confidence(): + session = _session(media_cache=None, tmdb_client=None) + await session._do_season_meta_request({"tmdb_id": "42", "season": 1}) + assert session.sent == [{ + "type": MNP.SEASON_META_RESP, "v": session.sent[0]["v"], + "tmdb_id": "42", "season": 1, "confidence": 0, + }] + + +async def test_season_meta_cache_hit_skips_the_tmdb_call(media_cache): + await media_cache.set_season_meta("42", 3, { + "name": "Season 3", "overview": "cached overview", "air_date": "2023-01-01", + "poster_path": "/cached.jpg", + }) + client = FakeTmdbClient() + session = _session(media_cache=media_cache, tmdb_client=client) + + await session._do_season_meta_request({"tmdb_id": "42", "season": 3}) + + assert client.tv_season_calls == [], "a cached season must not be re-fetched" + resp = session.sent[0] + assert resp["type"] == MNP.SEASON_META_RESP + assert resp["confidence"] == 1.0 + assert resp["overview"] == "cached overview" + + +async def test_season_meta_cache_miss_fetches_and_caches(media_cache): + client = FakeTmdbClient(season_json={ + "name": "Season 1", "overview": "fresh overview", "air_date": "2020-01-01", + "poster_path": "/fresh.jpg", + }) + session = _session(media_cache=media_cache, tmdb_client=client) + + await session._do_season_meta_request({"tmdb_id": "7", "season": 1}) + + assert client.tv_season_calls == [("7", 1, None)] + resp = session.sent[0] + assert resp["overview"] == "fresh overview" + assert resp["poster_thumb_hash"] is not None + cached = await media_cache.get_season_meta("7", 1) + assert cached["overview"] == "fresh overview", "a fetched season must be cached for next time" + + +async def test_season_meta_empty_overview_falls_back_to_english(media_cache): + async def tv_season(tmdb_id, season, language=None): + if language == "en-US": + return {"name": "S1", "overview": "English overview", "air_date": "2020-01-01", + "poster_path": "/p.jpg"} + return {"name": "S1", "overview": "", "air_date": "2020-01-01", "poster_path": "/p.jpg"} + + client = FakeTmdbClient() + client.tv_season = tv_season + session = _session(media_cache=media_cache, tmdb_client=client) + + await session._do_season_meta_request({"tmdb_id": "9", "season": 1}) + + assert session.sent[0]["overview"] == "English overview" + + +# ── tmdb_search_req ────────────────────────────────────────────────────────── + +async def test_search_missing_query_is_refused(): + session = _session() + await session._do_tmdb_search_request({"media_type": "movie"}) + assert session.sent == [{"type": "error", "detail": "Missing query or media_type"}] + + +async def test_search_bad_media_type_is_refused(): + session = _session() + await session._do_tmdb_search_request({"query": "war", "media_type": "album"}) + assert session.sent == [{"type": "error", "detail": "Missing query or media_type"}] + + +async def test_search_with_no_cache_or_client_returns_empty_results(): + session = _session(media_cache=None, tmdb_client=None) + await session._do_tmdb_search_request({"query": "war", "media_type": "tv"}) + assert session.sent == [{ + "type": MNP.TMDB_SEARCH_RESP, "v": session.sent[0]["v"], + "query": "war", "media_type": "tv", "results": [], + }] + + +async def test_search_movie_calls_movie_search_and_echoes_media_type(media_cache): + client = FakeTmdbClient() + session = _session(media_cache=media_cache, tmdb_client=client) + + await session._do_tmdb_search_request({"query": "War of the Worlds", "media_type": "movie"}) + + assert client.movie_search_calls == ["War of the Worlds"] + assert client.tv_search_calls == [] + resp = session.sent[0] + assert resp["type"] == MNP.TMDB_SEARCH_RESP + assert resp["media_type"] == "movie", ( + "media_type must be echoed back — otherwise a movie search and a tv " + "search for the same query are indistinguishable to the client's " + "keyed response matching (transport.js tmdb_search_resp handler)") + assert resp["results"] == [{ + "tmdb_id": "111", "title": "War of the Worlds", "year": "2019", + "poster_thumb_hash": resp["results"][0]["poster_thumb_hash"], + }] + + +async def test_search_tv_calls_tv_search(media_cache): + client = FakeTmdbClient() + session = _session(media_cache=media_cache, tmdb_client=client) + + await session._do_tmdb_search_request({"query": "War of the Worlds", "media_type": "tv"}) + + assert client.tv_search_calls == ["War of the Worlds"] + assert client.movie_search_calls == [] + assert session.sent[0]["media_type"] == "tv" -- cgit v1.2.3