fix(quarantine): auto-delete siblings + cancel retry on approve (#920)

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 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-24 12:37:33 +02:00
parent 55f7176c34
commit 842bc0ab34
2 changed files with 37 additions and 3 deletions

View file

@ -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}")

View file

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