Fix queue worker failure handling and thumbnail fallbacks

This commit is contained in:
Dymas
2026-05-16 11:02:18 +02:00
parent 0194476b33
commit 92762b07d5
7 changed files with 181 additions and 17 deletions
+120
View File
@@ -225,6 +225,48 @@ class DownloadQueueCancelTests(unittest.TestCase):
killpg.assert_called_once_with(4321, queue_jobs.signal.SIGTERM)
class DownloadQueueWorkerFailureTests(unittest.TestCase):
def setUp(self):
queue = APP.DownloadQueue(lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"}, start_worker=False)
with queue._connect() as conn:
conn.execute("DELETE FROM jobs")
def test_run_job_marks_startup_failure_and_cleans_staging(self):
queue = APP.DownloadQueue(lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"}, start_worker=False)
job = queue.add({"query": "Queue Show", "title": "Queue Show", "season": "1", "episodes": "1"})
staging_dir = queue_jobs.job_staging_dir(job)
with mock.patch.object(queue_jobs.subprocess, "Popen", side_effect=FileNotFoundError("ani-cli missing")):
queue._run_job(job)
stored = queue._find(job["id"])
self.assertEqual(stored["status"], "failed")
self.assertEqual(stored["exit_code"], 1)
self.assertIn("Could not start download", stored["log"][-1])
self.assertFalse(staging_dir.exists())
def test_failed_download_cleans_staging_dir(self):
queue = APP.DownloadQueue(lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"}, start_worker=False)
job = queue.add({"query": "Queue Show", "title": "Queue Show", "season": "1", "episodes": "1"})
staging_dir = queue_jobs.job_staging_dir(job)
staging_dir.mkdir(parents=True, exist_ok=True)
(staging_dir / "partial.mp4").write_text("partial", encoding="utf-8")
class FakeProc:
pid = 4321
stdout = []
def wait(self):
return 1
with mock.patch.object(queue_jobs.subprocess, "Popen", return_value=FakeProc()):
queue._run_job(job)
stored = queue._find(job["id"])
self.assertEqual(stored["status"], "failed")
self.assertFalse(staging_dir.exists())
class WatchlistRefreshAllTests(unittest.TestCase):
def test_refresh_all_iterates_every_show_id(self):
store = object.__new__(APP.WatchlistStore)
@@ -475,6 +517,84 @@ class ThumbnailEventRecoveryTests(unittest.TestCase):
self.assertIsNone(second)
def test_ensure_thumbnail_falls_back_from_stale_animeschedule_route(self):
now = APP.now_iso()
item = {
"show_id": "show-thumb-2",
"title": "Route Fallback Show",
"category": "watching",
"downloaded": False,
"status": "tracked",
"status_message": "Waiting for thumbnail.",
"created_at": now,
"updated_at": now,
"last_checked": None,
"sub_count": 0,
"dub_count": 0,
"expected_count": 0,
"airing_status": None,
"sub_latest_episode": None,
"dub_latest_episode": None,
"animeschedule_route": "stale-route",
"animeschedule_title": "Route Fallback Show",
"anidb_aid": None,
"anidb_title": None,
"thumbnail_path": None,
"thumbnail_checked_at": None,
}
with APP.WATCHLIST.lock, APP.WATCHLIST._connect() as conn:
APP.WATCHLIST._upsert_conn(conn, item)
with mock.patch.object(APP, "fetch_animeschedule_cover_by_route", side_effect=RuntimeError("stale route")), mock.patch.object(
APP, "fetch_animeschedule_cover", return_value={"route": "fresh-route", "title": "Route Fallback Show", "url": "https://example.com/cover.jpg"}
) as title_lookup, mock.patch.object(APP, "cache_thumbnail_image", return_value="show-thumb-2.jpg"):
result = APP.WATCHLIST.ensure_thumbnail("show-thumb-2")
title_lookup.assert_called_once_with("Route Fallback Show")
self.assertEqual(result, APP.THUMBNAIL_ROOT / "show-thumb-2.jpg")
refreshed = APP.WATCHLIST.get("show-thumb-2")
self.assertEqual(refreshed["animeschedule_route"], "fresh-route")
def test_ensure_thumbnail_falls_back_from_stale_anidb_aid(self):
now = APP.now_iso()
item = {
"show_id": "show-thumb-3",
"title": "AniDB Fallback Show",
"category": "watching",
"downloaded": False,
"status": "tracked",
"status_message": "Waiting for thumbnail.",
"created_at": now,
"updated_at": now,
"last_checked": None,
"sub_count": 0,
"dub_count": 0,
"expected_count": 0,
"airing_status": None,
"sub_latest_episode": None,
"dub_latest_episode": None,
"animeschedule_route": None,
"animeschedule_title": None,
"anidb_aid": "12345",
"anidb_title": "AniDB Fallback Show",
"thumbnail_path": None,
"thumbnail_checked_at": None,
}
with APP.WATCHLIST.lock, APP.WATCHLIST._connect() as conn:
APP.WATCHLIST._upsert_conn(conn, item)
with mock.patch.object(APP, "fetch_animeschedule_cover", side_effect=RuntimeError("animeschedule failed")), mock.patch.object(
APP, "fetch_anidb_cover_by_aid", side_effect=RuntimeError("stale aid")
), mock.patch.object(
APP, "fetch_anidb_cover", return_value={"aid": "67890", "title": "AniDB Fallback Show", "url": "https://example.com/cover.jpg"}
) as title_lookup, mock.patch.object(APP, "cache_thumbnail_image", return_value="show-thumb-3.jpg"):
result = APP.WATCHLIST.ensure_thumbnail("show-thumb-3")
title_lookup.assert_called_once_with("AniDB Fallback Show")
self.assertEqual(result, APP.THUMBNAIL_ROOT / "show-thumb-3.jpg")
refreshed = APP.WATCHLIST.get("show-thumb-3")
self.assertEqual(refreshed["anidb_aid"], "67890")
class WatchlistCompletionTests(unittest.TestCase):
def setUp(self):