diff --git a/web_server.py b/web_server.py index 4683fceb..64366554 100644 --- a/web_server.py +++ b/web_server.py @@ -3465,6 +3465,67 @@ def get_wishlist_count(): print(f"Error getting wishlist count: {e}") return jsonify({"error": str(e)}), 500 +@app.route('/api/wishlist/tracks', methods=['GET']) +def get_wishlist_tracks(): + """Endpoint to get wishlist tracks for display in modal.""" + try: + from core.wishlist_service import get_wishlist_service + wishlist_service = get_wishlist_service() + tracks = wishlist_service.get_wishlist_tracks_for_download() + return jsonify({"tracks": tracks}) + except Exception as e: + print(f"Error getting wishlist tracks: {e}") + return jsonify({"error": str(e)}), 500 + +@app.route('/api/wishlist/download_missing', methods=['POST']) +def start_wishlist_missing_downloads(): + """ + This endpoint fetches wishlist tracks and manages them with batch processing + identical to playlist processing, maintaining exactly 3 concurrent downloads. + """ + try: + from core.wishlist_service import get_wishlist_service + wishlist_service = get_wishlist_service() + + # Get wishlist tracks formatted for download modal + wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download() + if not wishlist_tracks: + return jsonify({"success": False, "error": "No tracks in wishlist"}), 400 + + batch_id = str(uuid.uuid4()) + + # Use "wishlist" as the playlist_id for consistency in the modal system + playlist_id = "wishlist" + playlist_name = "Wishlist" + + # Create task queue for this batch - convert wishlist tracks to the expected format + task_queue = [] + with tasks_lock: + download_batches[batch_id] = { + 'phase': 'analysis', + 'playlist_id': playlist_id, + 'playlist_name': playlist_name, + 'queue': task_queue, + 'active_count': 0, + 'max_concurrent': 3, + 'queue_index': 0, + 'analysis_total': len(wishlist_tracks), + 'analysis_processed': 0, + 'analysis_results': [] + } + + # Submit the wishlist processing job using the same processing function + missing_download_executor.submit(_run_full_missing_tracks_process, batch_id, playlist_id, wishlist_tracks) + + return jsonify({ + "success": True, + "batch_id": batch_id + }) + + except Exception as e: + print(f"Error starting wishlist download process: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/api/database/update', methods=['POST']) def start_database_update(): """Endpoint to start the database update process.""" @@ -3625,7 +3686,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): found, confidence = False, 0.0 for artist in artists: - artist_name = artist if isinstance(artist, str) else str(artist) + # Handle both string format and Spotify API format {'name': 'Artist Name'} + if isinstance(artist, str): + artist_name = artist + elif isinstance(artist, dict) and 'name' in artist: + artist_name = artist['name'] + else: + artist_name = str(artist) db_track, track_confidence = db.check_track_exists( track_name, artist_name, confidence_threshold=0.7, server_source=active_server ) @@ -3715,11 +3782,31 @@ def _download_track_worker(task_id, batch_id=None): track_data = task['track_info'] # Recreate a SpotifyTrack object for the matching engine + # Handle both string format and Spotify API format for artists + raw_artists = track_data.get('artists', []) + processed_artists = [] + for artist in raw_artists: + if isinstance(artist, str): + processed_artists.append(artist) + elif isinstance(artist, dict) and 'name' in artist: + processed_artists.append(artist['name']) + else: + processed_artists.append(str(artist)) + + # Handle album field - extract name if it's a dictionary + raw_album = track_data.get('album', '') + if isinstance(raw_album, dict) and 'name' in raw_album: + album_name = raw_album['name'] + elif isinstance(raw_album, str): + album_name = raw_album + else: + album_name = str(raw_album) + track = SpotifyTrack( id=track_data.get('id', ''), name=track_data.get('name', ''), - artists=track_data.get('artists', []), - album=track_data.get('album', ''), + artists=processed_artists, + album=album_name, duration_ms=track_data.get('duration_ms', 0), popularity=track_data.get('popularity', 0) ) diff --git a/webui/static/script.js b/webui/static/script.js index bc5e2822..6459bf26 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1782,8 +1782,28 @@ async function checkForActiveProcesses() { async function rehydrateModal(processInfo) { const { playlist_id, playlist_name, batch_id } = processInfo; - console.log(`💧 Rehydrating modal for playlist "${playlist_name}" (batch: ${batch_id})`); + console.log(`💧 Rehydrating modal for "${playlist_name}" (batch: ${batch_id})`); + // Handle wishlist processes specially + if (playlist_id === "wishlist") { + await openDownloadMissingWishlistModal(); + const process = activeDownloadProcesses[playlist_id]; + if (!process) return; + + process.status = 'running'; + process.batchId = batch_id; + updateRefreshButtonState(); + + document.getElementById(`begin-analysis-btn-${playlist_id}`).style.display = 'none'; + document.getElementById(`cancel-all-btn-${playlist_id}`).style.display = 'inline-block'; + + startModalDownloadPolling(playlist_id); + + process.modalElement.style.display = 'none'; + return; + } + + // Handle regular Spotify playlist processes let playlistData = spotifyPlaylists.find(p => p.id === playlist_id); if (!playlistData) { console.warn(`Cannot rehydrate modal: Playlist data for ${playlist_id} not loaded.`); @@ -2296,6 +2316,210 @@ function closeDownloadMissingModal(playlistId) { } } +async function openDownloadMissingWishlistModal() { + const playlistId = "wishlist"; // Use a consistent ID for wishlist + + // Check if a process is already active for the wishlist + if (activeDownloadProcesses[playlistId]) { + console.log(`Modal for wishlist already exists. Showing it.`); + const process = activeDownloadProcesses[playlistId]; + if (process.modalElement) { + // Show helpful message if it's a completed process + if (process.status === 'complete') { + showToast('Showing previous results. Close this modal to start a new analysis.', 'info'); + } + process.modalElement.style.display = 'flex'; + } + return; // Don't create a new one + } + + console.log(`📥 Opening Download Missing Tracks modal for wishlist`); + + // Fetch actual wishlist tracks from the server + let tracks; + try { + const response = await fetch('/api/wishlist/count'); + const countData = await response.json(); + if (countData.count === 0) { + showToast('Wishlist is empty. No tracks to download.', 'info'); + return; + } + + // Fetch the actual wishlist tracks for display + const tracksResponse = await fetch('/api/wishlist/tracks'); + if (!tracksResponse.ok) { + throw new Error('Failed to fetch wishlist tracks'); + } + const tracksData = await tracksResponse.json(); + tracks = tracksData.tracks || []; + + } catch (error) { + showToast(`Failed to fetch wishlist data: ${error.message}`, 'error'); + return; + } + + currentPlaylistTracks = tracks; + currentModalPlaylistId = playlistId; + + let modal = document.createElement('div'); + modal.id = `download-missing-modal-${playlistId}`; // Unique ID + modal.className = 'download-missing-modal'; // Use class for styling + modal.style.display = 'none'; // Start hidden + document.body.appendChild(modal); + + // Register the new process in our global state tracker + activeDownloadProcesses[playlistId] = { + status: 'idle', // idle, running, complete, cancelled + modalElement: modal, + poller: null, + batchId: null, + playlist: { id: playlistId, name: "Wishlist" }, // Create a pseudo-playlist object + tracks: tracks + }; + + modal.innerHTML = ` +
+ `; + + modal.style.display = 'flex'; +} + +async function startWishlistMissingTracksProcess(playlistId) { + const process = activeDownloadProcesses[playlistId]; + if (!process) return; + + console.log(`🚀 Kicking off wishlist missing tracks process`); + try { + process.status = 'running'; + updateRefreshButtonState(); + document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none'; + document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block'; + + const response = await fetch('/api/wishlist/download_missing', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + const data = await response.json(); + if (!data.success) { + // Special handling for rate limit + if (response.status === 429) { + throw new Error(`${data.error} Try closing some other download processes first.`); + } + throw new Error(data.error); + } + + process.batchId = data.batch_id; + console.log(`✅ Wishlist process started successfully. Batch ID: ${data.batch_id}`); + + // Start polling for updates + startModalDownloadPolling(playlistId); + + } catch (error) { + console.error('Error starting wishlist missing tracks process:', error); + showToast(`Error: ${error.message}`, 'error'); + + // Reset UI state on error + process.status = 'idle'; + updateRefreshButtonState(); + document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'inline-block'; + document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none'; + } +} + async function startMissingTracksProcess(playlistId) { const process = activeDownloadProcesses[playlistId]; if (!process) return; @@ -2337,6 +2561,7 @@ async function startMissingTracksProcess(playlistId) { function updateTrackAnalysisResults(playlistId, results) { + // Update match results for all rows (tracks are now pre-populated) for (const result of results) { const matchElement = document.getElementById(`match-${playlistId}-${result.track_index}`); if (matchElement) { @@ -3728,6 +3953,25 @@ function escapeHtml(text) { return div.innerHTML; } +function formatArtists(artists) { + if (!artists || !Array.isArray(artists)) { + return 'Unknown Artist'; + } + + // Handle both string arrays and object arrays with 'name' property + const artistNames = artists.map(artist => { + if (typeof artist === 'string') { + return artist; + } else if (artist && typeof artist === 'object' && artist.name) { + return artist.name; + } else { + return 'Unknown Artist'; + } + }); + + return artistNames.join(', ') || 'Unknown Artist'; +} + async function showVersionInfo() { try { console.log('Fetching version info...'); @@ -4037,6 +4281,15 @@ window.cancelAllOperations = cancelAllOperations; window.cancelTrackDownload = cancelTrackDownload; window.handleViewProgressClick = handleViewProgressClick; +// Wishlist Modal functions +window.openDownloadMissingWishlistModal = openDownloadMissingWishlistModal; +window.startWishlistMissingTracksProcess = startWishlistMissingTracksProcess; +window.handleWishlistButtonClick = handleWishlistButtonClick; + +// Helper functions +window.escapeHtml = escapeHtml; +window.formatArtists = formatArtists; + // APPEND THIS JAVASCRIPT SNIPPET (B) @@ -4970,6 +5223,12 @@ async function loadDashboardData() { updateButton.addEventListener('click', handleDbUpdateButtonClick); } + // Attach event listener for the wishlist button + const wishlistButton = document.getElementById('wishlist-button'); + if (wishlistButton) { + wishlistButton.addEventListener('click', handleWishlistButtonClick); + } + // Initial load of stats await fetchAndUpdateDbStats(); @@ -4986,6 +5245,9 @@ async function loadDashboardData() { // Also check the status of any ongoing update when the page loads await checkAndUpdateDbProgress(); + + // Check for any active download processes that need rehydration + await checkForActiveProcesses(); } // --- Data Fetching and UI Updates --- @@ -5225,4 +5487,24 @@ async function handleDbUpdateButtonClick() { showToast('Error sending stop request.', 'error'); } } +} + +async function handleWishlistButtonClick() { + try { + // First check if there are any tracks in the wishlist + const response = await fetch('/api/wishlist/count'); + const data = await response.json(); + + if (data.count === 0) { + showToast('Wishlist is empty. No tracks to download.', 'info'); + return; + } + + // Open the wishlist download modal + await openDownloadMissingWishlistModal(); + + } catch (error) { + console.error('Error handling wishlist button click:', error); + showToast(`Error opening wishlist: ${error.message}`, 'error'); + } } \ No newline at end of file