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
+125 -29
View File
@@ -2,9 +2,19 @@
"""Config page template for ani-cli-web."""
from app_support import DISCORD_WEBHOOK_EVENT_CHOICES, DISCORD_WEBHOOK_EVENT_LABELS
from template_helpers import BROWSER_API_HELPER, render_page_links, render_sidebar_brand
WEBHOOK_EVENT_OPTIONS = "\n".join(
(
f' <label class="check-item"><input class="webhook-event" type="checkbox" value="{event_name}"> '
f"<span>{DISCORD_WEBHOOK_EVENT_LABELS[event_name]}</span></label>"
)
for event_name in DISCORD_WEBHOOK_EVENT_CHOICES
)
CONFIG_HTML = r"""<!doctype html>
<html lang="en">
<head>
@@ -49,7 +59,7 @@ CONFIG_HTML = r"""<!doctype html>
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 24%);
opacity: 0.5;
}
button, input, select {
button, input, select, textarea {
font: inherit;
}
button {
@@ -74,7 +84,7 @@ CONFIG_HTML = r"""<!doctype html>
font-weight: 700;
box-shadow: 0 16px 32px rgba(123, 92, 255, 0.26);
}
input, select {
input, select, textarea {
width: 100%;
min-height: 42px;
border-radius: 14px;
@@ -85,7 +95,12 @@ CONFIG_HTML = r"""<!doctype html>
outline: none;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
input:focus, select:focus {
textarea {
min-height: 108px;
padding: 12px 13px;
resize: vertical;
}
input:focus, select:focus, textarea:focus {
border-color: var(--focus);
box-shadow: 0 0 0 3px rgba(178, 150, 255, 0.12);
}
@@ -233,6 +248,38 @@ CONFIG_HTML = r"""<!doctype html>
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.check-item {
display: flex;
align-items: center;
gap: 10px;
min-height: 48px;
border: 1px solid var(--line);
border-radius: 16px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.03);
color: var(--text);
text-transform: none;
letter-spacing: 0;
font-size: 13px;
font-weight: 600;
}
.check-item input {
width: 18px;
min-height: 18px;
margin: 0;
accent-color: var(--accent);
}
.inline-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.settings-main, .deps {
width: min(760px, 100%);
}
@@ -242,7 +289,7 @@ CONFIG_HTML = r"""<!doctype html>
.toolbar, .row { align-items: stretch; flex-direction: column; }
}
@media (max-width: 640px) {
.form-grid {
.form-grid, .check-grid {
grid-template-columns: 1fr;
}
}
@@ -330,6 +377,28 @@ CONFIG_HTML = r"""<!doctype html>
<div class="field-hint">When enabled, the server periodically starts the existing background “Refresh All” job using this interval. Bulk refreshes now wait this many seconds between shows to avoid flooding upstream sources.</div>
</section>
<section class="settings settings-main">
<div class="toolbar">
<div>
<h2>Discord notifications</h2>
<p class="muted">Send webhook notifications for refresh discoveries, refresh problems, and unexpected runtime errors.</p>
</div>
<button id="testWebhookBtn" type="button">Test webhook</button>
</div>
<div class="stack">
<label>Discord webhook URL
<textarea id="discordWebhookUrl" placeholder="https://discord.com/api/webhooks/..."></textarea>
</label>
<div class="field-hint">The new-episodes notification includes one card per anime, with local cached thumbnails attached to the webhook when available.</div>
</div>
<div class="stack">
<label>Notify for</label>
<div class="check-grid">
""" + WEBHOOK_EVENT_OPTIONS + r"""
</div>
</div>
</section>
<section class="deps" id="deps"></section>
</main>
</div>
@@ -342,7 +411,9 @@ CONFIG_HTML = r"""<!doctype html>
download_dir: "",
watchlist_auto_refresh_enabled: false,
watchlist_auto_refresh_minutes: 60,
watchlist_refresh_delay_seconds: 5
watchlist_refresh_delay_seconds: 5,
discord_webhook_url: "",
discord_webhook_events: []
}
};
@@ -353,42 +424,66 @@ CONFIG_HTML = r"""<!doctype html>
$("notice").textContent = text || "";
}
function selectedWebhookEvents() {
return [...document.querySelectorAll(".webhook-event:checked")].map((input) => input.value);
}
function formConfigPayload() {
return {
download_dir: $("downloadDir").value,
mode: $("configMode").value,
quality: $("configQuality").value,
watchlist_auto_refresh_enabled: $("watchlistAutoRefreshEnabled").value === "true",
watchlist_auto_refresh_minutes: $("watchlistAutoRefreshMinutes").value,
watchlist_refresh_delay_seconds: $("watchlistRefreshDelaySeconds").value,
discord_webhook_url: $("discordWebhookUrl").value,
discord_webhook_events: selectedWebhookEvents()
};
}
function applyConfig(data) {
state.config = data;
$("downloadDir").value = data.download_dir;
$("configMode").value = data.mode;
$("configQuality").value = data.quality;
$("watchlistAutoRefreshEnabled").value = String(Boolean(data.watchlist_auto_refresh_enabled));
$("watchlistAutoRefreshMinutes").value = data.watchlist_auto_refresh_minutes;
$("watchlistRefreshDelaySeconds").value = data.watchlist_refresh_delay_seconds;
$("discordWebhookUrl").value = data.discord_webhook_url || "";
const selected = new Set(data.discord_webhook_events || []);
for (const input of document.querySelectorAll(".webhook-event")) {
input.checked = selected.has(input.value);
}
}
async function loadConfig() {
state.config = await api("/api/config");
$("downloadDir").value = state.config.download_dir;
$("configMode").value = state.config.mode;
$("configQuality").value = state.config.quality;
$("watchlistAutoRefreshEnabled").value = String(Boolean(state.config.watchlist_auto_refresh_enabled));
$("watchlistAutoRefreshMinutes").value = state.config.watchlist_auto_refresh_minutes;
$("watchlistRefreshDelaySeconds").value = state.config.watchlist_refresh_delay_seconds;
applyConfig(await api("/api/config"));
}
async function saveConfig() {
try {
const data = await api("/api/config", {
applyConfig(await api("/api/config", {
method: "POST",
body: JSON.stringify({
download_dir: $("downloadDir").value,
mode: $("configMode").value,
quality: $("configQuality").value,
watchlist_auto_refresh_enabled: $("watchlistAutoRefreshEnabled").value === "true",
watchlist_auto_refresh_minutes: $("watchlistAutoRefreshMinutes").value,
watchlist_refresh_delay_seconds: $("watchlistRefreshDelaySeconds").value
})
});
state.config = data;
$("downloadDir").value = data.download_dir;
$("configMode").value = data.mode;
$("configQuality").value = data.quality;
$("watchlistAutoRefreshEnabled").value = String(Boolean(data.watchlist_auto_refresh_enabled));
$("watchlistAutoRefreshMinutes").value = data.watchlist_auto_refresh_minutes;
$("watchlistRefreshDelaySeconds").value = data.watchlist_refresh_delay_seconds;
body: JSON.stringify(formConfigPayload())
}));
setNotice("Defaults saved.");
} catch (error) {
setNotice(error.message);
}
}
async function testWebhook() {
try {
const data = await api("/api/config/webhook/test", {
method: "POST",
body: JSON.stringify(formConfigPayload())
});
setNotice(data.message || "Discord webhook test notification sent.");
} catch (error) {
setNotice(error.message);
}
}
async function loadDeps() {
const data = await api("/api/dependencies");
const el = $("deps");
@@ -410,6 +505,7 @@ CONFIG_HTML = r"""<!doctype html>
}
$("saveConfigBtn").addEventListener("click", saveConfig);
$("testWebhookBtn").addEventListener("click", testWebhook);
(async function init() {
try {