Add Discord webhook refresh notifications

This commit is contained in:
Dymas
2026-05-21 18:56:49 +02:00
parent a95b4c343f
commit 9648b4f6e4
9 changed files with 638 additions and 54 deletions
+122
View File
@@ -1090,6 +1090,8 @@ class ConfigSnapshotTests(unittest.TestCase):
"watchlist_auto_refresh_enabled": False,
"watchlist_auto_refresh_minutes": 60,
"watchlist_refresh_delay_seconds": 5,
"discord_webhook_url": "",
"discord_webhook_events": [],
}
try:
snapshot = APP.get_config_snapshot()
@@ -1103,6 +1105,8 @@ 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.assertEqual(config["discord_webhook_url"], "")
self.assertEqual(config["discord_webhook_events"], [])
def test_normalize_config_parses_watchlist_schedule_fields(self):
config = APP.normalize_config(
@@ -1116,6 +1120,44 @@ class ConfigSnapshotTests(unittest.TestCase):
self.assertEqual(config["watchlist_auto_refresh_minutes"], 15)
self.assertEqual(config["watchlist_refresh_delay_seconds"], 7)
def test_normalize_config_filters_discord_webhook_events(self):
config = APP.normalize_config(
{
"discord_webhook_url": " https://discord.example/webhook ",
"discord_webhook_events": [
"watchlist_refresh_new_episodes",
"runtime_error",
"runtime_error",
"nope",
],
}
)
self.assertEqual(config["discord_webhook_url"], "https://discord.example/webhook")
self.assertEqual(
config["discord_webhook_events"],
["watchlist_refresh_new_episodes", "runtime_error"],
)
def test_test_discord_webhook_config_requires_url(self):
with self.assertRaises(ValueError):
APP.test_discord_webhook_config({"discord_webhook_url": ""})
def test_test_discord_webhook_config_sends_test_event(self):
with mock.patch.object(APP, "send_discord_webhook_event") as send_webhook:
result = APP.test_discord_webhook_config(
{
"discord_webhook_url": "https://discord.example/webhook",
"discord_webhook_events": [],
}
)
self.assertEqual(result["message"], "Discord webhook test notification sent.")
send_webhook.assert_called_once()
config, event_name = send_webhook.call_args.args[:2]
self.assertEqual(config["discord_webhook_url"], "https://discord.example/webhook")
self.assertEqual(event_name, "test")
self.assertTrue(send_webhook.call_args.kwargs["force"])
class WatchlistAutoRefreshSchedulerTests(unittest.TestCase):
def test_tick_waits_then_triggers_refresh(self):
@@ -1153,6 +1195,57 @@ class WatchlistAutoRefreshSchedulerTests(unittest.TestCase):
self.assertEqual(wait_mock.call_count, 2)
wait_mock.assert_called_with(5)
def test_refresh_job_sends_new_episode_and_failure_notifications(self):
class FakeStore:
def all_show_ids(self):
return ["show-a", "show-b"]
def refresh(self, show_id, include_thumbnail=True):
if show_id == "show-a":
return {
"show_id": "show-a",
"title": "Show A",
"status": "updated",
"had_new_episodes": True,
"sub_count": 12,
"dub_count": 4,
"sub_delta": 1,
"dub_delta": 0,
"expected_count": 12,
"airing_status": "Finished",
"thumbnail_path": None,
}
return {
"show_id": "show-b",
"title": "Show B",
"status": "error",
"status_message": "Could not refresh episodes: boom",
"had_new_episodes": False,
}
service = APP.WatchlistRefreshJobs(
FakeStore(),
config_getter=lambda: {
"discord_webhook_url": "https://discord.example/webhook",
"discord_webhook_events": [
"watchlist_refresh_new_episodes",
"watchlist_refresh_failed",
],
},
start_worker=False,
)
job = service.start()["job"]
job = service._next_pending()
with mock.patch.object(queue_jobs, "send_discord_webhook_event") as send_webhook:
service._run_job(job)
self.assertEqual(send_webhook.call_count, 2)
self.assertEqual(send_webhook.call_args_list[0].args[1], "watchlist_refresh_new_episodes")
self.assertEqual(send_webhook.call_args_list[0].args[2]["items"][0]["title"], "Show A")
self.assertEqual(send_webhook.call_args_list[1].args[1], "watchlist_refresh_failed")
self.assertEqual(send_webhook.call_args_list[1].args[2]["items"][0]["title"], "Show B")
def test_tick_resets_timer_when_schedule_changes(self):
config = {
"watchlist_auto_refresh_enabled": True,
@@ -1238,6 +1331,8 @@ class StartupBehaviorTests(unittest.TestCase):
"watchlist_auto_refresh_enabled": False,
"watchlist_auto_refresh_minutes": 60,
"watchlist_refresh_delay_seconds": 5,
"discord_webhook_url": "",
"discord_webhook_events": [],
}
APP.WATCHLIST_AUTO_REFRESH = mock.Mock()
saved = APP.save_runtime_config(
@@ -1248,6 +1343,8 @@ class StartupBehaviorTests(unittest.TestCase):
"watchlist_auto_refresh_enabled": True,
"watchlist_auto_refresh_minutes": 15,
"watchlist_refresh_delay_seconds": 8,
"discord_webhook_url": "https://discord.example/webhook",
"discord_webhook_events": ["runtime_error"],
}
)
self.assertEqual(saved["mode"], "dub")
@@ -1256,6 +1353,8 @@ 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.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()
finally:
APP.CONFIG = original
@@ -1299,6 +1398,29 @@ class HandlerRouteTests(unittest.TestCase):
self.assertEqual(handler.json_payload["watchlist_auto_refresh_minutes"], 25)
self.assertEqual(handler.json_payload["watchlist_refresh_delay_seconds"], 9)
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"]}'
handler = DummyHandler("/api/config", body=body, content_length=len(body))
APP.Handler.do_POST(handler)
self.assertEqual(handler.json_status, HTTPStatus.OK)
self.assertEqual(handler.json_payload["discord_webhook_url"], "https://discord.example/webhook")
self.assertEqual(
handler.json_payload["discord_webhook_events"],
["runtime_error", "watchlist_refresh_failed"],
)
def test_config_webhook_test_route_uses_unsaved_payload(self):
body = b'{"discord_webhook_url":"https://discord.example/webhook","discord_webhook_events":["runtime_error"]}'
handler = DummyHandler("/api/config/webhook/test", body=body, content_length=len(body))
test_webhook = mock.Mock(return_value={"message": "ok"})
handler.handler_context = mock.Mock(test_discord_webhook_config=test_webhook)
APP.Handler.do_POST(handler)
self.assertEqual(handler.json_status, HTTPStatus.OK)
self.assertEqual(handler.json_payload["message"], "ok")
test_webhook.assert_called_once_with(
{"discord_webhook_url": "https://discord.example/webhook", "discord_webhook_events": ["runtime_error"]}
)
def test_body_json_rejects_oversized_request(self):
handler = DummyHandler("/api/watchlist/upload-thumbnail", body=b"{}", content_length=APP.MAX_JSON_BODY_BYTES + 1)
with self.assertRaises(APP.HttpError) as ctx: