add thread_id for telegram notification

This commit is contained in:
Skowll 2026-05-14 18:46:54 +02:00 committed by GitHub
parent 9865caa789
commit 5cbdcbbfe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 161 additions and 3 deletions

View file

@ -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 {}

View file

@ -4890,7 +4890,7 @@ function _promptNotifyConfig(groupName) {
if (type === 'discord_webhook') {
fieldsDiv.innerHTML = '<input id="deploy-notify-url" type="text" placeholder="Discord Webhook URL" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
} else if (type === 'telegram') {
fieldsDiv.innerHTML = '<input id="deploy-notify-token" type="text" placeholder="Bot Token" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;"><input id="deploy-notify-chat" type="text" placeholder="Chat ID" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
fieldsDiv.innerHTML = '<input id="deploy-notify-token" type="text" placeholder="Bot Token" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;"><input id="deploy-notify-chat" type="text" placeholder="Chat ID" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;"><input id="deploy-notify-thread" type="text" placeholder="Thread ID" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
} else if (type === 'pushbullet') {
fieldsDiv.innerHTML = '<input id="deploy-notify-token" type="text" placeholder="Access Token" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
} 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 `<div class="config-row">
<label>Bot Token</label>
<input type="text" id="cfg-${slotKey}-bot_token" value="${botToken}" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
@ -6012,6 +6013,10 @@ function _renderBlockConfigFields(slotKey, blockType, config) {
<label>Chat ID</label>
<input type="text" id="cfg-${slotKey}-chat_id" value="${chatId}" placeholder="-1001234567890 or @channelname">
</div>
<div class="config-row">
<label>Thread ID</label>
<input type="text" id="cfg-${slotKey}-thread_id" value="${threadID}" placeholder="Optional integer">
</div>
<div class="config-row">
<label>Message</label>
<textarea id="cfg-${slotKey}-message" placeholder="Message with {variables}...">${config.message || '{name} completed with status: {status}'}</textarea>
@ -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 || '',
};
}

View file

@ -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 = '<div class="library-history-loading">Loading...</div>';
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 = `<div class="library-history-empty">Error: ${escapeHtml(data.error || 'Failed to load')}</div>`;
return;
}
if (entries.length === 0) {
list.innerHTML = '<div class="library-history-empty">🛡️<br><br>No quarantined files. Nice and clean.</div>';
return;
}
list.innerHTML = entries.map(renderQuarantineEntry).join('');
} catch (err) {
console.error('Error loading quarantine entries:', err);
list.innerHTML = '<div class="library-history-empty">Error loading quarantine</div>';
}
}
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 = `<span class="library-history-badge" style="border-color:${triggerColor};color:${triggerColor}">${escapeHtml(triggerLabel)}</span>`;
const reasonDetail = `<div class="library-history-entry-source"><span class="lh-prov-label">Reason:</span> ${escapeHtml(entry.reason || 'Unknown')}</div>`;
return `<div class="library-history-entry lh-expandable" onclick="this.classList.toggle('lh-expanded')">
<div class="library-history-thumb-placeholder">🛡</div>
<div class="library-history-entry-content">
<div class="library-history-entry-row1">
<div class="library-history-entry-text">
<div class="library-history-entry-title">${escapeHtml(entry.expected_track || entry.original_filename || 'Unknown')}</div>
<div class="library-history-entry-meta">${escapeHtml(meta)}</div>
</div>
<div class="library-history-entry-badges">${triggerBadge}</div>
<div class="library-history-entry-time">${formatHistoryTime(entry.timestamp)}</div>
<button class="lh-audit-btn" title="${approveTitle}" onclick="event.stopPropagation();${approveCall}">${approveLabel}</button>
<button class="lh-audit-btn" title="Delete permanently" style="border-color:rgba(248,113,113,0.4);color:#f87171" onclick="event.stopPropagation();deleteQuarantineEntry('${id}')">Delete</button>
<span class="lh-expand-btn">&#x25BE;</span>
</div>
<div class="library-history-entry-details">
${reasonDetail}
</div>
</div>
</div>`;
}
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 = {
<ul>
<li><strong>Bot Token:</strong> Your Telegram bot token (from @BotFather)</li>
<li><strong>Chat ID:</strong> The chat/group ID to send messages to</li>
<li><strong>Thread ID:</strong> Thread ID</li>
<li><strong>Message Template:</strong> Custom message with variable placeholders</li>
</ul>