diff --git a/web_server.py b/web_server.py index 36b336b0..bbd1c0ca 100644 --- a/web_server.py +++ b/web_server.py @@ -4382,6 +4382,14 @@ def _on_download_completed(batch_id, task_id, success=True): # Mark batch as complete and process wishlist outside of lock to prevent deadlocks batch['phase'] = 'complete' + # Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist + playlist_id = batch.get('playlist_id') + if playlist_id and playlist_id.startswith('youtube_'): + url_hash = playlist_id.replace('youtube_', '') + if url_hash in youtube_playlist_states: + youtube_playlist_states[url_hash]['phase'] = 'download_complete' + print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase") + print(f"🎉 [Batch Manager] Batch {batch_id} complete - stopping monitor") download_monitor.stop_monitoring(batch_id) @@ -4459,6 +4467,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): with tasks_lock: if batch_id in download_batches: download_batches[batch_id]['phase'] = 'complete' + + # Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist + if playlist_id.startswith('youtube_'): + url_hash = playlist_id.replace('youtube_', '') + if url_hash in youtube_playlist_states: + youtube_playlist_states[url_hash]['phase'] = 'download_complete' + print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase (no missing tracks)") return print(f" transitioning batch {batch_id} to download phase with {len(missing_tracks)} tracks.") @@ -4490,6 +4505,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): if batch_id in download_batches: download_batches[batch_id]['phase'] = 'error' download_batches[batch_id]['error'] = str(e) + + # Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on error + if playlist_id.startswith('youtube_'): + url_hash = playlist_id.replace('youtube_', '') + if url_hash in youtube_playlist_states: + youtube_playlist_states[url_hash]['phase'] = 'discovered' + print(f"📋 Reset YouTube playlist {url_hash} to discovered phase (error)") def _download_track_worker(task_id, batch_id=None): """ @@ -4956,7 +4978,8 @@ def get_active_processes(): "discovery_progress": state['discovery_progress'], "spotify_matches": state['spotify_matches'], "spotify_total": state['spotify_total'], - "converted_spotify_playlist_id": state.get('converted_spotify_playlist_id') + "converted_spotify_playlist_id": state.get('converted_spotify_playlist_id'), + "download_process_id": state.get('download_process_id') # batch_id for download modal rehydration }) print(f"📊 Active processes check: {len([p for p in active_processes if p['type'] == 'batch'])} download batches, {len([p for p in active_processes if p['type'] == 'youtube_playlist'])} YouTube playlists") @@ -5196,6 +5219,14 @@ def cancel_batch(batch_id): # Mark batch as cancelled download_batches[batch_id]['phase'] = 'cancelled' + # Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist + playlist_id = download_batches[batch_id].get('playlist_id') + if playlist_id and playlist_id.startswith('youtube_'): + url_hash = playlist_id.replace('youtube_', '') + if url_hash in youtube_playlist_states: + youtube_playlist_states[url_hash]['phase'] = 'discovered' + print(f"📋 Reset YouTube playlist {url_hash} to discovered phase (batch cancelled)") + # Cancel all individual tasks in the batch cancelled_count = 0 for task_id in download_batches[batch_id].get('queue', []): @@ -5296,6 +5327,15 @@ def start_missing_tracks_process(playlist_id): 'analysis_results': [] } + # Link YouTube playlist to download process if this is a YouTube playlist + if playlist_id.startswith('youtube_'): + url_hash = playlist_id.replace('youtube_', '') + if url_hash in youtube_playlist_states: + youtube_playlist_states[url_hash]['download_process_id'] = batch_id + youtube_playlist_states[url_hash]['phase'] = 'downloading' + youtube_playlist_states[url_hash]['converted_spotify_playlist_id'] = playlist_id + print(f"🔗 Linked YouTube playlist {url_hash} to download process {batch_id} (converted ID: {playlist_id})") + missing_download_executor.submit(_run_full_missing_tracks_process, batch_id, playlist_id, tracks) return jsonify({ @@ -5543,6 +5583,7 @@ def parse_youtube_playlist_endpoint(): 'url': url, 'sync_playlist_id': None, 'converted_spotify_playlist_id': None, + 'download_process_id': None, # Track associated download missing tracks process 'created_at': time.time(), 'last_accessed': time.time(), 'discovery_future': None, @@ -5903,6 +5944,8 @@ def get_all_youtube_playlists(): 'discovery_progress': state['discovery_progress'], 'spotify_matches': state['spotify_matches'], 'spotify_total': state['spotify_total'], + 'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'), + 'download_process_id': state.get('download_process_id'), 'created_at': state['created_at'], 'last_accessed': state['last_accessed'] } diff --git a/webui/static/script.js b/webui/static/script.js index e88ac5df..9e2ad2b3 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1790,9 +1790,8 @@ async function checkForActiveProcesses() { } } - // Note: YouTube playlists are now handled by loadYouTubePlaylistsFromBackend() - // in loadSyncData(), which provides more complete data than active processes. - // Skip YouTube rehydration here to avoid conflicts and duplicate cards. + // Note: YouTube playlists are handled by loadYouTubePlaylistsFromBackend() and rehydrateYouTubePlaylist() + // in loadSyncData(), which provides more complete data than active processes and handles download modal rehydration. console.log(`â„šī¸ Skipping ${youtubeProcesses.length} YouTube playlists - handled by full backend loading`); } } catch (error) { @@ -1804,6 +1803,12 @@ async function rehydrateModal(processInfo, userRequested = false) { const { playlist_id, playlist_name, batch_id } = processInfo; console.log(`💧 Rehydrating modal for "${playlist_name}" (batch: ${batch_id}) - User requested: ${userRequested}`); + // Handle YouTube virtual playlists - skip rehydration here, handled by YouTube system + if (playlist_id.startsWith('youtube_')) { + console.log(`â­ī¸ Skipping YouTube virtual playlist rehydration - handled by YouTube system`); + return; + } + // Handle wishlist processes specially if (playlist_id === "wishlist") { console.log(`🔍 Current activeDownloadProcesses keys: [${Object.keys(activeDownloadProcesses).join(', ')}]`); @@ -1957,6 +1962,57 @@ async function loadYouTubePlaylistsFromBackend() { } } + // Rehydrate download modals for YouTube playlists in downloading/download_complete phases + for (const playlistInfo of playlists) { + if ((playlistInfo.phase === 'downloading' || playlistInfo.phase === 'download_complete') && + playlistInfo.converted_spotify_playlist_id && playlistInfo.download_process_id) { + + const convertedPlaylistId = playlistInfo.converted_spotify_playlist_id; + + if (!activeDownloadProcesses[convertedPlaylistId]) { + console.log(`💧 Rehydrating download modal for YouTube playlist: ${playlistInfo.playlist.name}`); + try { + // Create the download modal using the YouTube-specific function + const spotifyTracks = youtubePlaylistStates[playlistInfo.url_hash]?.discoveryResults + ?.filter(result => result.spotify_data) + ?.map(result => result.spotify_data) || []; + + if (spotifyTracks.length > 0) { + await openDownloadMissingModalForYouTube( + convertedPlaylistId, + playlistInfo.playlist.name, + spotifyTracks + ); + + // Set the modal to running state with the correct batch ID + const process = activeDownloadProcesses[convertedPlaylistId]; + if (process) { + process.status = 'running'; + process.batchId = playlistInfo.download_process_id; + + // Update UI to running state + const beginBtn = document.getElementById(`begin-analysis-btn-${convertedPlaylistId}`); + const cancelBtn = document.getElementById(`cancel-all-btn-${convertedPlaylistId}`); + if (beginBtn) beginBtn.style.display = 'none'; + if (cancelBtn) cancelBtn.style.display = 'inline-block'; + + // Start polling for this process + startModalDownloadPolling(convertedPlaylistId); + + // Hide modal since this is background rehydration + process.modalElement.style.display = 'none'; + console.log(`✅ Rehydrated download modal for YouTube playlist: ${playlistInfo.playlist.name}`); + } + } else { + console.warn(`âš ī¸ No Spotify tracks found for YouTube download modal: ${playlistInfo.playlist.name}`); + } + } catch (error) { + console.error(`❌ Error rehydrating download modal for ${playlistInfo.playlist.name}:`, error); + } + } + } + } + console.log(`✅ Successfully hydrated ${playlists.length} YouTube playlists from backend`); } catch (error) { @@ -2878,6 +2934,13 @@ function closeDownloadMissingModal(playlistId) { process.modalElement.style.display = 'none'; } else { console.log(`Closing and cleaning up download modal for playlist ${playlistId}.`); + + // Reset YouTube playlist phase to 'discovered' when modal is closed after completion + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'discovered'); + } + cleanupDownloadProcess(playlistId); } } @@ -3098,6 +3161,12 @@ async function startMissingTracksProcess(playlistId) { process.status = 'running'; updatePlaylistCardUI(playlistId); updateRefreshButtonState(); + + // Update YouTube playlist phase to 'downloading' if this is a YouTube playlist + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'downloading'); + } document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none'; document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block'; @@ -3235,15 +3304,35 @@ function startModalDownloadPolling(playlistId) { if (data.phase === 'cancelled') { process.status = 'cancelled'; + + // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on cancel + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'discovered'); + } + showToast(`Process cancelled for ${process.playlist.name}.`, 'info'); } else if (data.phase === 'error') { process.status = 'complete'; // Treat as complete to allow cleanup updatePlaylistCardUI(playlistId); // Update card to show ready for review + + // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on error + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'discovered'); + } + showToast(`Process for ${process.playlist.name} failed!`, 'error'); } else { process.status = 'complete'; updatePlaylistCardUI(playlistId); // Update card to show ready for review + // Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'download_complete'); + } + // Handle background wishlist processing completion specially if (isBackgroundWishlist) { console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${failedOrCancelledCount} failed`); @@ -6574,7 +6663,39 @@ function handleYouTubeCardClick(urlHash) { // Need to get playlist ID from converted Spotify data const spotifyPlaylistId = state.convertedSpotifyPlaylistId; if (spotifyPlaylistId) { - openDownloadMissingModal(spotifyPlaylistId); + // Check if we have discovery results, if not load them first + if (!state.discoveryResults || state.discoveryResults.length === 0) { + console.log('🔍 Loading discovery results for download modal...'); + fetch(`/api/youtube/state/${urlHash}`) + .then(response => response.json()) + .then(fullState => { + if (fullState.discovery_results) { + state.discoveryResults = fullState.discovery_results; + console.log(`✅ Loaded ${state.discoveryResults.length} discovery results`); + + // Now open the modal with the loaded data + const playlistName = `[YouTube] ${state.playlist.name}`; + const spotifyTracks = state.discoveryResults + .filter(result => result.spotify_data) + .map(result => result.spotify_data); + openDownloadMissingModalForYouTube(spotifyPlaylistId, playlistName, spotifyTracks); + } else { + console.error('❌ No discovery results found for downloads'); + showToast('Unable to open download modal - no discovery data', 'error'); + } + }) + .catch(error => { + console.error('❌ Error loading discovery results:', error); + showToast('Error loading playlist data', 'error'); + }); + } else { + // Use the YouTube-specific function to maintain proper state linking + const playlistName = `[YouTube] ${state.playlist.name}`; + const spotifyTracks = state.discoveryResults + .filter(result => result.spotify_data) + .map(result => result.spotify_data); + openDownloadMissingModalForYouTube(spotifyPlaylistId, playlistName, spotifyTracks); + } } else { console.error('❌ No converted Spotify playlist ID found for downloads'); showToast('Unable to open download modal - missing playlist data', 'error'); @@ -7256,8 +7377,7 @@ async function startYouTubeDownloadMissing(urlHash) { // Open download missing tracks modal for YouTube playlist await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks); - // Update YouTube card phase when download process starts - updateYouTubeCardPhase(urlHash, 'downloading'); + // Phase will change to 'downloading' when user clicks "Begin Analysis" button } catch (error) { console.error('❌ Error starting download missing tracks:', error);