-
-
${tracks.length}
+
${tracks.length}
Total Tracks
-
π Library Analysis
- Ready to start
+ Ready to start
β¬ Downloads
- Waiting for analysis
+ Waiting for analysis
-
`;
-
- // Reset state
- activeAnalysisTaskId = null;
- analysisResults = [];
- missingTracks = [];
- currentDownloadBatchId = null;
- if (modalDownloadPoller) {
- clearInterval(modalDownloadPoller);
- modalDownloadPoller = null;
- }
-
- // Show modal
+
modal.style.display = 'flex';
}
-function closeDownloadMissingModal() {
- // Clean up any active tasks
- if (activeAnalysisTaskId) {
- fetch(`/api/tracks/analyze/cancel/${activeAnalysisTaskId}`, { method: 'POST' })
- .catch(e => console.warn('Failed to cancel analysis task:', e));
+
+
+function closeDownloadMissingModal(playlistId) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process) {
+ // If somehow called without a process, try to find and remove the element
+ const modal = document.getElementById(`download-missing-modal-${playlistId}`);
+ if (modal && modal.parentElement) {
+ modal.parentElement.removeChild(modal);
+ }
+ return;
}
-
- const modal = document.getElementById('download-missing-modal');
- if (modal) {
- modal.style.display = 'none';
- }
-
- // Reset state
- activeAnalysisTaskId = null;
- currentPlaylistTracks = [];
- analysisResults = [];
- missingTracks = [];
- currentDownloadBatchId = null;
- currentModalPlaylistId = null;
- if (modalDownloadPoller) {
- clearInterval(modalDownloadPoller);
- modalDownloadPoller = null;
+
+ // If the process is running, just hide the modal.
+ // If it's idle, complete, or cancelled, perform a full cleanup.
+ if (process.status === 'running') {
+ console.log(`Hiding active download modal for playlist ${playlistId}.`);
+ process.modalElement.style.display = 'none';
+ } else {
+ console.log(`Closing and cleaning up download modal for playlist ${playlistId}.`);
+ cleanupDownloadProcess(playlistId);
}
}
-async function startTrackAnalysis() {
- console.log(`π Starting track analysis for ${currentPlaylistTracks.length} tracks`);
+async function startTrackAnalysis(playlistId) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process) return;
+
+ console.log(`π Starting track analysis for ${process.tracks.length} tracks in playlist ${playlistId}`);
try {
- // Update UI to analysis mode
- document.getElementById('begin-analysis-btn').style.display = 'none';
- document.getElementById('cancel-all-btn').style.display = 'inline-block';
- document.getElementById('analysis-progress-text').textContent = 'Starting analysis...';
+ process.status = 'running';
+ updatePlaylistCardUI(playlistId);
+
+ document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none';
+ document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block';
+ document.getElementById(`analysis-progress-text-${playlistId}`).textContent = 'Starting analysis...';
- // Start analysis
const response = await fetch('/api/tracks/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- tracks: currentPlaylistTracks
- })
+ body: JSON.stringify({ tracks: process.tracks })
});
const data = await response.json();
if (!data.success) throw new Error(data.error);
- activeAnalysisTaskId = data.task_id;
- console.log(`β
Analysis started with task ID: ${activeAnalysisTaskId}`);
-
- // Start polling for results
- startAnalysisPolling();
+ process.analysisTaskId = data.task_id;
+ startAnalysisPolling(playlistId);
} catch (error) {
- console.error('β Failed to start analysis:', error);
showToast(`Failed to start analysis: ${error.message}`, 'error');
-
- // Reset UI
- document.getElementById('begin-analysis-btn').style.display = 'inline-block';
- document.getElementById('cancel-all-btn').style.display = 'none';
- document.getElementById('analysis-progress-text').textContent = 'Ready to start';
+ process.status = 'cancelled';
+ cleanupDownloadProcess(playlistId);
}
}
-function startAnalysisPolling() {
- if (!activeAnalysisTaskId) return;
-
- const pollInterval = setInterval(async () => {
+function startAnalysisPolling(playlistId) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process || !process.analysisTaskId) return;
+
+ const poller = setInterval(async () => {
+ // If process is gone, stop polling
+ if (!activeDownloadProcesses[playlistId]) {
+ clearInterval(poller);
+ return;
+ }
+
try {
- const response = await fetch(`/api/tracks/analyze/status/${activeAnalysisTaskId}`);
+ const response = await fetch(`/api/tracks/analyze/status/${process.analysisTaskId}`);
const status = await response.json();
- if (response.status === 404 || status.error) {
- console.error('β Analysis task not found or error:', status.error);
- clearInterval(pollInterval);
- return;
+ if (status.error) throw new Error(status.error);
+
+ document.getElementById(`analysis-progress-fill-${playlistId}`).style.width = `${status.progress || 0}%`;
+ document.getElementById(`analysis-progress-text-${playlistId}`).textContent =
+ `${status.processed || 0}/${status.total || 0} tracks analyzed (${status.progress || 0}%)`;
+
+ if (status.results) {
+ updateTrackAnalysisResults(playlistId, status.results);
}
- // Update progress bar
- const progressPercent = status.progress || 0;
- document.getElementById('analysis-progress-fill').style.width = `${progressPercent}%`;
- document.getElementById('analysis-progress-text').textContent =
- `${status.processed || 0}/${status.total || 0} tracks analyzed (${progressPercent}%)`;
-
- // Update table with individual results
- if (status.results && status.results.length > 0) {
- updateTrackAnalysisResults(status.results);
- }
-
- // Check if complete
if (status.status === 'complete') {
- console.log(`β
Analysis complete: ${status.total_found} found, ${status.total_missing} missing`);
- clearInterval(pollInterval);
- onAnalysisComplete(status);
- } else if (status.status === 'error') {
- console.error('β Analysis failed:', status.error);
- clearInterval(pollInterval);
- showToast(`Analysis failed: ${status.error}`, 'error');
- resetToInitialState();
- } else if (status.status === 'cancelled') {
- console.log('β οΈ Analysis was cancelled');
- clearInterval(pollInterval);
- resetToInitialState();
+ clearInterval(poller);
+ onAnalysisComplete(playlistId, status);
+ } else if (status.status === 'error' || status.status === 'cancelled') {
+ clearInterval(poller);
+ throw new Error(status.error || 'Analysis was cancelled');
}
-
} catch (error) {
- console.error('β Error polling analysis status:', error);
- clearInterval(pollInterval);
- showToast('Failed to get analysis status', 'error');
+ clearInterval(poller);
+ showToast(`Analysis failed: ${error.message}`, 'error');
+ process.status = 'cancelled';
+ cleanupDownloadProcess(playlistId);
}
- }, 1000); // Poll every second
+ }, 1000);
}
-function updateTrackAnalysisResults(results) {
+function updateTrackAnalysisResults(playlistId, results) {
for (const result of results) {
- const trackIndex = result.track_index;
- const matchElement = document.getElementById(`match-${trackIndex}`);
-
+ const matchElement = document.getElementById(`match-${playlistId}-${result.track_index}`);
if (matchElement) {
- if (result.found) {
- matchElement.textContent = 'β
Found';
- matchElement.className = 'track-match-status match-found';
- } else {
- matchElement.textContent = 'β Missing';
- matchElement.className = 'track-match-status match-missing';
- }
+ matchElement.textContent = result.found ? 'β
Found' : 'β Missing';
+ matchElement.className = `track-match-status ${result.found ? 'match-found' : 'match-missing'}`;
}
}
}
-function onAnalysisComplete(status) {
- // Update dashboard stats
- document.getElementById('stat-found').textContent = status.total_found || 0;
- document.getElementById('stat-missing').textContent = status.total_missing || 0;
+function onAnalysisComplete(playlistId, status) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process) return;
+
+ document.getElementById(`stat-found-${playlistId}`).textContent = status.total_found || 0;
+ document.getElementById(`stat-missing-${playlistId}`).textContent = status.total_missing || 0;
+ document.getElementById(`analysis-progress-text-${playlistId}`).textContent = 'Analysis complete!';
- // Update progress text
- document.getElementById('analysis-progress-text').textContent = 'Analysis complete!';
- document.getElementById('download-progress-text').textContent = 'Ready to download missing tracks';
+ process.analysisResults = status.results || [];
+ process.missingTracks = process.analysisResults.filter(r => !r.found);
- // Store results
- analysisResults = status.results || [];
- missingTracks = analysisResults.filter(r => !r.found);
-
- console.log(`π Analysis results: ${analysisResults.length} total, ${missingTracks.length} missing`);
-
- // Update UI and automatically start downloads if there are missing tracks
- document.getElementById('cancel-all-btn').style.display = 'none';
-
- if (missingTracks.length > 0) {
- console.log(`π Analysis complete - automatically starting downloads for ${missingTracks.length} missing tracks`);
- // Automatically initiate downloads - no button needed, just like the GUI
- initiateMissingDownloads();
+ if (process.missingTracks.length > 0) {
+ initiateMissingDownloads(playlistId);
} else {
showToast('All tracks were found in your library!', 'success');
- document.getElementById('download-progress-text').textContent = 'No downloads needed - all tracks found!';
+ process.status = 'complete';
+ document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none';
+ // Don't auto-close, let user close it.
}
}
-async function initiateMissingDownloads() {
- if (missingTracks.length === 0) {
- showToast('No missing tracks to download', 'info');
- return;
- }
-
- console.log(`β¬ Starting enhanced downloads for ${missingTracks.length} missing tracks`);
-
+async function initiateMissingDownloads(playlistId) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process || process.missingTracks.length === 0) return;
+
try {
- // Update UI - Cancel button should already be visible from analysis
- document.getElementById('download-progress-text').textContent = 'Initiating downloads...';
+ document.getElementById(`download-progress-text-${playlistId}`).textContent = 'Initiating downloads...';
- // Set initial status for all missing tracks
- for (const result of missingTracks) {
- const statusElement = document.getElementById(`download-${result.track_index}`);
- const actionsElement = document.getElementById(`actions-${result.track_index}`);
- if (statusElement) {
- statusElement.textContent = 'βΈοΈ Pending';
- statusElement.className = 'track-download-status download-pending';
- }
- if (actionsElement) {
- actionsElement.innerHTML = ``;
- }
+ for (const result of process.missingTracks) {
+ const statusElement = document.getElementById(`download-${playlistId}-${result.track_index}`);
+ const actionsElement = document.getElementById(`actions-${playlistId}-${result.track_index}`);
+ if (statusElement) statusElement.textContent = 'βΈοΈ Pending';
+ if (actionsElement) actionsElement.innerHTML = ``;
}
- // Call new playlist-specific endpoint
- const response = await fetch(`/api/playlists/${currentModalPlaylistId}/download_missing`, {
+ const response = await fetch(`/api/playlists/${playlistId}/download_missing`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- missing_tracks: missingTracks.map(track => ({
- track: track.track,
- track_index: track.track_index
- })) // Include both track data and original index
+ missing_tracks: process.missingTracks.map(r => ({ track: r.track, track_index: r.track_index }))
})
});
const data = await response.json();
if (!data.success) throw new Error(data.error);
- // Store batch ID for polling
- currentDownloadBatchId = data.batch_id;
-
- // PHASE 2: Clear cancelled tracks for new session (GUI PARITY)
- cancelledTracks.clear();
- console.log(`β
Started download batch: ${currentDownloadBatchId}, cleared cancelled tracks`);
-
- // Clear any locally cancelled flags from previous sessions
- document.querySelectorAll('tr[data-locally-cancelled="true"]').forEach(row => {
- row.removeAttribute('data-locally-cancelled');
- });
-
- // Start live polling
- startModalDownloadPolling();
-
- showToast(`Started downloads for ${missingTracks.length} tracks with live progress tracking.`, 'success');
+ process.batchId = data.batch_id;
+ startModalDownloadPolling(playlistId);
+ showToast(`Started downloads for ${process.missingTracks.length} tracks.`, 'success');
} catch (error) {
- console.error('β Failed to start downloads:', error);
showToast(`Failed to start downloads: ${error.message}`, 'error');
-
- // Reset UI on error - show the begin analysis button again
- document.getElementById('begin-analysis-btn').style.display = 'inline-block';
- document.getElementById('cancel-all-btn').style.display = 'none';
- document.getElementById('download-progress-text').textContent = 'Download initiation failed';
+ process.status = 'cancelled';
+ cleanupDownloadProcess(playlistId);
}
}
-function startModalDownloadPolling() {
- if (!currentDownloadBatchId) {
- console.warn('No batch ID available for polling');
- return;
- }
-
- if (modalDownloadPoller) {
- clearInterval(modalDownloadPoller);
- }
-
- console.log(`π Starting download status polling for batch: ${currentDownloadBatchId}`);
-
- modalDownloadPoller = setInterval(async () => {
- try {
- const response = await fetch(`/api/playlists/${currentDownloadBatchId}/download_status`);
- const data = await response.json();
-
- if (data.error) {
- console.error('Polling error:', data.error);
- return;
- }
-
- const tasks = data.tasks || [];
- let completedCount = 0;
- let failedCount = 0;
- let totalTasks = tasks.length;
-
- // Update each track's status in the modal
- for (const task of tasks) {
- const trackIndex = task.track_index;
- const row = document.querySelector(`tr[data-track-index="${trackIndex}"]`);
-
- const isMissingTrack = missingTracks.some(mt => mt.track_index === trackIndex);
-
- if (row && isMissingTrack) {
- // PHASE 2 & 5: Respect local cancellation with recovery (GUI PARITY)
- // Skip polling updates for tracks that were cancelled locally
- if (row.dataset.locallyCancelled === 'true') {
- console.log(`βοΈ Skipping polling update for locally cancelled track ${trackIndex}`);
-
- // PHASE 5: Recovery check - ensure cancelled status is maintained
- const statusElement = row.querySelector('.track-download-status');
- if (statusElement && !statusElement.textContent.includes('π« Cancelled')) {
- console.warn(`π§ Recovering cancelled status for track ${trackIndex}`);
- statusElement.textContent = 'π« Cancelled';
- statusElement.className = 'track-download-status download-cancelled';
-
- const actionsElement = row.querySelector('.track-actions');
- if (actionsElement) {
- actionsElement.innerHTML = '-';
- }
- }
- continue;
- }
-
- const statusElement = row.querySelector('.track-download-status');
- const actionsElement = row.querySelector('.track-actions');
- row.dataset.taskId = task.task_id;
-
- const status = task.status;
- const progress = task.progress || 0; // Get live progress from backend
+function startModalDownloadPolling(playlistId) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process || !process.batchId) return;
- switch (status) {
- case 'pending':
- statusElement.textContent = 'βΈοΈ Pending';
- statusElement.className = 'track-download-status download-pending';
- actionsElement.innerHTML = ``;
- break;
- case 'searching':
- statusElement.textContent = 'π Searching...';
- statusElement.className = 'track-download-status download-searching';
- actionsElement.innerHTML = ``;
- break;
- case 'downloading':
- statusElement.textContent = `β¬ Downloading... ${Math.round(progress)}%`;
- statusElement.className = 'track-download-status download-downloading';
- actionsElement.innerHTML = ``;
- break;
- case 'completed':
- statusElement.textContent = 'β
Completed';
- statusElement.className = 'track-download-status download-complete';
- actionsElement.innerHTML = '-';
- completedCount++;
- break;
- case 'failed':
- statusElement.textContent = 'β Failed';
- statusElement.className = 'track-download-status download-failed';
- actionsElement.innerHTML = '-';
- failedCount++;
- break;
- case 'cancelled':
- statusElement.textContent = 'β Cancelled';
- statusElement.className = 'track-download-status download-cancelled';
- actionsElement.innerHTML = '-';
- failedCount++;
- break;
- default:
- statusElement.textContent = `βͺ ${status}`;
- statusElement.className = 'track-download-status';
- break;
- }
- }
- }
-
- // PHASE 2: Include locally cancelled tracks in progress calculation (GUI PARITY)
- const locallyCancelledCount = cancelledTracks.size;
- const totalFinished = completedCount + failedCount + locallyCancelledCount;
- const progressPercent = totalTasks > 0 ? (totalFinished / totalTasks) * 100 : 0;
-
- document.getElementById('download-progress-fill').style.width = `${progressPercent}%`;
- document.getElementById('download-progress-text').textContent =
- `${completedCount}/${totalTasks} completed (${progressPercent.toFixed(0)}%)`;
- document.getElementById('stat-downloaded').textContent = completedCount;
-
- // Stop polling when all tasks are complete (including locally cancelled)
- if (totalFinished >= totalTasks && totalTasks > 0) {
- clearInterval(modalDownloadPoller);
- modalDownloadPoller = null;
- document.getElementById('cancel-all-btn').style.display = 'none';
- console.log('β
All download tasks completed, stopping polling');
- if (completedCount > 0) {
- showToast(`Download completed: ${completedCount} tracks downloaded successfully!`, 'success');
- }
- }
-
- } catch (error) {
- console.error('Error polling download status:', error);
+ if (process.poller) clearInterval(process.poller);
+
+ process.poller = setInterval(async () => {
+ if (!activeDownloadProcesses[playlistId]) {
+ clearInterval(process.poller);
+ return;
}
- }, 2000); // Poll every 2 seconds
+
+ try {
+ const response = await fetch(`/api/playlists/${process.batchId}/download_status`);
+ const data = await response.json();
+ if (data.error) throw new Error(data.error);
+
+ let completedCount = 0;
+ let failedOrCancelledCount = 0;
+
+ (data.tasks || []).forEach(task => {
+ const statusElement = document.getElementById(`download-${playlistId}-${task.track_index}`);
+ const row = statusElement ? statusElement.closest('tr') : null;
+ if (!row) return;
+
+ if (row.dataset.locallyCancelled === 'true') {
+ failedOrCancelledCount++;
+ return;
+ }
+
+ row.dataset.taskId = task.task_id;
+ let statusText = '';
+ switch (task.status) {
+ case 'pending': statusText = 'βΈοΈ Pending'; break;
+ case 'searching': statusText = 'π Searching...'; break;
+ case 'downloading': statusText = `β¬ Downloading... ${Math.round(task.progress || 0)}%`; break;
+ case 'completed': statusText = 'β
Completed'; completedCount++; break;
+ case 'failed': statusText = 'β Failed'; failedOrCancelledCount++; break;
+ case 'cancelled': statusText = 'π« Cancelled'; failedOrCancelledCount++; break;
+ default: statusText = `βͺ ${task.status}`; break;
+ }
+ statusElement.textContent = statusText;
+ if (['completed', 'failed', 'cancelled'].includes(task.status)) {
+ document.getElementById(`actions-${playlistId}-${task.track_index}`).innerHTML = '-';
+ }
+ });
+
+ const totalMissing = process.missingTracks.length;
+ const totalFinished = completedCount + failedOrCancelledCount;
+ const progressPercent = totalMissing > 0 ? (totalFinished / totalMissing) * 100 : 0;
+
+ document.getElementById(`download-progress-fill-${playlistId}`).style.width = `${progressPercent}%`;
+ document.getElementById(`download-progress-text-${playlistId}`).textContent =
+ `${completedCount}/${totalMissing} completed (${progressPercent.toFixed(0)}%)`;
+ document.getElementById(`stat-downloaded-${playlistId}`).textContent = completedCount;
+
+ if (totalFinished >= totalMissing) {
+ process.status = 'complete';
+ showToast(`Downloads complete for ${process.playlist.name}!`, 'success');
+ document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none';
+ clearInterval(process.poller);
+ process.poller = null;
+ updatePlaylistCardUI(playlistId); // Final card update
+ }
+ } catch (error) {
+ console.error(`Polling error for ${playlistId}:`, error);
+ // Don't stop polling on transient errors
+ }
+ }, 2000);
}
async function updateModalWithLiveDownloadProgress() {
try {
@@ -2188,16 +2122,17 @@ async function updateModalWithLiveDownloadProgress() {
}
}
-function cancelAllOperations() {
- console.log('π Cancelling all operations');
-
- // Cancel analysis if running
- if (activeAnalysisTaskId) {
- fetch(`/api/tracks/analyze/cancel/${activeAnalysisTaskId}`, { method: 'POST' })
- .catch(e => console.warn('Failed to cancel analysis:', e));
+async function cancelAllOperations(playlistId) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process) return;
+
+ if (process.analysisTaskId) {
+ await fetch(`/api/tracks/analyze/cancel/${process.analysisTaskId}`, { method: 'POST' });
}
-
- resetToInitialState();
+ // Note: Batch cancellation isn't implemented on the backend yet,
+ // but cleaning up the process will stop polling and allow individual cancellations.
+ process.status = 'cancelled';
+ cleanupDownloadProcess(playlistId);
showToast('Operations cancelled', 'info');
}
@@ -2247,93 +2182,38 @@ function resetToInitialState() {
missingTracks = [];
}
-async function cancelTrackDownload(trackIndex) {
- console.log(`π Cancelling download for track ${trackIndex}`);
+async function cancelTrackDownload(playlistId, trackIndex) {
+ const process = activeDownloadProcesses[playlistId];
+ if (!process) return;
+
+ const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${trackIndex}"]`);
+ if (!row) return;
+
+ const taskId = row.dataset.taskId;
+ if (!taskId) {
+ showToast('Task not started yet, cannot cancel.', 'warning');
+ return;
+ }
+
+ // UI update for immediate feedback
+ row.dataset.locallyCancelled = 'true';
+ document.getElementById(`download-${playlistId}-${trackIndex}`).textContent = 'π« Cancelled';
+ document.getElementById(`actions-${playlistId}-${trackIndex}`).innerHTML = '-';
try {
- // Find the table row for this track
- const row = document.querySelector(`tr[data-track-index="${trackIndex}"]`);
- if (!row) {
- console.error(`Could not find row for track index ${trackIndex}`);
- return;
- }
-
- // PHASE 5: Prevent double cancellation and check terminal states (GUI PARITY)
- if (row.dataset.locallyCancelled === 'true') {
- console.warn(`Track ${trackIndex} is already cancelled locally`);
- showToast('Track is already cancelled', 'info');
- return;
- }
-
- const statusElement = row.querySelector('.track-download-status');
- const currentStatus = statusElement?.textContent || '';
-
- // Check if track is in a terminal state where cancellation doesn't make sense
- if (currentStatus.includes('β
Completed') || currentStatus.includes('β Failed')) {
- console.warn(`Cannot cancel track ${trackIndex} - already in terminal state: ${currentStatus}`);
- showToast(`Cannot cancel - track is already ${currentStatus.includes('β
') ? 'completed' : 'failed'}`, 'warning');
- return;
- }
-
- // PHASE 1: Immediate UI response (GUI PARITY)
- // Update UI immediately to show definitive cancellation like GUI
- const actionsElement = row.querySelector('.track-actions');
-
- if (statusElement) {
- statusElement.textContent = 'π« Cancelled';
- statusElement.className = 'track-download-status download-cancelled';
- }
- if (actionsElement) {
- actionsElement.innerHTML = '-';
- }
-
- // PHASE 2: Add to cancelled tracks Set and mark locally cancelled (GUI PARITY)
- cancelledTracks.add(trackIndex);
- row.dataset.locallyCancelled = 'true';
- console.log(`β
Track ${trackIndex} added to cancelledTracks Set and marked locally cancelled (immediate UI response)`);
-
- // Get the task ID for backend cancellation
- const taskId = row.dataset.taskId;
- if (!taskId) {
- console.warn(`No task ID found for track ${trackIndex}, using local cancellation only`);
- showToast('Track cancelled (local only)', 'info');
- return;
- }
-
- // PHASE 5: Call backend with timeout to avoid hanging (GUI PARITY)
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
-
const response = await fetch('/api/downloads/cancel_task', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ task_id: taskId }),
- signal: controller.signal
+ body: JSON.stringify({ task_id: taskId })
});
-
- clearTimeout(timeoutId);
-
const data = await response.json();
if (data.success) {
- console.log(`β
Successfully cancelled task ${taskId} on backend`);
- showToast('Download cancelled successfully', 'info');
+ showToast('Download cancelled and added to wishlist.', 'info');
} else {
- console.warn(`Backend cancellation failed for task ${taskId}: ${data.error}`);
- // Don't change UI - we already showed cancelled status immediately
- showToast('Download cancelled (backend error, but stopped locally)', 'warning');
+ throw new Error(data.error);
}
-
} catch (error) {
- // PHASE 5: Handle different error types appropriately (GUI PARITY)
- if (error.name === 'AbortError') {
- console.warn('β±οΈ Backend cancellation timed out after 5 seconds');
- showToast('Download cancelled (backend timeout, but stopped locally)', 'warning');
- } else {
- console.error('β Error with backend cancellation:', error);
- showToast('Download cancelled (network error, but stopped locally)', 'warning');
- }
- // Don't revert UI - track is still cancelled locally like GUI behavior
- console.log(`Track ${trackIndex} remains cancelled locally despite backend error`);
+ showToast(`Could not cancel task: ${error.message}`, 'error');
}
}
diff --git a/webui/static/style.css b/webui/static/style.css
index 5dcb1f4e..5c1854e7 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -4931,4 +4931,53 @@ body {
.modal-close-section {
display: flex;
align-items: center;
+}
+
+
+/* ==============================================
+ PERSISTENT DOWNLOAD MODAL STYLES
+ ============================================== */
+
+.download-missing-modal {
+ display: none; /* Hidden by default */
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(8px);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+}
+
+.playlist-card-actions button {
+ background: transparent;
+ border: 1px solid #1db954;
+ border-radius: 15px;
+ color: #1db954;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 8px 16px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.playlist-card-actions button:hover {
+ background: #1db954;
+ color: #000000;
+}
+
+.playlist-card-actions .view-progress-btn {
+ background: #1db954;
+ color: #000000;
+}
+
+.playlist-card-actions .view-progress-btn:hover {
+ background: #1ed760;
+}
+
+.hidden {
+ display: none !important;
}
\ No newline at end of file