diff --git a/web_server.py b/web_server.py index 2025220d..07bda0ea 100644 --- a/web_server.py +++ b/web_server.py @@ -7117,14 +7117,33 @@ def approve_quarantine_item(entry_id): # for this one restored pass so multi-reason failures do not loop. context['_skip_quarantine_check'] = 'all' context['_approved_quarantine_trigger'] = trigger - # Re-dispatch through the same pipeline. Run async so the HTTP - # request returns quickly — UI polls /list to see the entry vanish. + # If the caller (download-modal chooser) passed the originating task, run + # the re-import through the verification WRAPPER with that task_id so the + # task is marked completed on success — otherwise the modal row stays + # stuck on "Quarantined" even though the file imported. The sidecar + # context lost task_id/batch_id (the wrapper pops them before quarantine), + # so we re-supply them here. Manager-tab approvals (no task_id) keep the + # original inner-pipeline path. + _req = request.get_json(silent=True) or {} + _task_id = (_req.get('task_id') or '').strip() or None + _batch_id = None + if _task_id: + with tasks_lock: + _t = download_tasks.get(_task_id) + if isinstance(_t, dict): + _batch_id = _t.get('batch_id') + context['task_id'] = _task_id + if _batch_id: + context['batch_id'] = _batch_id context_key = f"approve_{entry_id}_{int(time.time())}" - threading.Thread( - target=lambda: _post_process_matched_download(context_key, context, restored_path), - daemon=True, - ).start() - logger.info(f"[Quarantine] Approved {entry_id} (original_trigger={trigger}, bypass=all) → re-running pipeline") + if _task_id: + _reprocess = lambda: _post_process_matched_download_with_verification( + context_key, context, restored_path, _task_id, _batch_id, + ) + else: + _reprocess = lambda: _post_process_matched_download(context_key, context, restored_path) + threading.Thread(target=_reprocess, daemon=True).start() + logger.info(f"[Quarantine] Approved {entry_id} (original_trigger={trigger}, bypass=all, task={_task_id}) → re-running pipeline") return jsonify({"success": True, "trigger_bypassed": "all", "original_trigger": trigger}) except Exception as e: logger.error(f"[Quarantine] Error approving {entry_id}: {e}") diff --git a/webui/static/downloads.js b/webui/static/downloads.js index ebf82c13..a158bc9e 100644 --- a/webui/static/downloads.js +++ b/webui/static/downloads.js @@ -3449,7 +3449,7 @@ function showQuarantineChooser({ entryId, taskId, reason, trackName }) { requestAnimationFrame(() => overlay.classList.add('visible')); const acceptBtn = overlay.querySelector('#qc-accept-btn'); - if (acceptBtn) acceptBtn.addEventListener('click', () => acceptQuarantineFromChooser(acceptBtn, entryId)); + if (acceptBtn) acceptBtn.addEventListener('click', () => acceptQuarantineFromChooser(acceptBtn, entryId, taskId)); const searchBtn = overlay.querySelector('#qc-search-btn'); if (searchBtn) searchBtn.addEventListener('click', () => { closeQuarantineChooser(); @@ -3457,7 +3457,7 @@ function showQuarantineChooser({ entryId, taskId, reason, trackName }) { }); } -async function acceptQuarantineFromChooser(button, entryId) { +async function acceptQuarantineFromChooser(button, entryId, taskId) { if (!entryId) { showToast('Cannot accept — missing quarantine id.', 'error'); return; } const confirmed = await showConfirmDialog({ title: 'Accept Quarantined File', @@ -3473,7 +3473,11 @@ async function acceptQuarantineFromChooser(button, entryId) { // Release the preview stream first so the file isn't locked during the move. await _qcReleaseAudio(); try { - const resp = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/approve`, { method: 'POST' }); + const resp = await fetch(`/api/quarantine/${encodeURIComponent(entryId)}/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ task_id: taskId || '' }), + }); const data = await resp.json(); if (data.success) { showToast('Accepted. Re-running post-processing.', 'success'); @@ -3770,6 +3774,14 @@ function processModalStatusUpdate(playlistId, data) { statusEl.classList.remove('has-error-tooltip'); statusEl.removeAttribute('title'); statusEl.removeAttribute('data-error-msg'); + // Clear clickable/quarantine state each render; the failure + // branch below re-adds it when still applicable. Without this a + // task that flips failed/quarantined -> completed (e.g. after + // Accept & Import) keeps a stale chooser on the cell. + statusEl.classList.remove('has-candidates'); + delete statusEl.dataset.quarantineEntryId; + delete statusEl.dataset.quarantineReason; + delete statusEl.dataset.quarantineTrack; statusEl.textContent = statusText; if ((task.status === 'failed' || task.status === 'cancelled' || task.status === 'not_found') && task.error_message) {