From 58e160aba5aa6d1612731b3d0b626afed42b0bfe Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 28 Aug 2025 13:04:58 -0700 Subject: [PATCH] cancel button --- web_server.py | 9 +++ webui/static/script.js | 122 +++++++++++++++++++---------------------- 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/web_server.py b/web_server.py index f0e47edb..d6d5f86b 100644 --- a/web_server.py +++ b/web_server.py @@ -3506,6 +3506,9 @@ def _download_track_worker(task_id, batch_id=None): # Cancellation Checkpoint 1: Before doing anything with tasks_lock: + if task_id not in download_tasks: + print(f"❌ [Modal Worker] Task {task_id} was deleted before starting") + return if download_tasks[task_id]['status'] == 'cancelled': print(f"❌ [Modal Worker] Task {task_id} cancelled before starting") # Free worker slot when cancelled @@ -3586,6 +3589,9 @@ def _download_track_worker(task_id, batch_id=None): for query_index, query in enumerate(search_queries): # Cancellation check before each query with tasks_lock: + if task_id not in download_tasks: + print(f"❌ [Modal Worker] Task {task_id} was deleted during query {query_index + 1}") + return if download_tasks[task_id]['status'] == 'cancelled': print(f"❌ [Modal Worker] Task {task_id} cancelled during query {query_index + 1}") # Free worker slot when cancelled @@ -3662,6 +3668,9 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) for candidate_index, candidate in enumerate(candidates): # Check cancellation before each attempt with tasks_lock: + if task_id not in download_tasks: + print(f"❌ [Modal Worker] Task {task_id} was deleted during candidate {candidate_index + 1}") + return False if download_tasks[task_id]['status'] == 'cancelled': print(f"❌ [Modal Worker] Task {task_id} cancelled during candidate {candidate_index + 1}") # Free worker slot when cancelled diff --git a/webui/static/script.js b/webui/static/script.js index 0284e59a..58d58eb2 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -2048,6 +2048,9 @@ function startModalDownloadPolling(playlistId) { default: statusText = `⚪ ${task.status}`; break; } if(statusEl) statusEl.textContent = statusText; + if (actionsEl && !['completed', 'failed', 'cancelled'].includes(task.status) && actionsEl.innerHTML === '-') { + actionsEl.innerHTML = ``; + } if (actionsEl && ['completed', 'failed', 'cancelled'].includes(task.status)) { actionsEl.innerHTML = '-'; } @@ -2060,12 +2063,23 @@ function startModalDownloadPolling(playlistId) { document.getElementById(`stat-downloaded-${playlistId}`).textContent = completedCount; if (data.phase === 'complete' || data.phase === 'error' || (missingCount > 0 && totalFinished >= missingCount)) { - process.status = 'complete'; - showToast(`Process complete for ${process.playlist.name}!`, 'success'); + // --- REPLACE THE INSIDE OF THIS IF BLOCK with the following --- + if (data.phase === 'cancelled') { + process.status = 'cancelled'; + showToast(`Process cancelled for ${process.playlist.name}.`, 'info'); + } else if (data.phase === 'error') { + process.status = 'complete'; // Treat as complete to allow cleanup + showToast(`Process for ${process.playlist.name} failed!`, 'error'); + } else { + process.status = 'complete'; + showToast(`Process complete for ${process.playlist.name}!`, 'success'); + } + document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none'; clearInterval(process.poller); process.poller = null; updatePlaylistCardUI(playlistId); + // --- END OF REPLACEMENT BLOCK --- } } } catch (error) { @@ -2133,72 +2147,46 @@ async function cancelAllOperations(playlistId) { const process = activeDownloadProcesses[playlistId]; if (!process) return; - console.log(`🚫 Cancelling all operations for playlist ${playlistId}`); - - try { - // First, try to cancel the entire batch if we have a batch ID - if (process.batchId) { - try { - const batchResponse = await fetch(`/api/playlists/${process.batchId}/cancel_batch`, { - method: 'POST' - }); - const batchData = await batchResponse.json(); - if (batchData.success) { - console.log(`✅ Cancelled batch ${process.batchId} with ${batchData.cancelled_tasks} tasks`); - } - } catch (error) { - console.warn('Failed to cancel batch, falling back to individual cancellation:', error); - } - } - - // Also cancel individual download tasks for immediate UI feedback - const modal = document.getElementById(`download-missing-modal-${playlistId}`); - if (modal) { - const taskRows = modal.querySelectorAll('tr[data-task-id]'); - const cancellationPromises = []; - - taskRows.forEach(row => { - const taskId = row.dataset.taskId; - const trackIndex = row.dataset.trackIndex; - - if (taskId && row.dataset.locallyCancelled !== 'true') { - // Mark as locally cancelled for immediate UI feedback - row.dataset.locallyCancelled = 'true'; - const statusElement = document.getElementById(`download-${playlistId}-${trackIndex}`); - const actionsElement = document.getElementById(`actions-${playlistId}-${trackIndex}`); - if (statusElement) statusElement.textContent = '🚫 Cancelled'; - if (actionsElement) actionsElement.innerHTML = '-'; - - // Add to cancellation promises - cancellationPromises.push( - fetch('/api/downloads/cancel_task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ task_id: taskId }) - }).catch(error => { - console.warn(`Failed to cancel task ${taskId}:`, error); - }) - ); - } - }); - - // Wait for all cancellations to complete - if (cancellationPromises.length > 0) { - await Promise.allSettled(cancellationPromises); - console.log(`✅ Cancelled ${cancellationPromises.length} individual download tasks`); - } - } - - process.status = 'cancelled'; - cleanupDownloadProcess(playlistId); - showToast('All operations cancelled', 'info'); - - } catch (error) { - console.error('Error during cancellation:', error); - process.status = 'cancelled'; - cleanupDownloadProcess(playlistId); - showToast('Operations cancelled (with errors)', 'warning'); + // Prevent multiple cancel all operations + if (process.cancellingAll) { + console.log(`⚠️ Cancel All already in progress for ${playlistId}`); + return; } + process.cancellingAll = true; + + console.log(`🚫 Cancel All clicked for playlist ${playlistId} - closing modal and cleaning up server`); + + showToast('Cancelling all operations and closing modal...', 'info'); + + // Mark process as complete immediately so polling stops + process.status = 'complete'; + + // Stop any active polling + if (process.poller) { + clearInterval(process.poller); + process.poller = null; + } + + // Tell server to stop starting new downloads and clean up the batch + if (process.batchId) { + try { + // Cancel the batch (stops new downloads from starting) + const cancelResponse = await fetch(`/api/playlists/${process.batchId}/cancel_batch`, { + method: 'POST' + }); + if (cancelResponse.ok) { + const cancelData = await cancelResponse.json(); + console.log(`✅ Server stopped new downloads for batch ${process.batchId}`); + } + } catch (error) { + console.warn('Error during server batch cancel:', error); + } + } + + // Close the modal immediately - this will handle cleanup + closeDownloadMissingModal(playlistId); + + showToast('Modal closed. Active downloads will finish in background.', 'success'); } function resetToInitialState() {