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:
parent
e5724c6faa
commit
b567eb4808
2 changed files with 50 additions and 16 deletions
|
|
@ -278,15 +278,19 @@ class VideoEnrichmentEngine:
|
||||||
w = self.workers.get("tmdb")
|
w = self.workers.get("tmdb")
|
||||||
if not w or not w.enabled:
|
if not w or not w.enabled:
|
||||||
return []
|
return []
|
||||||
try:
|
cached = self._cache_get(("trending",))
|
||||||
results = w.client.trending() or []
|
if cached is None:
|
||||||
except Exception:
|
try:
|
||||||
logger.exception("video trending failed")
|
cached = w.client.trending() or []
|
||||||
return []
|
self._cache_put(("trending",), cached, ttl=3600)
|
||||||
for r in results:
|
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"):
|
if r.get("tmdb_id"):
|
||||||
r["library_id"] = self.db.library_id_for_tmdb(r["kind"], r["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:
|
def tmdb_detail(self, kind, tmdb_id) -> dict | None:
|
||||||
"""Full detail for a TMDB title not in the library — same shape as the
|
"""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")
|
w = self.workers.get("tmdb")
|
||||||
if not w or not w.enabled:
|
if not w or not w.enabled:
|
||||||
return None
|
return None
|
||||||
|
key = ("season", tv_id, season_number)
|
||||||
|
cached = self._cache_get(key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
try:
|
try:
|
||||||
se = w.client.season_episodes(tv_id, season_number)
|
se = w.client.season_episodes(tv_id, season_number)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -365,8 +373,10 @@ class VideoEnrichmentEngine:
|
||||||
"still_url": e.get("still_url"), "has_still": bool(e.get("still_url")),
|
"still_url": e.get("still_url"), "has_still": bool(e.get("still_url")),
|
||||||
"owned": False}
|
"owned": False}
|
||||||
for e in (se.get("episodes") or []) if e.get("episode_number") is not None]
|
for e in (se.get("episodes") or []) if e.get("episode_number") is not None]
|
||||||
return {"season_number": season_number, "overview": se.get("overview"),
|
out = {"season_number": season_number, "overview": se.get("overview"),
|
||||||
"poster_url": se.get("poster_url"), "episodes": eps}
|
"poster_url": se.get("poster_url"), "episodes": eps}
|
||||||
|
self._cache_put(key, out)
|
||||||
|
return out
|
||||||
|
|
||||||
def person_detail(self, tmdb_id) -> dict | None:
|
def person_detail(self, tmdb_id) -> dict | None:
|
||||||
"""A person (actor/director) page — bio + filmography, each credit
|
"""A person (actor/director) page — bio + filmography, each credit
|
||||||
|
|
@ -374,13 +384,17 @@ class VideoEnrichmentEngine:
|
||||||
w = self.workers.get("tmdb")
|
w = self.workers.get("tmdb")
|
||||||
if not w or not w.enabled:
|
if not w or not w.enabled:
|
||||||
return None
|
return None
|
||||||
try:
|
p = self._cache_get(("person", tmdb_id))
|
||||||
p = w.client.person(tmdb_id)
|
if p is None:
|
||||||
except Exception:
|
try:
|
||||||
logger.exception("person_detail failed for %s", tmdb_id)
|
p = w.client.person(tmdb_id)
|
||||||
return None
|
except Exception:
|
||||||
if not p:
|
logger.exception("person_detail failed for %s", tmdb_id)
|
||||||
return None
|
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 []:
|
for c in p.get("credits") or []:
|
||||||
if c.get("tmdb_id"):
|
if c.get("tmdb_id"):
|
||||||
c["library_id"] = self.db.library_id_for_tmdb(c["kind"], c["tmdb_id"])
|
c["library_id"] = self.db.library_id_for_tmdb(c["kind"], c["tmdb_id"])
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,26 @@ def test_engine_search_annotates_library(db):
|
||||||
assert "library_id" not in res[2] # people aren't library-matched
|
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):
|
def test_engine_trending_annotates_library(db):
|
||||||
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "Owned", "tmdb_id": 1})
|
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "Owned", "tmdb_id": 1})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue