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:
parent
cc2fb2ff79
commit
ae68750d9e
2 changed files with 73 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue