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:
parent
55f7176c34
commit
842bc0ab34
2 changed files with 37 additions and 3 deletions
|
|
@ -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}")
|
logger.info(f"[Quarantine] Auto-removed {len(removed_siblings)} sibling alternative(s) of {entry_id}: {removed_siblings}")
|
||||||
except Exception as sib_exc:
|
except Exception as sib_exc:
|
||||||
logger.warning(f"[Quarantine] Sibling cleanup for {entry_id} failed: {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({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"trigger_bypassed": "all",
|
"trigger_bypassed": "all",
|
||||||
"original_trigger": trigger,
|
"original_trigger": trigger,
|
||||||
"removed_siblings": removed_siblings,
|
"removed_siblings": removed_siblings,
|
||||||
|
"cancelled_retry_task": cancelled_retry_task,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Quarantine] Error approving {entry_id}: {e}")
|
logger.error(f"[Quarantine] Error approving {entry_id}: {e}")
|
||||||
|
|
|
||||||
|
|
@ -2786,10 +2786,17 @@ async function verifQuarApprove(idx, btn) {
|
||||||
const q = _verifQuarEntries[idx]; if (!q) return;
|
const q = _verifQuarEntries[idx]; if (!q) return;
|
||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
try {
|
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();
|
const d = await r.json();
|
||||||
if (d.success) {
|
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);
|
_verifLoadQuarantine(true);
|
||||||
} else {
|
} else {
|
||||||
showToast && showToast(d.error || 'Approve failed', 'error');
|
showToast && showToast(d.error || 'Approve failed', 'error');
|
||||||
|
|
@ -2905,7 +2912,11 @@ async function verifQuarApproveAll(btn) {
|
||||||
let ok = 0;
|
let ok = 0;
|
||||||
for (const q of entries) {
|
for (const q of entries) {
|
||||||
try {
|
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();
|
const d = await r.json();
|
||||||
if (d.success) ok++;
|
if (d.success) ok++;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue