From 842bc0ab34f95d85fdde5c29624aefefa3119d96 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 24 Jun 2026 12:37:33 +0200 Subject: [PATCH] fix(quarantine): auto-delete siblings + cancel retry on approve (#920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple quarantine entries accumulate per track (one per retry attempt) and approving one left the others behind and kept the retry running. - Approve now sends remove_siblings=true โ€” backend sibling deletion was already implemented but the flag was never passed from the UI - Toast message now reports how many duplicate candidates were removed - After approve, the backend cancels any in-flight quarantine-retry task for the same track (matched by title) so the engine stops fetching new candidates once the user has already accepted one - Approve All also sends remove_siblings=true Co-Authored-By: Claude Sonnet 4.6 --- web_server.py | 23 +++++++++++++++++++++++ webui/static/pages-extra.js | 17 ++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/web_server.py b/web_server.py index 1e5cc862..3e3c539f 100644 --- a/web_server.py +++ b/web_server.py @@ -7668,11 +7668,34 @@ def approve_quarantine_item(entry_id): logger.info(f"[Quarantine] Auto-removed {len(removed_siblings)} sibling alternative(s) of {entry_id}: {removed_siblings}") except Exception as sib_exc: logger.warning(f"[Quarantine] Sibling cleanup for {entry_id} failed: {sib_exc}") + # Cancel any still-running quarantine-retry task for the same track so + # the engine doesn't keep fetching new candidates after the user has + # already accepted one. Match by track title from the restored context. + cancelled_retry_task = None + try: + ti = context.get('track_info') if isinstance(context.get('track_info'), dict) else {} + _approved_name = (ti.get('name') or '').strip().lower() + if _approved_name: + with tasks_lock: + for _tid, _t in download_tasks.items(): + if not _t.get('_quarantine_retry'): + continue + if _t.get('status') in ('completed', 'cancelled', 'failed'): + continue + _tti = _t.get('track_info') if isinstance(_t.get('track_info'), dict) else {} + if (_tti.get('name') or '').strip().lower() == _approved_name: + _t['status'] = 'cancelled' + cancelled_retry_task = _tid + logger.info(f"[Quarantine] Cancelled in-flight retry task {_tid} for '{_approved_name}' (user approved alternative)") + break + except Exception as _crt_exc: + logger.debug(f"[Quarantine] Retry-cancel scan failed: {_crt_exc}") return jsonify({ "success": True, "trigger_bypassed": "all", "original_trigger": trigger, "removed_siblings": removed_siblings, + "cancelled_retry_task": cancelled_retry_task, }) except Exception as e: logger.error(f"[Quarantine] Error approving {entry_id}: {e}") diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js index 8dfd2f19..d398c309 100644 --- a/webui/static/pages-extra.js +++ b/webui/static/pages-extra.js @@ -2786,10 +2786,17 @@ async function verifQuarApprove(idx, btn) { const q = _verifQuarEntries[idx]; if (!q) return; if (btn) btn.disabled = true; try { - const r = await fetch(`/api/quarantine/${encodeURIComponent(q.id)}/approve`, { method: 'POST' }); + const r = await fetch(`/api/quarantine/${encodeURIComponent(q.id)}/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ remove_siblings: true }), + }); const d = await r.json(); if (d.success) { - showToast && showToast('Approved โ€” re-importing, will be marked human-verified ๐Ÿ›กโœ”', 'success'); + const extra = d.removed_siblings && d.removed_siblings.length + ? ` (${d.removed_siblings.length} duplicate candidate${d.removed_siblings.length > 1 ? 's' : ''} removed)` + : ''; + showToast && showToast(`Approved โ€” re-importing, will be marked human-verified ๐Ÿ›กโœ”${extra}`, 'success'); _verifLoadQuarantine(true); } else { showToast && showToast(d.error || 'Approve failed', 'error'); @@ -2905,7 +2912,11 @@ async function verifQuarApproveAll(btn) { let ok = 0; for (const q of entries) { try { - const r = await fetch(`/api/quarantine/${encodeURIComponent(q.id)}/approve`, { method: 'POST' }); + const r = await fetch(`/api/quarantine/${encodeURIComponent(q.id)}/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ remove_siblings: true }), + }); const d = await r.json(); if (d.success) ok++; } catch (e) {}