Add watchlist auto-download settings
This commit is contained in:
+126
-15
@@ -975,19 +975,19 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
def test_queue_refresh_marks_item_queued_without_sync_refresh(self):
|
||||
self.seed_watchlist_item(show_id="show-11", title="Refresh Show", category="watching", status="updated")
|
||||
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id: APP.WATCHLIST.get(show_id)) as schedule_refresh, mock.patch.object(
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)) as schedule_refresh, mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("queue_refresh should not refresh synchronously")
|
||||
):
|
||||
item = APP.WATCHLIST.queue_refresh("show-11", status_message="Queued from test.")
|
||||
|
||||
schedule_refresh.assert_called_once_with("show-11")
|
||||
schedule_refresh.assert_called_once_with("show-11", source="manual")
|
||||
self.assertEqual(item["status"], "queued")
|
||||
self.assertEqual(item["status_message"], "Queued from test.")
|
||||
|
||||
def test_mark_downloaded_moves_existing_category_to_finished_and_sets_download_flag(self):
|
||||
self.seed_watchlist_item(show_id="show-9", title="Queue Show", category="planned", downloaded=False)
|
||||
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id: APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("mark_downloaded should not refresh synchronously")
|
||||
):
|
||||
item = APP.WATCHLIST.mark_downloaded("show-9", title="Queue Show")
|
||||
@@ -998,7 +998,7 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
self.assertEqual(item["status"], "queued")
|
||||
|
||||
def test_mark_downloaded_creates_missing_show_in_finished_category(self):
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id: APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("mark_downloaded should not refresh synchronously")
|
||||
):
|
||||
item = APP.WATCHLIST.mark_downloaded("show-10", title="Downloaded Show")
|
||||
@@ -1011,7 +1011,7 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
def test_mark_downloaded_reconciles_unique_title_match_to_new_show_id(self):
|
||||
self.seed_watchlist_item(show_id="legacy-show-9", title="Queue Show", category="watching", downloaded=False)
|
||||
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id: APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)), mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("mark_downloaded should not refresh synchronously")
|
||||
):
|
||||
item = APP.WATCHLIST.mark_downloaded("resolved-show-9", title="Queue Show")
|
||||
@@ -1025,12 +1025,12 @@ class WatchlistCompletionTests(unittest.TestCase):
|
||||
self.assertEqual(APP.WATCHLIST.all_show_ids(), ["resolved-show-9"])
|
||||
|
||||
def test_add_queues_refresh_without_sync_refresh(self):
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id: APP.WATCHLIST.get(show_id)) as schedule_refresh, mock.patch.object(
|
||||
with mock.patch.object(APP.WATCHLIST, "schedule_refresh", side_effect=lambda show_id, source="manual": APP.WATCHLIST.get(show_id)) as schedule_refresh, mock.patch.object(
|
||||
APP.WATCHLIST, "refresh", side_effect=AssertionError("add should not refresh synchronously")
|
||||
):
|
||||
result = APP.WATCHLIST.add({"show_id": "show-22", "title": "Queued Show", "category": "watching"})
|
||||
|
||||
schedule_refresh.assert_called_once_with("show-22")
|
||||
schedule_refresh.assert_called_once_with("show-22", source="initial")
|
||||
self.assertTrue(result["created"])
|
||||
self.assertEqual(result["item"]["status"], "queued")
|
||||
self.assertIn("Refresh queued", result["message"])
|
||||
@@ -1196,7 +1196,7 @@ class WatchlistQueuedRefreshRecoveryTests(unittest.TestCase):
|
||||
|
||||
restored = APP.WatchlistStore(start_worker=False)
|
||||
|
||||
self.assertEqual(restored.refresh_pending, ["show-a", "show-b"])
|
||||
self.assertEqual(restored.refresh_pending, [("show-a", "manual"), ("show-b", "manual")])
|
||||
self.assertEqual(restored.refresh_pending_set, {"show-a", "show-b"})
|
||||
|
||||
|
||||
@@ -1226,7 +1226,7 @@ class WatchlistRefreshQueueTests(unittest.TestCase):
|
||||
store = object.__new__(APP.WatchlistStore)
|
||||
store.lock = threading.RLock()
|
||||
store.thumbnail_events = {}
|
||||
store.refresh_pending = ["show-2"]
|
||||
store.refresh_pending = [("show-2", "manual")]
|
||||
store.refresh_pending_set = {"show-2"}
|
||||
store.refresh_inflight_set = set()
|
||||
store.refresh_wakeup = threading.Event()
|
||||
@@ -1235,15 +1235,15 @@ class WatchlistRefreshQueueTests(unittest.TestCase):
|
||||
store.worker_enabled = False
|
||||
calls = []
|
||||
|
||||
def fake_refresh(show_id):
|
||||
calls.append(show_id)
|
||||
def fake_refresh(show_id, refresh_source="manual"):
|
||||
calls.append((show_id, refresh_source))
|
||||
store.refresh_stop_event.set()
|
||||
|
||||
store.refresh = fake_refresh
|
||||
|
||||
APP.WatchlistStore._refresh_loop(store)
|
||||
|
||||
self.assertEqual(calls, ["show-2"])
|
||||
self.assertEqual(calls, [("show-2", "manual")])
|
||||
self.assertEqual(store.refresh_inflight_set, set())
|
||||
self.assertEqual(store.refresh_pending, [])
|
||||
self.assertEqual(store.refresh_pending_set, set())
|
||||
@@ -1253,7 +1253,7 @@ class WatchlistRefreshQueueTests(unittest.TestCase):
|
||||
store.lock = threading.RLock()
|
||||
thumbnail_event = threading.Event()
|
||||
store.thumbnail_events = {"show-3": thumbnail_event}
|
||||
store.refresh_pending = ["show-3", "show-4"]
|
||||
store.refresh_pending = [("show-3", "manual"), ("show-4", "manual")]
|
||||
store.refresh_pending_set = {"show-3", "show-4"}
|
||||
store.refresh_inflight_set = {"show-3"}
|
||||
store._connect = APP.WATCHLIST._connect
|
||||
@@ -1277,7 +1277,7 @@ class WatchlistRefreshQueueTests(unittest.TestCase):
|
||||
result = APP.WatchlistStore.remove(store, "show-3")
|
||||
|
||||
self.assertEqual(result["item"]["show_id"], "show-3")
|
||||
self.assertEqual(store.refresh_pending, ["show-4"])
|
||||
self.assertEqual(store.refresh_pending, [("show-4", "manual")])
|
||||
self.assertEqual(store.refresh_pending_set, {"show-4"})
|
||||
self.assertEqual(store.refresh_inflight_set, set())
|
||||
self.assertTrue(thumbnail_event.is_set())
|
||||
@@ -1294,6 +1294,9 @@ class ConfigSnapshotTests(unittest.TestCase):
|
||||
"watchlist_auto_refresh_enabled": False,
|
||||
"watchlist_auto_refresh_minutes": 60,
|
||||
"watchlist_refresh_delay_seconds": 5,
|
||||
"auto_download_enabled": False,
|
||||
"auto_download_mode": "dub",
|
||||
"auto_download_quality": "best",
|
||||
"discord_webhook_url": "",
|
||||
"discord_webhook_events": [],
|
||||
}
|
||||
@@ -1309,6 +1312,9 @@ class ConfigSnapshotTests(unittest.TestCase):
|
||||
self.assertFalse(config["watchlist_auto_refresh_enabled"])
|
||||
self.assertEqual(config["watchlist_auto_refresh_minutes"], 60)
|
||||
self.assertEqual(config["watchlist_refresh_delay_seconds"], 5)
|
||||
self.assertFalse(config["auto_download_enabled"])
|
||||
self.assertEqual(config["auto_download_mode"], "dub")
|
||||
self.assertEqual(config["auto_download_quality"], "best")
|
||||
self.assertEqual(config["discord_webhook_url"], "")
|
||||
self.assertEqual(config["discord_webhook_events"], [])
|
||||
|
||||
@@ -1318,11 +1324,17 @@ class ConfigSnapshotTests(unittest.TestCase):
|
||||
"watchlist_auto_refresh_enabled": "true",
|
||||
"watchlist_auto_refresh_minutes": "15",
|
||||
"watchlist_refresh_delay_seconds": "7",
|
||||
"auto_download_enabled": "true",
|
||||
"auto_download_mode": "sub",
|
||||
"auto_download_quality": "720p",
|
||||
}
|
||||
)
|
||||
self.assertTrue(config["watchlist_auto_refresh_enabled"])
|
||||
self.assertEqual(config["watchlist_auto_refresh_minutes"], 15)
|
||||
self.assertEqual(config["watchlist_refresh_delay_seconds"], 7)
|
||||
self.assertTrue(config["auto_download_enabled"])
|
||||
self.assertEqual(config["auto_download_mode"], "sub")
|
||||
self.assertEqual(config["auto_download_quality"], "720")
|
||||
|
||||
def test_normalize_config_filters_discord_webhook_events(self):
|
||||
config = APP.normalize_config(
|
||||
@@ -1475,6 +1487,78 @@ class WatchlistAutoRefreshSchedulerTests(unittest.TestCase):
|
||||
start.assert_not_called()
|
||||
|
||||
|
||||
class AutoDownloadQueueTests(unittest.TestCase):
|
||||
def test_queue_watchlist_auto_download_skips_initial_refresh(self):
|
||||
item = {
|
||||
"show_id": "show-1",
|
||||
"title": "Example Show",
|
||||
"category": "watching",
|
||||
"previous_last_checked": None,
|
||||
"had_new_episodes": True,
|
||||
}
|
||||
with mock.patch.object(
|
||||
APP,
|
||||
"ensure_runtime",
|
||||
return_value={"config": {"auto_download_enabled": True}, "download_queue": mock.Mock()},
|
||||
):
|
||||
result = APP.queue_watchlist_auto_download(item, refresh_source="initial")
|
||||
self.assertFalse(result["queued"])
|
||||
self.assertEqual(result["reason"], "source:initial")
|
||||
|
||||
def test_queue_watchlist_auto_download_queues_only_missing_new_episodes(self):
|
||||
item = {
|
||||
"show_id": "show-42",
|
||||
"title": "Queue Show",
|
||||
"category": "watching",
|
||||
"previous_last_checked": "2026-05-25T10:00:00+00:00",
|
||||
"had_new_episodes": True,
|
||||
"previous_dub_count": 10,
|
||||
"previous_sub_count": 10,
|
||||
"dub_episode_values": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
||||
"sub_episode_values": ["1", "2", "3"],
|
||||
"auto_download_mode": "dub",
|
||||
"auto_download_quality": "best",
|
||||
"auto_download_name": 'Queue Show: Alt/Name',
|
||||
}
|
||||
download_queue = mock.Mock()
|
||||
download_queue.covered_episodes.return_value = {"11"}
|
||||
download_queue.add.side_effect = [
|
||||
{"id": "job-12", "episodes": "12"},
|
||||
]
|
||||
runtime = {
|
||||
"config": {
|
||||
"auto_download_enabled": True,
|
||||
"auto_download_mode": "dub",
|
||||
"auto_download_quality": "best",
|
||||
"download_dir": "/tmp/downloads",
|
||||
},
|
||||
"download_queue": download_queue,
|
||||
}
|
||||
with mock.patch.object(APP, "ensure_runtime", return_value=runtime), mock.patch.object(
|
||||
APP,
|
||||
"search_anime",
|
||||
return_value=[{"id": "show-42", "title": "Queue Show", "query": "Queue Show", "index": 2}],
|
||||
):
|
||||
result = APP.queue_watchlist_auto_download(item, refresh_source="scheduled")
|
||||
|
||||
self.assertTrue(result["queued"])
|
||||
self.assertEqual(result["episodes"], ["12"])
|
||||
download_queue.add.assert_called_once_with(
|
||||
{
|
||||
"show_id": "show-42",
|
||||
"query": "Queue Show",
|
||||
"title": "Queue Show",
|
||||
"anime_name": "Queue Show- Alt-Name",
|
||||
"season": "1",
|
||||
"index": 2,
|
||||
"mode": "dub",
|
||||
"quality": "best",
|
||||
"episodes": "12",
|
||||
"download_dir": "/tmp/downloads",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class StartupBehaviorTests(unittest.TestCase):
|
||||
def test_main_does_not_eagerly_initialize_runtime(self):
|
||||
server = mock.Mock()
|
||||
@@ -1535,6 +1619,9 @@ class StartupBehaviorTests(unittest.TestCase):
|
||||
"watchlist_auto_refresh_enabled": False,
|
||||
"watchlist_auto_refresh_minutes": 60,
|
||||
"watchlist_refresh_delay_seconds": 5,
|
||||
"auto_download_enabled": False,
|
||||
"auto_download_mode": "dub",
|
||||
"auto_download_quality": "best",
|
||||
"discord_webhook_url": "",
|
||||
"discord_webhook_events": [],
|
||||
}
|
||||
@@ -1547,6 +1634,9 @@ class StartupBehaviorTests(unittest.TestCase):
|
||||
"watchlist_auto_refresh_enabled": True,
|
||||
"watchlist_auto_refresh_minutes": 15,
|
||||
"watchlist_refresh_delay_seconds": 8,
|
||||
"auto_download_enabled": True,
|
||||
"auto_download_mode": "sub",
|
||||
"auto_download_quality": "720",
|
||||
"discord_webhook_url": "https://discord.example/webhook",
|
||||
"discord_webhook_events": ["runtime_error"],
|
||||
}
|
||||
@@ -1557,6 +1647,9 @@ class StartupBehaviorTests(unittest.TestCase):
|
||||
self.assertTrue(APP.CONFIG["watchlist_auto_refresh_enabled"])
|
||||
self.assertEqual(APP.CONFIG["watchlist_auto_refresh_minutes"], 15)
|
||||
self.assertEqual(APP.CONFIG["watchlist_refresh_delay_seconds"], 8)
|
||||
self.assertTrue(APP.CONFIG["auto_download_enabled"])
|
||||
self.assertEqual(APP.CONFIG["auto_download_mode"], "sub")
|
||||
self.assertEqual(APP.CONFIG["auto_download_quality"], "720")
|
||||
self.assertEqual(APP.CONFIG["discord_webhook_url"], "https://discord.example/webhook")
|
||||
self.assertEqual(APP.CONFIG["discord_webhook_events"], ["runtime_error"])
|
||||
APP.WATCHLIST_AUTO_REFRESH.notify_config_changed.assert_called_once_with()
|
||||
@@ -1594,13 +1687,16 @@ class HandlerRouteTests(unittest.TestCase):
|
||||
APP.initialize_runtime(start_workers=False)
|
||||
|
||||
def test_config_post_accepts_watchlist_schedule_fields(self):
|
||||
body = b'{"download_dir":"/tmp/example","mode":"sub","quality":"best","watchlist_auto_refresh_enabled":true,"watchlist_auto_refresh_minutes":"25","watchlist_refresh_delay_seconds":"9"}'
|
||||
body = b'{"download_dir":"/tmp/example","mode":"sub","quality":"best","watchlist_auto_refresh_enabled":true,"watchlist_auto_refresh_minutes":"25","watchlist_refresh_delay_seconds":"9","auto_download_enabled":true,"auto_download_mode":"dub","auto_download_quality":"720"}'
|
||||
handler = DummyHandler("/api/config", body=body, content_length=len(body))
|
||||
APP.Handler.do_POST(handler)
|
||||
self.assertEqual(handler.json_status, HTTPStatus.OK)
|
||||
self.assertTrue(handler.json_payload["watchlist_auto_refresh_enabled"])
|
||||
self.assertEqual(handler.json_payload["watchlist_auto_refresh_minutes"], 25)
|
||||
self.assertEqual(handler.json_payload["watchlist_refresh_delay_seconds"], 9)
|
||||
self.assertTrue(handler.json_payload["auto_download_enabled"])
|
||||
self.assertEqual(handler.json_payload["auto_download_mode"], "dub")
|
||||
self.assertEqual(handler.json_payload["auto_download_quality"], "720")
|
||||
|
||||
def test_config_post_accepts_discord_webhook_fields(self):
|
||||
body = b'{"download_dir":"/tmp/example","mode":"sub","quality":"best","discord_webhook_url":"https://discord.example/webhook","discord_webhook_events":["runtime_error","watchlist_refresh_failed","bad"]}'
|
||||
@@ -1744,6 +1840,15 @@ class HandlerRouteTests(unittest.TestCase):
|
||||
self.assertEqual(handler.json_payload, payload)
|
||||
self.assertEqual(handler.json_status, HTTPStatus.CREATED)
|
||||
|
||||
def test_watchlist_auto_download_settings_post_returns_updated_item(self):
|
||||
body = b'{"show_id":"show-1","mode":"dub","quality":"best","name":"Example Name"}'
|
||||
handler = DummyHandler("/api/watchlist/update-auto-download", body=body, content_length=len(body))
|
||||
payload = {"message": "Saved auto-download settings for Show 1.", "item": {"show_id": "show-1"}}
|
||||
handler.handler_context = mock.Mock(update_watchlist_auto_download=mock.Mock(return_value=payload))
|
||||
APP.Handler.do_POST(handler)
|
||||
self.assertEqual(handler.json_payload, payload)
|
||||
self.assertEqual(handler.json_status, HTTPStatus.OK)
|
||||
|
||||
def test_generic_handler_exception_skips_traceback_when_debug_disabled(self):
|
||||
handler = DummyHandler("/api/config")
|
||||
with mock.patch.object(http_handler, "debug_enabled", return_value=False), mock.patch.object(
|
||||
@@ -1837,6 +1942,12 @@ class TemplateHelperTests(unittest.TestCase):
|
||||
self.assertIn('altTitlesPanel.classList.toggle("open")', APP.WATCHLIST_HTML)
|
||||
self.assertIn("AniDB English alternative titles were not available for this show.", APP.WATCHLIST_HTML)
|
||||
|
||||
def test_watchlist_page_renders_auto_download_editor(self):
|
||||
self.assertIn("Auto-download", APP.WATCHLIST_HTML)
|
||||
self.assertIn('api("/api/watchlist/update-auto-download"', APP.WATCHLIST_HTML)
|
||||
self.assertIn("Save auto-download", APP.WATCHLIST_HTML)
|
||||
self.assertIn("Manual add does not trigger it", APP.WATCHLIST_HTML)
|
||||
|
||||
def test_watchlist_thumbnail_warmup_skips_repeated_attempts(self):
|
||||
self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML)
|
||||
self.assertIn("function thumbnailWarmupToken(item)", APP.WATCHLIST_HTML)
|
||||
|
||||
Reference in New Issue
Block a user