Video auto-wishlist: pull episode metadata from TMDB, like a manual add

Root cause of the metadata loss: a MANUAL 'add to wishlist' gets its episode
data from the TMDB season fetch (engine.tmdb_season — absolute still, overview,
season poster), while the automation read the local DB episodes table, where
stills are frequently empty/Plex-relative. So auto-added episodes came in blank
even after carrying the DB values.

The handler now fetches the SAME TMDB season metadata (cached per season,
injected for tests) and prefers it, falling back to the calendar/DB values if
TMDB is unavailable. Auto-added episodes now match manual ones.
This commit is contained in:
BoulderBadgeDad 2026-06-21 09:41:12 -07:00
parent cc2fb2ff79
commit ae68750d9e
2 changed files with 73 additions and 16 deletions

View file

@ -34,6 +34,28 @@ def _default_add_episodes(show_tmdb_id: Any, show_title: Any, episodes: List[Dic
show_tmdb_id, show_title, episodes, server_source=resolve_video_server())
def _default_season_meta(tmdb_id: Any, season_number: Any):
"""The SAME TMDB season fetch the show modal uses for a manual add — so auto-added
episodes carry identical stills / overviews / season posters, not the patchy values
the local DB happens to hold."""
from core.video.enrichment.engine import get_video_enrichment_engine
return get_video_enrichment_engine().tmdb_season(tmdb_id, season_number)
def _season_lookup(season_meta, tmdb_id, season_number, cache):
"""(season_poster_url, {episode_number: tmdb_episode}) for a show+season, fetched
once and cached. A TMDB hiccup degrades to empty (DB values fill in)."""
key = (tmdb_id, season_number)
if key not in cache:
try:
sm = season_meta(tmdb_id, season_number) or {}
except Exception: # noqa: BLE001 - never let a metadata fetch break the run
sm = {}
emap = {e.get('episode_number'): e for e in (sm.get('episodes') or []) if isinstance(e, dict)}
cache[key] = (sm.get('poster_url'), emap)
return cache[key]
def auto_video_add_airing_episodes(
config: Dict[str, Any],
deps: AutomationDeps,
@ -41,12 +63,14 @@ def auto_video_add_airing_episodes(
fetch_airing: Optional[Callable[[str], List[Dict[str, Any]]]] = None,
add_episodes: Optional[Callable[[Any, Any, List[Dict[str, Any]]], int]] = None,
today_fn: Optional[Callable[[], str]] = None,
season_meta: Optional[Callable[[Any, Any], Any]] = None,
) -> Dict[str, Any]:
"""Add today's airing (unowned, followed-show) episodes to the video wishlist.
Returns ``{'status': 'completed', 'episodes_added': int, 'shows': int, ...}``."""
fetch_airing = fetch_airing or _default_fetch_airing
add_episodes = add_episodes or _default_add_episodes
season_meta = season_meta or _default_season_meta
today_fn = today_fn or (lambda: date.today().isoformat())
automation_id = config.get('_automation_id')
try:
@ -59,21 +83,26 @@ def auto_video_add_airing_episodes(
# real season/episode. add_episodes_to_wishlist is idempotent, so re-runs
# never duplicate.
by_show: Dict[tuple, List[Dict[str, Any]]] = {}
season_cache: Dict[tuple, tuple] = {}
for r in rows:
if r.get('has_file'):
continue
tid = r.get('show_tmdb_id')
if not tid or r.get('season_number') is None or r.get('episode_number') is None:
sn, en = r.get('season_number'), r.get('episode_number')
if not tid or sn is None or en is None:
continue
# Pull the SAME TMDB metadata a manual add gets (still + overview + season
# poster); fall back to the calendar/DB values if TMDB is unavailable.
poster, emap = _season_lookup(season_meta, tid, sn, season_cache)
tm = emap.get(en) or {}
by_show.setdefault((tid, r.get('show_title')), []).append({
'season_number': r.get('season_number'),
'episode_number': r.get('episode_number'),
'title': r.get('title'),
'air_date': r.get('air_date'),
# carry the rich metadata so auto-added episodes look like manual ones
# (synopsis + still thumbnail), not blank rows
'overview': r.get('overview'),
'still_url': r.get('still_url'),
'season_number': sn,
'episode_number': en,
'title': r.get('title') or tm.get('title'),
'air_date': r.get('air_date') or tm.get('air_date'),
'overview': tm.get('overview') or r.get('overview'),
'still_url': tm.get('still_url') or r.get('still_url'),
'season_poster_url': poster,
})
added = 0

View file

@ -35,7 +35,8 @@ def test_adds_unowned_airings_grouped_by_show():
res = auto_video_add_airing_episodes(
{"_automation_id": "a1"}, _Deps(),
fetch_airing=lambda today: rows, add_episodes=add, today_fn=lambda: "2026-06-21")
fetch_airing=lambda today: rows, add_episodes=add, today_fn=lambda: "2026-06-21",
season_meta=lambda *a: None)
assert res["status"] == "completed"
assert res["episodes_added"] == 3 # 2 of Widows Bay + 1 of Another Show
@ -43,11 +44,19 @@ def test_adds_unowned_airings_grouped_by_show():
assert (1, "Widows Bay", 2) in added and (2, "Another Show", 1) in added
def test_episode_synopsis_and_still_are_carried_to_the_wishlist():
# auto-added episodes must look like manual ones — synopsis + still, not blank
rows = [{"show_tmdb_id": 1, "show_title": "X", "season_number": 1, "episode_number": 2,
def test_uses_tmdb_season_metadata_like_a_manual_add():
# the SAME TMDB source the manual 'add to wishlist' uses — absolute still + overview
# + season poster — preferred over the patchy DB values.
rows = [{"show_tmdb_id": 5, "show_title": "Y", "season_number": 2, "episode_number": 3,
"title": "Ep", "air_date": "2026-06-21", "has_file": False,
"overview": "A synopsis.", "still_url": "/library/metadata/9/thumb/1"}]
"overview": "db overview", "still_url": "/db/still"}]
def season_meta(tid, sn):
assert (tid, sn) == (5, 2)
return {"poster_url": "https://img/tmdb/s2.jpg",
"episodes": [{"episode_number": 3, "overview": "TMDB overview",
"still_url": "https://img/tmdb/s2e3.jpg"}]}
captured = {}
def add(tid, title, eps):
@ -56,9 +65,28 @@ def test_episode_synopsis_and_still_are_carried_to_the_wishlist():
auto_video_add_airing_episodes({"_automation_id": "a"}, _Deps(),
fetch_airing=lambda t: rows, add_episodes=add,
today_fn=lambda: "2026-06-21")
today_fn=lambda: "2026-06-21", season_meta=season_meta)
ep = captured["eps"][0]
assert ep["overview"] == "A synopsis."
assert ep["overview"] == "TMDB overview" # TMDB preferred over DB
assert ep["still_url"] == "https://img/tmdb/s2e3.jpg"
assert ep["season_poster_url"] == "https://img/tmdb/s2.jpg"
def test_falls_back_to_db_values_when_tmdb_unavailable():
# if the TMDB fetch returns nothing, still carry the calendar/DB overview + still
rows = [{"show_tmdb_id": 1, "show_title": "X", "season_number": 1, "episode_number": 2,
"has_file": False, "overview": "db synopsis", "still_url": "/library/metadata/9/thumb/1"}]
captured = {}
def add(tid, title, eps):
captured["eps"] = eps
return len(eps)
auto_video_add_airing_episodes({"_automation_id": "a"}, _Deps(),
fetch_airing=lambda t: rows, add_episodes=add,
today_fn=lambda: "2026-06-21", season_meta=lambda *a: None)
ep = captured["eps"][0]
assert ep["overview"] == "db synopsis"
assert ep["still_url"] == "/library/metadata/9/thumb/1"