From b567eb4808227affa5b8696ea3dbad133127cea5 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 15 Jun 2026 12:55:56 -0700 Subject: [PATCH] video: cache person pages, lazy preview seasons + trending (close the gaps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owned-item extras and preview detail payloads were already cached (30-min TTL); person_detail, tmdb_season, and trending were not — so person pages and lazy preview seasons re-hit TMDB each view. Now cached too (trending 1h). Library ownership is re-annotated fresh on each call so 'In Library' badges stay current while the expensive TMDB fetch is reused. Tests for person + season caching. --- core/video/enrichment/engine.py | 46 +++++++++++++++++++++------------ tests/test_video_enrichment.py | 20 ++++++++++++++ 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/core/video/enrichment/engine.py b/core/video/enrichment/engine.py index 18c5c432..cf5409e7 100644 --- a/core/video/enrichment/engine.py +++ b/core/video/enrichment/engine.py @@ -278,15 +278,19 @@ class VideoEnrichmentEngine: w = self.workers.get("tmdb") if not w or not w.enabled: return [] - try: - results = w.client.trending() or [] - except Exception: - logger.exception("video trending failed") - return [] - for r in results: + cached = self._cache_get(("trending",)) + if cached is None: + try: + cached = w.client.trending() or [] + self._cache_put(("trending",), cached, ttl=3600) + except Exception: + logger.exception("video trending failed") + return [] + # Re-annotate ownership fresh each call (cheap) so it tracks the library. + for r in cached: if r.get("tmdb_id"): r["library_id"] = self.db.library_id_for_tmdb(r["kind"], r["tmdb_id"]) - return results + return cached def tmdb_detail(self, kind, tmdb_id) -> dict | None: """Full detail for a TMDB title not in the library — same shape as the @@ -352,6 +356,10 @@ class VideoEnrichmentEngine: w = self.workers.get("tmdb") if not w or not w.enabled: return None + key = ("season", tv_id, season_number) + cached = self._cache_get(key) + if cached is not None: + return cached try: se = w.client.season_episodes(tv_id, season_number) except Exception: @@ -365,8 +373,10 @@ class VideoEnrichmentEngine: "still_url": e.get("still_url"), "has_still": bool(e.get("still_url")), "owned": False} for e in (se.get("episodes") or []) if e.get("episode_number") is not None] - return {"season_number": season_number, "overview": se.get("overview"), - "poster_url": se.get("poster_url"), "episodes": eps} + out = {"season_number": season_number, "overview": se.get("overview"), + "poster_url": se.get("poster_url"), "episodes": eps} + self._cache_put(key, out) + return out def person_detail(self, tmdb_id) -> dict | None: """A person (actor/director) page — bio + filmography, each credit @@ -374,13 +384,17 @@ class VideoEnrichmentEngine: w = self.workers.get("tmdb") if not w or not w.enabled: return None - try: - p = w.client.person(tmdb_id) - except Exception: - logger.exception("person_detail failed for %s", tmdb_id) - return None - if not p: - return None + p = self._cache_get(("person", tmdb_id)) + if p is None: + try: + p = w.client.person(tmdb_id) + except Exception: + logger.exception("person_detail failed for %s", tmdb_id) + return None + if not p: + return None + self._cache_put(("person", tmdb_id), p) + # Re-annotate ownership fresh each call (cheap) so it tracks the library. for c in p.get("credits") or []: if c.get("tmdb_id"): c["library_id"] = self.db.library_id_for_tmdb(c["kind"], c["tmdb_id"]) diff --git a/tests/test_video_enrichment.py b/tests/test_video_enrichment.py index a3b4ebce..f513908a 100644 --- a/tests/test_video_enrichment.py +++ b/tests/test_video_enrichment.py @@ -257,6 +257,26 @@ def test_engine_search_annotates_library(db): assert "library_id" not in res[2] # people aren't library-matched +def test_person_detail_caches_tmdb_call(db): + calls = [] + class Tmdb: + enabled = True + def person(self, tid): calls.append(tid); return {"tmdb_id": tid, "name": "P", "credits": []} + eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()}) + eng.person_detail(7); eng.person_detail(7) + assert calls == [7] # second view served from cache + + +def test_tmdb_season_caches_tmdb_call(db): + calls = [] + class Tmdb: + enabled = True + def season_episodes(self, tv, sn): calls.append((tv, sn)); return {"overview": "S", "episodes": []} + eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()}) + eng.tmdb_season(9, 1); eng.tmdb_season(9, 1) + assert calls == [(9, 1)] + + def test_engine_trending_annotates_library(db): mid = db.upsert_movie("plex", {"server_id": "m1", "title": "Owned", "tmdb_id": 1})