video enrichment: backfill season posters from TMDB (server usually lacks them)
The media server rarely has distinct per-season art, so season cards fell back to a gradient. TMDB's show detail carries a poster_path per season — the show worker now returns those, and enrichment_apply backfills seasons.poster_url for seasons the server left without art (gap-only, never clobbers server art). The image proxy streams a stored full URL (TMDB) directly vs. proxying a server path. Seam tests: TMDB returns season posters, backfill fills only missing seasons.
This commit is contained in:
parent
5e8143dd1d
commit
878e467f69
5 changed files with 61 additions and 0 deletions
|
|
@ -23,6 +23,17 @@ def register_routes(bp):
|
|||
try:
|
||||
import requests
|
||||
from config.settings import config_manager
|
||||
path = ref["poster_url"]
|
||||
# Enrichment can store a full external URL (e.g. a TMDB season poster) —
|
||||
# stream it directly; otherwise it's a server path needing the token.
|
||||
if path.startswith("http://") or path.startswith("https://"):
|
||||
upstream = requests.get(path, timeout=15, stream=True)
|
||||
if upstream.status_code != 200:
|
||||
abort(404)
|
||||
resp = Response(upstream.iter_content(8192),
|
||||
content_type=upstream.headers.get("Content-Type", "image/jpeg"))
|
||||
resp.headers["Cache-Control"] = "public, max-age=86400"
|
||||
return resp
|
||||
source = ref.get("server_source")
|
||||
if source == "plex":
|
||||
cfg = config_manager.get_plex_config() or {}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,15 @@ class TMDBClient:
|
|||
if ert:
|
||||
meta["runtime_minutes"] = ert[0]
|
||||
meta["tvdb_id"] = _int(ext.get("tvdb_id"))
|
||||
# Per-season posters — the reliable source of distinct season art
|
||||
# (the media server usually lacks it). Backfilled into seasons.
|
||||
seasons = []
|
||||
for s in (dr.get("seasons") or []):
|
||||
pp, sn = s.get("poster_path"), s.get("season_number")
|
||||
if pp and sn is not None:
|
||||
seasons.append({"season_number": sn, "poster_url": self.IMG + pp})
|
||||
if seasons:
|
||||
meta["seasons"] = seasons
|
||||
except Exception:
|
||||
logger.exception("TMDB details fetch failed for %s", title or tmdb_id)
|
||||
return {"id": tmdb_id, "metadata": {k: v for k, v in meta.items() if v}}
|
||||
|
|
|
|||
|
|
@ -250,6 +250,17 @@ class VideoDatabase:
|
|||
has = conn.execute(f"SELECT 1 FROM {lt} WHERE {oc}=? LIMIT 1", (item_id,)).fetchone()
|
||||
if not has:
|
||||
self._set_genres(conn, lt, oc, item_id, genres)
|
||||
# Per-season poster backfill (TMDB) — fills only seasons the server
|
||||
# left without art.
|
||||
seasons_meta = (metadata or {}).get("seasons")
|
||||
if matched and seasons_meta and tbl == "shows":
|
||||
for s in seasons_meta:
|
||||
sn, purl = s.get("season_number"), s.get("poster_url")
|
||||
if sn is None or not purl:
|
||||
continue
|
||||
conn.execute(
|
||||
"UPDATE seasons SET poster_url=COALESCE(NULLIF(poster_url, ''), ?) "
|
||||
"WHERE show_id=? AND season_number=?", (purl, item_id, sn))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
|
|||
|
|
@ -542,6 +542,22 @@ def test_enrichment_backfills_genres_when_item_has_none(db):
|
|||
assert db.movie_detail(mid)["genres"] == ["Comedy", "Drama"]
|
||||
|
||||
|
||||
def test_enrichment_backfills_season_posters_only_when_missing(db):
|
||||
# Season 1 has no art (server didn't provide it); season 2 has server art.
|
||||
sid = db.upsert_show_tree("plex", {"server_id": "s1", "title": "S", "seasons": [
|
||||
{"season_number": 1, "episodes": [{"episode_number": 1}]},
|
||||
{"season_number": 2, "poster_url": "/server.jpg", "episodes": [{"episode_number": 1}]}]})
|
||||
db.enrichment_apply("tmdb", "show", sid, matched=True, external_id=1, metadata={"seasons": [
|
||||
{"season_number": 1, "poster_url": "https://image.tmdb.org/s1.jpg"},
|
||||
{"season_number": 2, "poster_url": "https://image.tmdb.org/s2.jpg"}]})
|
||||
with db.connect() as c:
|
||||
rows = {r["season_number"]: r["poster_url"] for r in c.execute(
|
||||
"SELECT season_number, poster_url FROM seasons WHERE show_id=?", (sid,)).fetchall()}
|
||||
assert rows[1] == "https://image.tmdb.org/s1.jpg" # gap filled from TMDB
|
||||
assert rows[2] == "/server.jpg" # server art kept, not clobbered
|
||||
assert db.show_detail(sid)["seasons"][0]["has_poster"] is True
|
||||
|
||||
|
||||
def test_error_status_is_distinct_and_retryable_in_ui(db):
|
||||
a = db.upsert_movie("plex", {"server_id": "m1", "title": "A"})
|
||||
db.enrichment_apply("tmdb", "movie", a, matched=False, error=True)
|
||||
|
|
|
|||
|
|
@ -98,6 +98,20 @@ def test_tmdb_pulls_full_metadata(monkeypatch):
|
|||
assert m["genres"] == ["Sci-Fi", "Drama"] and m["status"] == "Released" and m["imdb_id"] == "tt1"
|
||||
|
||||
|
||||
def test_tmdb_show_returns_season_posters(monkeypatch):
|
||||
class _Resp:
|
||||
def __init__(self, b): self._b = b
|
||||
def raise_for_status(self): pass
|
||||
def json(self): return self._b
|
||||
detail = {"overview": "O", "external_ids": {}, "seasons": [
|
||||
{"season_number": 1, "poster_path": "/a.jpg"},
|
||||
{"season_number": 2, "poster_path": None}]}
|
||||
monkeypatch.setitem(sys.modules, "requests", types.SimpleNamespace(get=lambda u, **k: _Resp(detail)))
|
||||
m = TMDBClient("KEY").match("show", "Show", 2020, known_id=1396)["metadata"]
|
||||
assert m["seasons"] == [{"season_number": 1,
|
||||
"poster_url": "https://image.tmdb.org/t/p/original/a.jpg"}]
|
||||
|
||||
|
||||
def test_tmdb_client_raises_on_rate_limit(monkeypatch):
|
||||
# A 429 must raise (→ worker records 'error'), not silently return no-match.
|
||||
class _Resp429:
|
||||
|
|
|
|||
Loading…
Reference in a new issue