From 5cbdcbbfe5d11e01ea9b26dc9bdaf0ce6dc27cee Mon Sep 17 00:00:00 2001 From: Skowll Date: Thu, 14 May 2026 18:46:54 +0200 Subject: [PATCH 1/3] add thread_id for telegram notification --- core/automation_engine.py | 3 +- webui/static/stats-automations.js | 10 +- webui/static/wishlist-tools.js | 151 ++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 3 deletions(-) diff --git a/core/automation_engine.py b/core/automation_engine.py index d19cbb58..82329279 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -997,6 +997,7 @@ class AutomationEngine: """Send message via Telegram Bot API.""" bot_token = config.get('bot_token', '').strip() chat_id = config.get('chat_id', '').strip() + thread_id = config.get('thread_id', '').strip() if not bot_token or not chat_id: raise ValueError("Bot token and chat ID are required for Telegram") @@ -1007,7 +1008,7 @@ class AutomationEngine: resp = requests.post( f'https://api.telegram.org/bot{bot_token}/sendMessage', - json={"chat_id": chat_id, "text": message, "parse_mode": "HTML"}, + json={"chat_id": chat_id, "message_thread_id": thread_id, "text": message, "parse_mode": "HTML"}, timeout=10, ) data = resp.json() if resp.status_code == 200 else {} diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 7df4ab4b..72f5fe5f 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -4890,7 +4890,7 @@ function _promptNotifyConfig(groupName) { if (type === 'discord_webhook') { fieldsDiv.innerHTML = ''; } else if (type === 'telegram') { - fieldsDiv.innerHTML = ''; + fieldsDiv.innerHTML = ''; } else if (type === 'pushbullet') { fieldsDiv.innerHTML = ''; } else { @@ -4911,7 +4911,7 @@ function _promptNotifyConfig(groupName) { if (type === 'discord_webhook') { config = { webhook_url: (overlay.querySelector('#deploy-notify-url')?.value || '').trim() }; } else if (type === 'telegram') { - config = { bot_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim(), chat_id: (overlay.querySelector('#deploy-notify-chat')?.value || '').trim() }; + config = { bot_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim(), chat_id: (overlay.querySelector('#deploy-notify-chat')?.value || '').trim(), thread_id: (overlay.querySelector('#deploy-notify-thread')?.value || '').trim() }; } else if (type === 'pushbullet') { config = { access_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim() }; } else { @@ -6004,6 +6004,7 @@ function _renderBlockConfigFields(slotKey, blockType, config) { if (blockType === 'telegram') { const botToken = _escAttr(config.bot_token || ''); const chatId = _escAttr(config.chat_id || ''); + const threadID = _escAttr(config.thread_id || ''); return `
@@ -6012,6 +6013,10 @@ function _renderBlockConfigFields(slotKey, blockType, config) {
+
+ + +
@@ -6291,6 +6296,7 @@ function _readPlacedConfig(slotKey) { return { bot_token: document.getElementById('cfg-' + slotKey + '-bot_token')?.value?.trim() || '', chat_id: document.getElementById('cfg-' + slotKey + '-chat_id')?.value?.trim() || '', + thread_id: document.getElementById('cfg-' + slotKey + '-thread_id')?.value?.trim() || '', message: document.getElementById('cfg-' + slotKey + '-message')?.value || '', }; } diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index f59ea4d0..c3a4d7fd 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -3094,6 +3094,150 @@ function openLibraryHistoryModal() { } } +// ────────────────────────────────────────────────────────────────────── +// Quarantine tab — rendered inside the Library History modal as a third +// tab next to Downloads + Server Imports. Reuses the existing list + +// pagination chrome; provides per-row Approve / Recover / Delete actions. +// ────────────────────────────────────────────────────────────────────── + +async function loadQuarantineList() { + const list = document.getElementById('library-history-list'); + const pagination = document.getElementById('library-history-pagination'); + const sourceBar = document.getElementById('history-source-bar'); + if (!list) return; + list.innerHTML = '
Loading...
'; + if (pagination) pagination.innerHTML = ''; + if (sourceBar) sourceBar.style.display = 'none'; + + try { + const resp = await fetch('/api/quarantine/list'); + const data = await resp.json(); + const entries = data.entries || []; + const countEl = document.getElementById('history-quarantine-count'); + if (countEl) countEl.textContent = entries.length; + + if (!data.success) { + list.innerHTML = `
Error: ${escapeHtml(data.error || 'Failed to load')}
`; + return; + } + if (entries.length === 0) { + list.innerHTML = '
🛡️

No quarantined files. Nice and clean.
'; + return; + } + list.innerHTML = entries.map(renderQuarantineEntry).join(''); + } catch (err) { + console.error('Error loading quarantine entries:', err); + list.innerHTML = '
Error loading quarantine
'; + } +} + +function renderQuarantineEntry(entry) { + const triggerLabels = { integrity: 'Duration / Integrity', acoustid: 'AcoustID Mismatch', bit_depth: 'Bit Depth Filter', unknown: 'Unknown' }; + const triggerColors = { integrity: '#facc15', acoustid: '#ef5350', bit_depth: '#fb923c', unknown: '#888' }; + const triggerLabel = triggerLabels[entry.trigger] || entry.trigger || 'Unknown'; + const triggerColor = triggerColors[entry.trigger] || '#888'; + + const id = escapeHtml(entry.id); + const approveLabel = entry.has_full_context ? 'Approve' : 'Recover'; + const approveTitle = entry.has_full_context + ? 'Re-run post-processing with only the failing check skipped' + : 'Legacy entry — move to Staging, finish via Import flow'; + const approveCall = entry.has_full_context + ? `approveQuarantineEntry('${id}')` + : `recoverQuarantineEntry('${id}')`; + + const meta = [entry.expected_artist, entry.original_filename].filter(Boolean).join(' — '); + const triggerBadge = `${escapeHtml(triggerLabel)}`; + const reasonDetail = `
Reason: ${escapeHtml(entry.reason || 'Unknown')}
`; + + return `
+
🛡️
+
+
+
+
${escapeHtml(entry.expected_track || entry.original_filename || 'Unknown')}
+ +
+
${triggerBadge}
+
${formatHistoryTime(entry.timestamp)}
+ + + +
+
+ ${reasonDetail} +
+
+
`; +} + +async function approveQuarantineEntry(entryId) { + const ok = await showConfirmDialog({ + title: 'Approve Quarantined File', + message: 'Re-run post-processing for this file with only the failing check skipped. The file will be tagged, lyrics generated, and moved into your library. Other quality gates (AcoustID + bit-depth) still run.', + confirmText: 'Approve & Import', + cancelText: 'Cancel', + }); + if (!ok) return; + try { + const r = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/approve`, { method: 'POST' }); + const data = await r.json(); + if (!data.success) { + showToast(`Approve failed: ${data.error}`, 'error'); + } else { + showToast(`Approved — skipped ${data.trigger_bypassed} check, re-running pipeline.`, 'success'); + } + } catch (err) { + showToast(`Approve failed: ${err.message}`, 'error'); + } + loadQuarantineList(); +} + +async function recoverQuarantineEntry(entryId) { + const ok = await showConfirmDialog({ + title: 'Recover To Staging', + message: 'Legacy entry — no embedded context. The file will be moved to your Staging folder so you can finish via the Import page (manual match).', + confirmText: 'Move To Staging', + cancelText: 'Cancel', + }); + if (!ok) return; + try { + const r = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/recover`, { method: 'POST' }); + const data = await r.json(); + if (!data.success) { + showToast(`Recover failed: ${data.error}`, 'error'); + } else { + showToast('Moved to Staging — finish via the Import page.', 'success'); + } + } catch (err) { + showToast(`Recover failed: ${err.message}`, 'error'); + } + loadQuarantineList(); +} + +async function deleteQuarantineEntry(entryId) { + const ok = await showConfirmDialog({ + title: 'Delete Quarantined File', + message: 'This permanently removes the file and its metadata sidecar. Cannot be undone.', + confirmText: 'Delete', + cancelText: 'Cancel', + destructive: true, + }); + if (!ok) return; + try { + const r = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}`, { method: 'DELETE' }); + const data = await r.json(); + if (!data.success) { + showToast(`Delete failed: ${data.error}`, 'error'); + } else { + showToast('Quarantined file deleted.', 'success'); + } + } catch (err) { + showToast(`Delete failed: ${err.message}`, 'error'); + } + loadQuarantineList(); +} + function closeLibraryHistoryModal() { const overlay = document.getElementById('library-history-overlay'); if (overlay) overlay.classList.add('hidden'); @@ -3110,6 +3254,12 @@ function switchHistoryTab(tab) { async function loadLibraryHistory() { const { tab, page, limit } = _libraryHistoryState; + if (tab === 'quarantine') { + // Refresh the count for the other two tabs in the background so + // the badge stays accurate when the user switches over. + loadQuarantineList(); + return; + } const list = document.getElementById('library-history-list'); const pagination = document.getElementById('library-history-pagination'); if (!list) return; @@ -6199,6 +6349,7 @@ const TOOL_HELP_CONTENT = { From d47e4ccc0d16c31833635d79580896d44e5332ac Mon Sep 17 00:00:00 2001 From: Skowll Date: Sun, 24 May 2026 14:13:57 +0200 Subject: [PATCH 2/3] edit telegram_id after initial pr --- core/automation_engine.py | 12 ++++++++++-- webui/static/stats-automations.js | 6 +++--- webui/static/wishlist-tools.js | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/automation_engine.py b/core/automation_engine.py index 82329279..424c445e 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -997,7 +997,8 @@ class AutomationEngine: """Send message via Telegram Bot API.""" bot_token = config.get('bot_token', '').strip() chat_id = config.get('chat_id', '').strip() - thread_id = config.get('thread_id', '').strip() + thread_id = str(config.get('thread_id', '')).strip() + if not bot_token or not chat_id: raise ValueError("Bot token and chat ID are required for Telegram") @@ -1006,9 +1007,16 @@ class AutomationEngine: for key, value in variables.items(): message = message.replace('{' + key + '}', value) + payload = {"chat_id": chat_id, "text": message, "parse_mode": "HTML"} + if thread_id: + try: + payload["message_thread_id"] = int(thread_id) + except ValueError: + pass # invalid — fall back to main chat + resp = requests.post( f'https://api.telegram.org/bot{bot_token}/sendMessage', - json={"chat_id": chat_id, "message_thread_id": thread_id, "text": message, "parse_mode": "HTML"}, + json=payload, timeout=10, ) data = resp.json() if resp.status_code == 200 else {} diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 947a8c16..56a7e06a 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -4315,7 +4315,7 @@ function _promptNotifyConfig(groupName) { if (type === 'discord_webhook') { config = { webhook_url: (overlay.querySelector('#deploy-notify-url')?.value || '').trim() }; } else if (type === 'telegram') { - config = { bot_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim(), chat_id: (overlay.querySelector('#deploy-notify-chat')?.value || '').trim(), thread_id: (overlay.querySelector('#deploy-notify-thread')?.value || '').trim() }; + config = { bot_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim(), chat_id: (overlay.querySelector('#deploy-notify-chat')?.value || '').trim(), thread_id: (overlay.querySelector('#deploy-notify-thread')?.value || '').trim() }; } else if (type === 'pushbullet') { config = { access_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim() }; } else { @@ -5447,7 +5447,7 @@ function _renderBlockConfigFields(slotKey, blockType, config) { if (blockType === 'telegram') { const botToken = _escAttr(config.bot_token || ''); const chatId = _escAttr(config.chat_id || ''); - const threadID = _escAttr(config.thread_id || ''); + const threadId = _escAttr(config.thread_id || ''); return `
@@ -5458,7 +5458,7 @@ function _renderBlockConfigFields(slotKey, blockType, config) {
- +
diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index 45e46fb4..df62111c 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -6530,7 +6530,7 @@ const TOOL_HELP_CONTENT = {
  • Bot Token: Your Telegram bot token (from @BotFather)
  • Chat ID: The chat/group ID to send messages to
  • -
  • Thread ID: Thread ID
  • +
  • Thread ID: Optional — only set if your group uses Telegram topics. Leave blank for the main chat.
  • Message Template: Custom message with variable placeholders
From 4ae65f4de95c857d8d8467785b8649cf23c49944 Mon Sep 17 00:00:00 2001 From: Skowll Date: Sun, 24 May 2026 14:52:03 +0200 Subject: [PATCH 3/3] remove unecessary str --- core/automation_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/automation_engine.py b/core/automation_engine.py index 424c445e..84b8ca51 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -997,7 +997,7 @@ class AutomationEngine: """Send message via Telegram Bot API.""" bot_token = config.get('bot_token', '').strip() chat_id = config.get('chat_id', '').strip() - thread_id = str(config.get('thread_id', '')).strip() + thread_id = config.get('thread_id', '').strip() if not bot_token or not chat_id: raise ValueError("Bot token and chat ID are required for Telegram")