diff --git a/routes/utils/celery_tasks.py b/routes/utils/celery_tasks.py index 59cda3f..23b5a90 100644 --- a/routes/utils/celery_tasks.py +++ b/routes/utils/celery_tasks.py @@ -1041,7 +1041,11 @@ def download_playlist(self, **task_data): custom_dir_format=custom_dir_format, custom_track_format=custom_track_format, pad_tracks=pad_tracks, - progress_callback=self.progress_callback + initial_retry_delay=initial_retry_delay, + retry_delay_increase=retry_delay_increase, + max_retries=max_retries, + progress_callback=self.progress_callback, + spotify_quality=fall_quality ) return {"status": "success", "message": "Playlist download completed"} diff --git a/routes/utils/playlist.py b/routes/utils/playlist.py index 3878a7a..633b180 100755 --- a/routes/utils/playlist.py +++ b/routes/utils/playlist.py @@ -95,7 +95,8 @@ def download_playlist( pad_tracks=pad_tracks, initial_retry_delay=initial_retry_delay, retry_delay_increase=retry_delay_increase, - max_retries=max_retries + max_retries=max_retries, + spotify_quality=fall_quality ) print(f"DEBUG: Playlist download completed successfully using Deezer credentials") except Exception as e: diff --git a/static/js/queue.js b/static/js/queue.js index 399a603..83e0001 100644 --- a/static/js/queue.js +++ b/static/js/queue.js @@ -126,7 +126,7 @@ class DownloadQueue { entry.hasEnded = true; // Close SSE connection - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); if (entry.intervalId) { clearInterval(entry.intervalId); @@ -143,7 +143,7 @@ class DownloadQueue { // Close all SSE connections when the page is about to unload window.addEventListener('beforeunload', () => { - this.closeAllSSEConnections(); + this.clearAllPollingIntervals(); }); } @@ -222,7 +222,7 @@ class DownloadQueue { // Only start monitoring if explicitly requested if (startMonitoring && this.isEntryVisible(queueId)) { - this.startEntryMonitoring(queueId); + this.startDownloadStatusMonitoring(queueId); } this.dispatchEvent('downloadAdded', { queueId, item, type }); @@ -230,7 +230,7 @@ class DownloadQueue { } /* Start processing the entry only if it is visible. */ - async startEntryMonitoring(queueId) { + async startDownloadStatusMonitoring(queueId) { const entry = this.queueEntries[queueId]; if (!entry || entry.hasEnded) return; @@ -295,7 +295,7 @@ class DownloadQueue { // If the entry is already in a terminal state, don't set up SSE if (['error', 'complete', 'cancel', 'cancelled', 'done'].includes(data.last_line.status)) { entry.hasEnded = true; - this.handleTerminalState(entry, queueId, data.last_line); + this.handleDownloadCompletion(entry, queueId, data.last_line); return; } } @@ -305,7 +305,7 @@ class DownloadQueue { } // Set up SSE connection for real-time updates - this.setupSSEConnection(queueId); + this.setupPollingInterval(queueId); } /* Helper Methods */ @@ -415,7 +415,8 @@ class DownloadQueue { const defaultMessage = (type === 'playlist') ? 'Reading track list' : 'Initializing download...'; // Use display values if available, or fall back to standard fields - const displayTitle = item.name || 'Unknown'; + // Support both 'name' and 'music' fields which may be used by the backend + const displayTitle = item.name || item.music || item.song || 'Unknown'; const displayType = type.charAt(0).toUpperCase() + type.slice(1); const div = document.createElement('article'); @@ -497,7 +498,7 @@ class DownloadQueue { entry.hasEnded = true; // Close any active connections - this.closeSSEConnection(queueid); + this.clearPollingInterval(queueid); if (entry.intervalId) { clearInterval(entry.intervalId); @@ -668,7 +669,7 @@ class DownloadQueue { const entry = this.queueEntries[queueId]; if (entry) { // Close any SSE connection - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); // Clean up any intervals if (entry.intervalId) { @@ -723,12 +724,16 @@ class DownloadQueue { function pluralize(word) { return word.endsWith('s') ? word : word + 's'; } + + // Extract the track name - check 'music' field first (from backend), then 'song', then 'name' + const trackName = data.music || data.song || data.name || 'Unknown'; + switch (data.status) { case 'queued': if (data.type === 'album' || data.type === 'playlist') { return `Queued ${data.type} "${data.name}"${data.position ? ` (position ${data.position})` : ''}`; } else if (data.type === 'track') { - return `Queued track "${data.name}"${data.artist ? ` by ${data.artist}` : ''}`; + return `Queued track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}`; } return `Queued ${data.type} "${data.name}"`; @@ -746,7 +751,7 @@ class DownloadQueue { case 'downloading': if (data.type === 'track') { - return `Downloading track "${data.song}" by ${data.artist}...`; + return `Downloading track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}...`; } return `Downloading ${data.type}...`; @@ -792,7 +797,7 @@ class DownloadQueue { case 'done': if (data.type === 'track') { - return `Finished track "${data.song || data.name}" by ${data.artist}`; + return `Finished track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}`; } else if (data.type === 'playlist') { return `Finished playlist "${data.name}" with ${data.total_tracks} tracks`; } else if (data.type === 'album') { @@ -804,7 +809,7 @@ class DownloadQueue { case 'complete': if (data.type === 'track') { - return `Finished track "${data.name || data.song}" by ${data.artist}`; + return `Finished track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}`; } else if (data.type === 'playlist') { return `Finished playlist "${data.name}" with ${data.total_tracks || ''} tracks`; } else if (data.type === 'album') { @@ -832,14 +837,14 @@ class DownloadQueue { return errorMsg; case 'skipped': - return `Track "${data.song}" skipped, it already exists!`; + return `Track "${trackName}" skipped, it already exists!`; case 'real_time': { const totalMs = data.time_elapsed; const minutes = Math.floor(totalMs / 60000); const seconds = Math.floor((totalMs % 60000) / 1000); const paddedSeconds = seconds < 10 ? '0' + seconds : seconds; - return `Real-time downloading track "${data.song}" by ${data.artist} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`; + return `Real-time downloading track "${trackName}"${data.artist ? ` by ${data.artist}` : ''} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`; } default: @@ -848,7 +853,7 @@ class DownloadQueue { } /* New Methods to Handle Terminal State, Inactivity and Auto-Retry */ - handleTerminalState(entry, queueId, progress) { + handleDownloadCompletion(entry, queueId, progress) { // Mark the entry as ended entry.hasEnded = true; @@ -863,11 +868,10 @@ class DownloadQueue { } // Stop polling - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); - // For error state, use longer timeout (30 seconds) - const isError = entry.lastStatus && entry.lastStatus.status === 'error'; - const cleanupDelay = isError ? 30000 : 5000; + // Use 10 seconds cleanup delay for all states including errors + const cleanupDelay = 10000; // Clean up after the appropriate delay setTimeout(() => { @@ -885,7 +889,7 @@ class DownloadQueue { const now = Date.now(); if (now - entry.lastUpdated > 300000) { const progress = { status: 'error', message: 'Inactivity timeout' }; - this.handleTerminalState(entry, queueId, progress); + this.handleDownloadCompletion(entry, queueId, progress); } else { if (logElement) { logElement.textContent = this.getStatusMessage(entry.lastStatus); @@ -928,7 +932,7 @@ class DownloadQueue { try { // Close any existing SSE connection - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); console.log(`Retrying download for ${entry.type} with URL: ${retryUrl}`); @@ -981,7 +985,7 @@ class DownloadQueue { } // Set up a new SSE connection for the retried download - this.setupSSEConnection(queueId); + this.setupPollingInterval(queueId); } else { logElement.textContent = 'Retry failed: invalid response from server'; } @@ -999,7 +1003,7 @@ class DownloadQueue { const entry = this.queueEntries[queueId]; // Only start monitoring if the entry is not in a terminal state and is visible if (!entry.hasEnded && this.isEntryVisible(queueId) && !this.sseConnections[queueId]) { - this.setupSSEConnection(queueId); + this.setupPollingInterval(queueId); } } } @@ -1075,7 +1079,7 @@ class DownloadQueue { const entry = this.queueEntries[queueId]; // Only start monitoring if the entry is not in a terminal state if (!entry.hasEnded && !this.sseConnections[queueId]) { - this.setupSSEConnection(queueId); + this.setupPollingInterval(queueId); } } @@ -1084,6 +1088,13 @@ class DownloadQueue { // Check for older API response format else if (data.album_prg_files && Array.isArray(data.album_prg_files)) { console.log(`Queued artist discography with ${data.album_prg_files.length} albums (old format)`); + + // Show a temporary message about the artist download + const artistMessage = document.createElement('div'); + artistMessage.className = 'queue-artist-message'; + artistMessage.textContent = `Queued ${data.album_prg_files.length} albums for ${item.name || 'artist'}. Loading...`; + document.getElementById('queueItems').prepend(artistMessage); + // Add each album to the download queue separately const queueIds = []; data.album_prg_files.forEach(prgFile => { @@ -1095,18 +1106,56 @@ class DownloadQueue { this.toggleVisibility(true); // Wait a short time before setting up SSE connections - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Remove the temporary message + artistMessage.remove(); + + // Fetch the latest tasks to show all newly created album downloads + await this.loadExistingPrgFiles(); // Set up SSE connections for each entry - for (const {queueId, prgFile} of queueIds) { + for (const queueId in this.queueEntries) { const entry = this.queueEntries[queueId]; if (entry && !entry.hasEnded) { - this.setupSSEConnection(queueId); + this.setupPollingInterval(queueId); } } return queueIds.map(({queueId}) => queueId); } + // Handle any other response format for artist downloads + else { + console.log(`Queued artist discography with unknown format:`, data); + + // Show a temporary message + const artistMessage = document.createElement('div'); + artistMessage.className = 'queue-artist-message'; + artistMessage.textContent = `Queued albums for ${item.name || 'artist'}. Loading...`; + document.getElementById('queueItems').prepend(artistMessage); + + // Make queue visible + this.toggleVisibility(true); + + // Wait a moment for tasks to be created on the backend + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Remove the temporary message + artistMessage.remove(); + + // Fetch the latest tasks to show all newly created album downloads + await this.loadExistingPrgFiles(); + + // Start monitoring all entries + for (const queueId in this.queueEntries) { + const entry = this.queueEntries[queueId]; + if (entry && !entry.hasEnded) { + this.setupPollingInterval(queueId); + } + } + + return data; + } } // Handle single-file downloads (tracks, albums, playlists) @@ -1119,7 +1168,7 @@ class DownloadQueue { // Set up SSE connection const entry = this.queueEntries[queueId]; if (entry && !entry.hasEnded) { - this.setupSSEConnection(queueId); + this.setupPollingInterval(queueId); } return queueId; @@ -1141,7 +1190,7 @@ class DownloadQueue { for (const queueId in this.queueEntries) { const entry = this.queueEntries[queueId]; // Close any active connections - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); // Don't remove the entry from DOM - we'll rebuild it entirely delete this.queueEntries[queueId]; @@ -1323,7 +1372,7 @@ class DownloadQueue { } /* Sets up a Server-Sent Events connection for real-time status updates */ - setupSSEConnection(queueId) { + setupPollingInterval(queueId) { console.log(`Setting up polling for ${queueId}`); const entry = this.queueEntries[queueId]; if (!entry || !entry.prgFile) { @@ -1332,15 +1381,15 @@ class DownloadQueue { } // Close any existing connection - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); try { // Immediately fetch initial data - this.fetchTaskStatus(queueId); + this.fetchDownloadStatus(queueId); // Create a polling interval of 1 second const intervalId = setInterval(() => { - this.fetchTaskStatus(queueId); + this.fetchDownloadStatus(queueId); }, 1000); // Store the interval ID for later cleanup @@ -1355,7 +1404,7 @@ class DownloadQueue { } } - async fetchTaskStatus(queueId) { + async fetchDownloadStatus(queueId) { const entry = this.queueEntries[queueId]; if (!entry || !entry.prgFile) { console.warn(`No entry or prgFile for ${queueId}`); @@ -1384,8 +1433,14 @@ class DownloadQueue { } } + // Filter the last_line if it doesn't match the entry's type + if (data.last_line && data.last_line.type && entry.type && data.last_line.type !== entry.type) { + console.log(`Skipping status update with type '${data.last_line.type}' for entry with type '${entry.type}'`); + return; + } + // Process the update - this.handleSSEUpdate(queueId, data); + this.handleStatusUpdate(queueId, data); // Handle terminal states if (data.last_line && ['complete', 'error', 'cancelled', 'done'].includes(data.last_line.status)) { @@ -1393,7 +1448,7 @@ class DownloadQueue { entry.hasEnded = true; setTimeout(() => { - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); this.cleanupEntry(queueId); }, 5000); } @@ -1409,7 +1464,7 @@ class DownloadQueue { } } - closeSSEConnection(queueId) { + clearPollingInterval(queueId) { if (this.sseConnections[queueId]) { console.log(`Stopping polling for ${queueId}`); try { @@ -1423,7 +1478,7 @@ class DownloadQueue { } /* Handle SSE update events */ - handleSSEUpdate(queueId, data) { + handleStatusUpdate(queueId, data) { const entry = this.queueEntries[queueId]; if (!entry) { console.warn(`No entry for ${queueId}`); @@ -1436,6 +1491,12 @@ class DownloadQueue { // Extract the actual status data from the API response const statusData = data.last_line || {}; + + // Skip updates where the type doesn't match the entry's type + if (statusData.type && entry.type && statusData.type !== entry.type) { + return; + } + status = statusData.status || data.event || 'unknown'; // For new polling API structure @@ -1446,6 +1507,16 @@ class DownloadQueue { } else { message = `Status: ${status}`; } + + // Extract trackName from different possible fields + const trackName = statusData.music || statusData.song || statusData.name || 'Unknown'; + if (trackName && trackName !== 'Unknown') { + // Update the title in the queue item if we have a track name + const titleEl = entry.element.querySelector('.title'); + if (titleEl && trackName !== titleEl.textContent) { + titleEl.textContent = trackName; + } + } // Track progress data if (data.progress_percent) { @@ -1538,6 +1609,13 @@ class DownloadQueue { this.retryDownload(queueId, logElement); }); + + // Set up automatic cleanup after 10 seconds + setTimeout(() => { + if (this.queueEntries[queueId] && this.queueEntries[queueId].hasEnded) { + this.cleanupEntry(queueId); + } + }, 10000); } else { // Cannot retry - just show error with close button logElement.innerHTML = ` @@ -1550,6 +1628,13 @@ class DownloadQueue { logElement.querySelector('.close-error-btn').addEventListener('click', () => { this.cleanupEntry(queueId); }); + + // Set up automatic cleanup after 10 seconds + setTimeout(() => { + if (this.queueEntries[queueId] && this.queueEntries[queueId].hasEnded) { + this.cleanupEntry(queueId); + } + }, 10000); } } @@ -1558,12 +1643,15 @@ class DownloadQueue { entry.element.classList.add('error'); // Close SSE connection - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); } else { // For non-error states, update the log element with the latest message const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`); if (logElement && message) { logElement.textContent = message; + } else if (logElement) { + // Generate a message if none provided + logElement.textContent = this.getStatusMessage(statusData); } // Set the proper status classes on the list item @@ -1597,14 +1685,14 @@ class DownloadQueue { // Handle terminal states (except errors which we handle separately above) if (['complete', 'cancelled', 'done'].includes(status)) { - this.handleTerminalState(entry, queueId, progress); + this.handleDownloadCompletion(entry, queueId, progress); } } /* Close all active SSE connections */ - closeAllSSEConnections() { + clearAllPollingIntervals() { for (const queueId in this.sseConnections) { - this.closeSSEConnection(queueId); + this.clearPollingInterval(queueId); } } }