diff --git a/web_server.py b/web_server.py index c450035b..20f535a0 100644 --- a/web_server.py +++ b/web_server.py @@ -4390,6 +4390,13 @@ def _on_download_completed(batch_id, task_id, success=True): youtube_playlist_states[url_hash]['phase'] = 'download_complete' print(f"📋 Updated YouTube playlist {url_hash} to download_complete phase") + # Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist + if playlist_id and playlist_id.startswith('tidal_'): + tidal_playlist_id = playlist_id.replace('tidal_', '') + if tidal_playlist_id in tidal_discovery_states: + tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete' + print(f"📋 Updated Tidal playlist {tidal_playlist_id} to download_complete phase") + print(f"🎉 [Batch Manager] Batch {batch_id} complete - stopping monitor") download_monitor.stop_monitoring(batch_id) @@ -4474,6 +4481,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): 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)") + + # Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist + if playlist_id.startswith('tidal_'): + tidal_playlist_id = playlist_id.replace('tidal_', '') + if tidal_playlist_id in tidal_discovery_states: + tidal_discovery_states[tidal_playlist_id]['phase'] = 'download_complete' + print(f"📋 Updated Tidal playlist {tidal_playlist_id} to download_complete phase (no missing tracks)") return print(f" transitioning batch {batch_id} to download phase with {len(missing_tracks)} tracks.") @@ -5335,6 +5349,15 @@ def start_missing_tracks_process(playlist_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})") + + # Link Tidal playlist to download process if this is a Tidal playlist + if playlist_id.startswith('tidal_'): + tidal_playlist_id = playlist_id.replace('tidal_', '') + if tidal_playlist_id in tidal_discovery_states: + tidal_discovery_states[tidal_playlist_id]['download_process_id'] = batch_id + tidal_discovery_states[tidal_playlist_id]['phase'] = 'downloading' + tidal_discovery_states[tidal_playlist_id]['converted_spotify_playlist_id'] = playlist_id + print(f"🔗 Linked Tidal playlist {tidal_playlist_id} to download process {batch_id} (converted ID: {playlist_id})") missing_download_executor.submit(_run_full_missing_tracks_process, batch_id, playlist_id, tracks) @@ -5763,6 +5786,8 @@ def get_tidal_playlist_states(): 'spotify_matches': state['spotify_matches'], 'spotify_total': state['spotify_total'], 'discovery_results': state['discovery_results'], + 'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'), + 'download_process_id': state.get('download_process_id'), 'last_accessed': state['last_accessed'] } states.append(state_info) @@ -5794,6 +5819,10 @@ def get_tidal_playlist_state(playlist_id): 'spotify_matches': state['spotify_matches'], 'spotify_total': state['spotify_total'], 'discovery_results': state['discovery_results'], + 'sync_playlist_id': state.get('sync_playlist_id'), + 'converted_spotify_playlist_id': state.get('converted_spotify_playlist_id'), + 'download_process_id': state.get('download_process_id'), + 'sync_progress': state.get('sync_progress', {}), 'last_accessed': state['last_accessed'] } diff --git a/webui/static/script.js b/webui/static/script.js index c0ed4400..0b81f177 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -3000,6 +3000,12 @@ function closeDownloadMissingModal(playlistId) { updateYouTubeCardPhase(urlHash, 'discovered'); } + // Reset Tidal playlist phase to 'discovered' when modal is closed after completion + if (playlistId.startsWith('tidal_')) { + const tidalPlaylistId = playlistId.replace('tidal_', ''); + updateTidalCardPhase(tidalPlaylistId, 'discovered'); + } + // Clear wishlist modal state when modal is fully closed if (playlistId === 'wishlist') { WishlistModalState.clear(); // Clear all tracking since modal is fully closed @@ -3234,6 +3240,16 @@ async function startMissingTracksProcess(playlistId) { const urlHash = playlistId.replace('youtube_', ''); updateYouTubeCardPhase(urlHash, 'downloading'); } + + // Update Tidal playlist phase to 'downloading' if this is a Tidal playlist + if (playlistId.startsWith('tidal_')) { + const tidalPlaylistId = playlistId.replace('tidal_', ''); + if (tidalPlaylistStates[tidalPlaylistId]) { + tidalPlaylistStates[tidalPlaylistId].phase = 'downloading'; + updateTidalCardPhase(tidalPlaylistId, 'downloading'); + console.log(`🔄 Updated Tidal playlist ${tidalPlaylistId} to downloading phase`); + } + } document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none'; document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block'; @@ -3419,6 +3435,16 @@ function startModalDownloadPolling(playlistId) { updateYouTubeCardPhase(urlHash, 'download_complete'); } + // Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist + if (playlistId.startsWith('tidal_')) { + const tidalPlaylistId = playlistId.replace('tidal_', ''); + if (tidalPlaylistStates[tidalPlaylistId]) { + tidalPlaylistStates[tidalPlaylistId].phase = 'download_complete'; + updateTidalCardPhase(tidalPlaylistId, 'download_complete'); + console.log(`✅ Updated Tidal playlist ${tidalPlaylistId} to download_complete phase`); + } + } + // Handle background wishlist processing completion specially if (isBackgroundWishlist) { console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${failedOrCancelledCount} failed`); @@ -6435,6 +6461,99 @@ async function handleTidalCardClick(playlistId) { } else if (state.phase === 'discovering' || state.phase === 'discovered' || state.phase === 'syncing' || state.phase === 'sync_complete') { // Reopen existing modal (like sync.py) openTidalDiscoveryModal(playlistId, state.playlist); + } else if (state.phase === 'downloading' || state.phase === 'download_complete') { + // Open download modal if we have the converted playlist ID + if (state.convertedSpotifyPlaylistId) { + console.log(`🔍 Opening download modal for Tidal playlist: ${state.playlist.name}`); + // Check if modal already exists, if not create it + if (activeDownloadProcesses[state.convertedSpotifyPlaylistId]) { + const process = activeDownloadProcesses[state.convertedSpotifyPlaylistId]; + if (process.modalElement) { + process.modalElement.style.display = 'flex'; + } + } else { + // Need to create the download modal - fetch the discovery results + await rehydrateTidalDownloadModal(playlistId, state); + } + } else { + console.error('❌ No converted Spotify playlist ID found for Tidal download modal'); + showToast('Unable to open download modal - missing playlist data', 'error'); + } + } +} + +async function rehydrateTidalDownloadModal(playlistId, state) { + try { + console.log(`💧 Rehydrating Tidal download modal for: ${state.playlist.name}`); + + // Get discovery results from backend if not already loaded + if (!state.discovery_results) { + console.log(`🔍 Fetching discovery results from backend for Tidal playlist: ${playlistId}`); + const stateResponse = await fetch(`/api/tidal/state/${playlistId}`); + if (stateResponse.ok) { + const fullState = await stateResponse.json(); + state.discovery_results = fullState.discovery_results; + state.convertedSpotifyPlaylistId = fullState.converted_spotify_playlist_id; + state.download_process_id = fullState.download_process_id; + console.log(`✅ Loaded ${fullState.discovery_results?.length || 0} discovery results from backend`); + } else { + console.error('❌ Failed to fetch Tidal discovery results from backend'); + showToast('Error loading playlist data', 'error'); + return; + } + } + + // Extract Spotify tracks from discovery results + const spotifyTracks = []; + for (const result of state.discovery_results) { + if (result.spotify_data) { + spotifyTracks.push(result.spotify_data); + } + } + + if (spotifyTracks.length === 0) { + console.error('❌ No Spotify tracks found for download modal'); + showToast('No Spotify matches found for download', 'error'); + return; + } + + const virtualPlaylistId = state.convertedSpotifyPlaylistId; + const playlistName = `[Tidal] ${state.playlist.name}`; + + // Create the download modal + await openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, spotifyTracks); + + // If we have a download process ID, set up the modal for the running state + if (state.download_process_id) { + const process = activeDownloadProcesses[virtualPlaylistId]; + if (process) { + process.status = state.phase === 'download_complete' ? 'complete' : 'running'; + process.batchId = state.download_process_id; + + // Update UI based on phase + const beginBtn = document.getElementById(`begin-analysis-btn-${virtualPlaylistId}`); + const cancelBtn = document.getElementById(`cancel-all-btn-${virtualPlaylistId}`); + + if (state.phase === 'downloading') { + if (beginBtn) beginBtn.style.display = 'none'; + if (cancelBtn) cancelBtn.style.display = 'inline-block'; + + // Start polling for live updates + startModalDownloadPolling(virtualPlaylistId); + console.log(`🔄 Started polling for active Tidal download: ${state.download_process_id}`); + } else if (state.phase === 'download_complete') { + if (beginBtn) beginBtn.style.display = 'none'; + if (cancelBtn) cancelBtn.style.display = 'none'; + console.log(`✅ Showing completed Tidal download results: ${state.download_process_id}`); + } + } + } + + console.log(`✅ Successfully rehydrated Tidal download modal for: ${state.playlist.name}`); + + } catch (error) { + console.error(`❌ Error rehydrating Tidal download modal:`, error); + showToast('Error opening download modal', 'error'); } } @@ -6679,6 +6798,62 @@ async function loadTidalPlaylistStatesFromBackend() { await applyTidalPlaylistState(stateInfo); } + // Rehydrate download modals for Tidal playlists in downloading/download_complete phases + for (const stateInfo of states) { + if ((stateInfo.phase === 'downloading' || stateInfo.phase === 'download_complete') && + stateInfo.converted_spotify_playlist_id && stateInfo.download_process_id) { + + const convertedPlaylistId = stateInfo.converted_spotify_playlist_id; + + if (!activeDownloadProcesses[convertedPlaylistId]) { + console.log(`💧 Rehydrating download modal for Tidal playlist: ${stateInfo.playlist_id}`); + try { + // Get the playlist data + const playlistData = tidalPlaylists.find(p => p.id === stateInfo.playlist_id); + if (!playlistData) { + console.warn(`⚠️ Playlist data not found for rehydration: ${stateInfo.playlist_id}`); + continue; + } + + // Create the download modal using the Tidal-specific function + const spotifyTracks = tidalPlaylistStates[stateInfo.playlist_id]?.discovery_results + ?.filter(result => result.spotify_data) + ?.map(result => result.spotify_data) || []; + + if (spotifyTracks.length > 0) { + await openDownloadMissingModalForTidal( + convertedPlaylistId, + `[Tidal] ${playlistData.name}`, + spotifyTracks + ); + + // Set the modal to running state with the correct batch ID + const process = activeDownloadProcesses[convertedPlaylistId]; + if (process) { + process.status = 'running'; + process.batchId = stateInfo.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); + + console.log(`✅ Rehydrated Tidal download modal for batch ${stateInfo.download_process_id}`); + } + } else { + console.warn(`⚠️ No Spotify tracks found for Tidal playlist rehydration: ${stateInfo.playlist_id}`); + } + } catch (error) { + console.error(`❌ Error rehydrating Tidal download modal for ${stateInfo.playlist_id}:`, error); + } + } + } + } + console.log('✅ Tidal playlist states loaded and applied'); } catch (error) { @@ -6687,7 +6862,7 @@ async function loadTidalPlaylistStatesFromBackend() { } async function applyTidalPlaylistState(stateInfo) { - const { playlist_id, phase, discovery_progress, spotify_matches, discovery_results } = stateInfo; + const { playlist_id, phase, discovery_progress, spotify_matches, discovery_results, converted_spotify_playlist_id, download_process_id } = stateInfo; try { console.log(`🎵 Applying saved state for Tidal playlist: ${playlist_id}, Phase: ${phase}`); @@ -6713,6 +6888,8 @@ async function applyTidalPlaylistState(stateInfo) { tidalPlaylistStates[playlist_id].discovery_progress = discovery_progress; tidalPlaylistStates[playlist_id].spotify_matches = spotify_matches; tidalPlaylistStates[playlist_id].discovery_results = discovery_results; + tidalPlaylistStates[playlist_id].convertedSpotifyPlaylistId = converted_spotify_playlist_id; + tidalPlaylistStates[playlist_id].download_process_id = download_process_id; tidalPlaylistStates[playlist_id].playlist = playlistData; // Ensure playlist data is set // Fetch full discovery results for non-fresh playlists (matching YouTube pattern) @@ -6729,6 +6906,8 @@ async function applyTidalPlaylistState(stateInfo) { tidalPlaylistStates[playlist_id].discovery_results = fullState.discovery_results; tidalPlaylistStates[playlist_id].discovery_progress = fullState.discovery_progress; tidalPlaylistStates[playlist_id].spotify_matches = fullState.spotify_matches; + tidalPlaylistStates[playlist_id].convertedSpotifyPlaylistId = fullState.converted_spotify_playlist_id; + tidalPlaylistStates[playlist_id].download_process_id = fullState.download_process_id; console.log(`✅ Restored ${fullState.discovery_results.length} discovery results for Tidal playlist: ${playlistData.name}`); } } else { @@ -7017,17 +7196,209 @@ function updateTidalModalButtons(urlHash, phase) { } } -function startTidalDownloadMissing(urlHash) { - console.log(`🎵 Starting Tidal download missing tracks for: ${urlHash}`); - const state = youtubePlaylistStates[urlHash]; - if (!state || !state.is_tidal_playlist) { - console.error('❌ Invalid Tidal playlist state for download'); +async function startTidalDownloadMissing(urlHash) { + try { + console.log('🔍 Starting download missing tracks for Tidal playlist:', urlHash); + + const state = youtubePlaylistStates[urlHash]; + if (!state || !state.is_tidal_playlist) { + console.error('❌ Invalid Tidal playlist state for download'); + return; + } + + // Get the actual Tidal playlist ID + const tidalPlaylistId = state.tidal_playlist_id; + const tidalState = tidalPlaylistStates[tidalPlaylistId]; + + if (!tidalState || !tidalState.discovery_results) { + showToast('No discovery results available for download', 'error'); + return; + } + + // Convert Tidal discovery results to Spotify tracks format (same as YouTube) + const spotifyTracks = []; + for (const result of tidalState.discovery_results) { + if (result.spotify_data) { + spotifyTracks.push(result.spotify_data); + } + } + + if (spotifyTracks.length === 0) { + showToast('No Spotify matches found for download', 'error'); + return; + } + + // Create a virtual playlist for the download system + const virtualPlaylistId = `tidal_${tidalPlaylistId}`; + const playlistName = `[Tidal] ${tidalState.playlist.name}`; + + // Store reference for card navigation (same as YouTube) + tidalState.convertedSpotifyPlaylistId = virtualPlaylistId; + state.convertedSpotifyPlaylistId = virtualPlaylistId; + + // Close the discovery modal if it's open (same as YouTube) + const discoveryModal = document.getElementById(`youtube-discovery-modal-${urlHash}`); + if (discoveryModal) { + discoveryModal.style.display = 'none'; + console.log('🔄 Closed Tidal discovery modal to show download modal'); + } + + // Open download missing tracks modal for Tidal playlist + await openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, spotifyTracks); + + // Phase will change to 'downloading' when user clicks "Begin Analysis" button + + } catch (error) { + console.error('❌ Error starting download missing tracks:', error); + showToast(`Error starting downloads: ${error.message}`, 'error'); + } +} + +async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, spotifyTracks) { + // Check if a process is already active for this virtual playlist + if (activeDownloadProcesses[virtualPlaylistId]) { + console.log(`Modal for ${virtualPlaylistId} already exists. Showing it.`); + const process = activeDownloadProcesses[virtualPlaylistId]; + if (process.modalElement) { + if (process.status === 'complete') { + showToast('Showing previous results. Close this modal to start a new analysis.', 'info'); + } + process.modalElement.style.display = 'flex'; + } return; } + + console.log(`📥 Opening Download Missing Tracks modal for Tidal playlist: ${virtualPlaylistId}`); - // TODO: Implement Tidal download missing tracks logic - // For now, show a message that this feature is coming soon - showToast('🔍 Tidal download missing tracks functionality coming soon!', 'info'); + // Create virtual playlist object for compatibility with existing modal logic + const virtualPlaylist = { + id: virtualPlaylistId, + name: playlistName, + track_count: spotifyTracks.length + }; + + // Store the tracks in the cache for the modal to use + playlistTrackCache[virtualPlaylistId] = spotifyTracks; + currentPlaylistTracks = spotifyTracks; + currentModalPlaylistId = virtualPlaylistId; + + let modal = document.createElement('div'); + modal.id = `download-missing-modal-${virtualPlaylistId}`; + modal.className = 'download-missing-modal'; + modal.style.display = 'none'; + document.body.appendChild(modal); + + // Register the new process in our global state tracker using the same structure as Spotify + activeDownloadProcesses[virtualPlaylistId] = { + status: 'idle', + modalElement: modal, + poller: null, + batchId: null, + playlist: virtualPlaylist, + tracks: spotifyTracks + }; + + // Use the exact same modal HTML structure as the existing Spotify modal + modal.innerHTML = ` +
+
+

Download Missing Tracks - ${escapeHtml(playlistName)}

+ × +
+ +
+
+
+
${spotifyTracks.length}
+
Total Tracks
+
+
+
-
+
Found in Library
+
+
+
-
+
Missing Tracks
+
+
+
0
+
Downloaded
+
+
+ +
+
+
+ 🔍 Library Analysis + Ready to start +
+
+
+
+
+
+
+ ⏬ Downloads + Waiting for analysis +
+
+
+
+
+
+ +
+
+

📋 Track Analysis & Download Status

+
+
+ + + + + + + + + + + + + + ${spotifyTracks.map((track, index) => ` + + + + + + + + + + `).join('')} + +
#TrackArtistDurationLibrary MatchDownload StatusActions
${index + 1}${escapeHtml(track.name)}${track.artists.join(', ')}${formatDuration(track.duration_ms)}🔍 Pending--
+
+
+
+ + +
+ `; + + modal.style.display = 'flex'; }