video: cache person pages, lazy preview seasons + trending (close the gaps)

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.
This commit is contained in:
BoulderBadgeDad 2026-06-15 12:55:56 -07:00
parent e5724c6faa
commit b567eb4808
2 changed files with 50 additions and 16 deletions

View file

@ -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"])

View file

@ -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})