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.
121 lines
4.8 KiB
Python
121 lines
4.8 KiB
Python
"""Sonarr-style 'wishlist today's airings' automation handler — pure logic with the
|
|
calendar read + wishlist write injected, so it runs without a DB or media server."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.automation.handlers.video_auto_wishlist_airing import auto_video_add_airing_episodes
|
|
|
|
|
|
class _Deps:
|
|
def __init__(self):
|
|
self.progress = []
|
|
|
|
def update_progress(self, automation_id, **kw):
|
|
self.progress.append(kw)
|
|
|
|
|
|
def _row(tid, title, s, e, owned=False):
|
|
return {"show_tmdb_id": tid, "show_title": title, "season_number": s,
|
|
"episode_number": e, "title": "Ep", "air_date": "2026-06-21", "has_file": owned}
|
|
|
|
|
|
def test_adds_unowned_airings_grouped_by_show():
|
|
rows = [
|
|
_row(1, "Widows Bay", 1, 1),
|
|
_row(1, "Widows Bay", 1, 2),
|
|
_row(2, "Another Show", 3, 5),
|
|
_row(1, "Widows Bay", 1, 3, owned=True), # already owned → skipped
|
|
{"show_title": "No id", "season_number": 1, "episode_number": 1}, # no tmdb id → skipped
|
|
]
|
|
added = []
|
|
|
|
def add(tid, title, eps):
|
|
added.append((tid, title, len(eps)))
|
|
return len(eps)
|
|
|
|
res = auto_video_add_airing_episodes(
|
|
{"_automation_id": "a1"}, _Deps(),
|
|
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
|
|
assert res["shows"] == 2
|
|
assert (1, "Widows Bay", 2) in added and (2, "Another Show", 1) in added
|
|
|
|
|
|
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": "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):
|
|
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=season_meta)
|
|
ep = captured["eps"][0]
|
|
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"
|
|
|
|
|
|
def test_queries_the_calendar_for_today():
|
|
seen = {}
|
|
|
|
def fetch(today):
|
|
seen["today"] = today
|
|
return []
|
|
|
|
auto_video_add_airing_episodes({"_automation_id": "a"}, _Deps(),
|
|
fetch_airing=fetch, add_episodes=lambda *a: 0,
|
|
today_fn=lambda: "2026-06-21")
|
|
assert seen["today"] == "2026-06-21" # start == end == today
|
|
|
|
|
|
def test_nothing_airing_is_a_clean_noop():
|
|
res = auto_video_add_airing_episodes({"_automation_id": "a"}, _Deps(),
|
|
fetch_airing=lambda t: [], add_episodes=lambda *a: 0,
|
|
today_fn=lambda: "2026-06-21")
|
|
assert res["status"] == "completed" and res["episodes_added"] == 0
|
|
|
|
|
|
def test_error_is_caught_and_reported():
|
|
def boom(today):
|
|
raise RuntimeError("calendar down")
|
|
|
|
deps = _Deps()
|
|
res = auto_video_add_airing_episodes({"_automation_id": "a"}, deps,
|
|
fetch_airing=boom, add_episodes=lambda *a: 0)
|
|
assert res["status"] == "error" and "calendar down" in res["error"]
|
|
assert any(p.get("status") == "error" for p in deps.progress)
|