diff --git a/web_server.py b/web_server.py index 913b985e..36b336b0 100644 --- a/web_server.py +++ b/web_server.py @@ -4924,10 +4924,13 @@ def start_playlist_missing_downloads(playlist_id): @app.route('/api/active-processes', methods=['GET']) def get_active_processes(): """ - This endpoint now only needs to check for active download batches, - as the analysis phase is now part of the batch process. + Returns all active processes for frontend rehydration: + - Download batch processes (Spotify playlists) + - YouTube discovery/sync processes (non-fresh phases) """ active_processes = [] + + # Add active download batch processes with tasks_lock: for batch_id, batch_data in download_batches.items(): if batch_data.get('phase') not in ['complete', 'error', 'cancelled']: @@ -4938,6 +4941,25 @@ def get_active_processes(): "batch_id": batch_id, "phase": batch_data.get('phase') }) + + # Add YouTube playlists in non-fresh phases for rehydration + for url_hash, state in youtube_playlist_states.items(): + # Include playlists that have progressed beyond fresh phase + if state['phase'] != 'fresh': + active_processes.append({ + "type": "youtube_playlist", + "url_hash": url_hash, + "url": state['url'], + "playlist_name": state['playlist']['name'], + "phase": state['phase'], + "status": state['status'], + "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') + }) + + 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") return jsonify({"active_processes": active_processes}) @app.route('/api/playlists//download_status', methods=['GET']) @@ -5480,8 +5502,8 @@ def get_playlist_tracks(playlist_id): # YOUTUBE PLAYLIST API ENDPOINTS # =================================================================== -# Global state for YouTube discovery processes -youtube_discovery_states = {} +# Global state for YouTube playlist management (persistent across page reloads) +youtube_playlist_states = {} # Key: url_hash, Value: persistent playlist state youtube_discovery_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="youtube_discovery") @app.route('/api/youtube/parse', methods=['POST']) @@ -5509,16 +5531,22 @@ def parse_youtube_playlist_endpoint(): # Create URL hash for state tracking url_hash = str(hash(url)) - # Initialize discovery state - youtube_discovery_states[url_hash] = { + # Initialize persistent playlist state (similar to Spotify download_batches structure) + youtube_playlist_states[url_hash] = { 'playlist': playlist_data, - 'phase': 'fresh', + 'phase': 'fresh', # fresh -> discovering -> discovered -> syncing -> sync_complete -> downloading -> download_complete 'discovery_results': [], 'discovery_progress': 0, 'spotify_matches': 0, 'spotify_total': len(playlist_data['tracks']), 'status': 'parsed', - 'url': url + 'url': url, + 'sync_playlist_id': None, + 'converted_spotify_playlist_id': None, + 'created_at': time.time(), + 'last_accessed': time.time(), + 'discovery_future': None, + 'sync_progress': {} } playlist_data['url_hash'] = url_hash @@ -5534,10 +5562,11 @@ def parse_youtube_playlist_endpoint(): def start_youtube_discovery(url_hash): """Start Spotify discovery process for a YouTube playlist""" try: - if url_hash not in youtube_discovery_states: + if url_hash not in youtube_playlist_states: return jsonify({"error": "YouTube playlist not found"}), 404 - state = youtube_discovery_states[url_hash] + state = youtube_playlist_states[url_hash] + state['last_accessed'] = time.time() # Update access time if state['phase'] == 'discovering': return jsonify({"error": "Discovery already in progress"}), 400 @@ -5563,10 +5592,11 @@ def start_youtube_discovery(url_hash): def get_youtube_discovery_status(url_hash): """Get real-time discovery status for a YouTube playlist""" try: - if url_hash not in youtube_discovery_states: + if url_hash not in youtube_playlist_states: return jsonify({"error": "YouTube playlist not found"}), 404 - state = youtube_discovery_states[url_hash] + state = youtube_playlist_states[url_hash] + state['last_accessed'] = time.time() # Update access time response = { 'phase': state['phase'], @@ -5588,7 +5618,7 @@ def get_youtube_discovery_status(url_hash): def _run_youtube_discovery_worker(url_hash): """Background worker for YouTube Spotify discovery process""" try: - state = youtube_discovery_states[url_hash] + state = youtube_playlist_states[url_hash] playlist = state['playlist'] tracks = playlist['tracks'] @@ -5735,10 +5765,11 @@ def _calculate_similarity(str1, str2): def start_youtube_sync(url_hash): """Start sync process for a YouTube playlist using discovered Spotify tracks""" try: - if url_hash not in youtube_discovery_states: + if url_hash not in youtube_playlist_states: return jsonify({"error": "YouTube playlist not found"}), 404 - state = youtube_discovery_states[url_hash] + state = youtube_playlist_states[url_hash] + state['last_accessed'] = time.time() # Update access time if state['phase'] not in ['discovered', 'sync_complete']: return jsonify({"error": "YouTube playlist not ready for sync"}), 400 @@ -5783,10 +5814,11 @@ def start_youtube_sync(url_hash): def get_youtube_sync_status(url_hash): """Get sync status for a YouTube playlist""" try: - if url_hash not in youtube_discovery_states: + if url_hash not in youtube_playlist_states: return jsonify({"error": "YouTube playlist not found"}), 404 - state = youtube_discovery_states[url_hash] + state = youtube_playlist_states[url_hash] + state['last_accessed'] = time.time() # Update access time sync_playlist_id = state.get('sync_playlist_id') if not sync_playlist_id: @@ -5821,10 +5853,11 @@ def get_youtube_sync_status(url_hash): def cancel_youtube_sync(url_hash): """Cancel sync for a YouTube playlist""" try: - if url_hash not in youtube_discovery_states: + if url_hash not in youtube_playlist_states: return jsonify({"error": "YouTube playlist not found"}), 404 - state = youtube_discovery_states[url_hash] + state = youtube_playlist_states[url_hash] + state['last_accessed'] = time.time() # Update access time sync_playlist_id = state.get('sync_playlist_id') if sync_playlist_id: @@ -5847,6 +5880,131 @@ def cancel_youtube_sync(url_hash): print(f"❌ Error cancelling YouTube sync: {e}") return jsonify({"error": str(e)}), 500 +# New YouTube Playlist Management Endpoints (for persistent state) + +@app.route('/api/youtube/playlists', methods=['GET']) +def get_all_youtube_playlists(): + """Get all stored YouTube playlists for frontend hydration (similar to Spotify playlists)""" + try: + playlists = [] + current_time = time.time() + + for url_hash, state in youtube_playlist_states.items(): + # Update access time when requested + state['last_accessed'] = current_time + + # Return essential data for card recreation + playlist_info = { + 'url_hash': url_hash, + 'url': state['url'], + 'playlist': state['playlist'], + 'phase': state['phase'], + 'status': state['status'], + 'discovery_progress': state['discovery_progress'], + 'spotify_matches': state['spotify_matches'], + 'spotify_total': state['spotify_total'], + 'created_at': state['created_at'], + 'last_accessed': state['last_accessed'] + } + playlists.append(playlist_info) + + print(f"📋 Returning {len(playlists)} stored YouTube playlists for hydration") + return jsonify({"playlists": playlists}) + + except Exception as e: + print(f"❌ Error getting YouTube playlists: {e}") + return jsonify({"error": str(e)}), 500 + +@app.route('/api/youtube/state/', methods=['GET']) +def get_youtube_playlist_state(url_hash): + """Get specific YouTube playlist state (detailed version of status endpoint)""" + try: + if url_hash not in youtube_playlist_states: + return jsonify({"error": "YouTube playlist not found"}), 404 + + state = youtube_playlist_states[url_hash] + state['last_accessed'] = time.time() + + # Return full state information (including results for modal hydration) + response = { + 'url_hash': url_hash, + 'url': state['url'], + 'playlist': state['playlist'], + 'phase': state['phase'], + 'status': state['status'], + 'discovery_progress': state['discovery_progress'], + 'spotify_matches': state['spotify_matches'], + 'spotify_total': state['spotify_total'], + 'discovery_results': state['discovery_results'], + 'sync_playlist_id': state['sync_playlist_id'], + 'converted_spotify_playlist_id': state['converted_spotify_playlist_id'], + 'sync_progress': state['sync_progress'], + 'created_at': state['created_at'], + 'last_accessed': state['last_accessed'] + } + + return jsonify(response) + + except Exception as e: + print(f"❌ Error getting YouTube playlist state: {e}") + return jsonify({"error": str(e)}), 500 + +@app.route('/api/youtube/reset/', methods=['POST']) +def reset_youtube_playlist(url_hash): + """Reset YouTube playlist to fresh phase (clear discovery/sync data)""" + try: + if url_hash not in youtube_playlist_states: + return jsonify({"error": "YouTube playlist not found"}), 404 + + state = youtube_playlist_states[url_hash] + + # Stop any active discovery + if 'discovery_future' in state and state['discovery_future']: + state['discovery_future'].cancel() + + # Reset state to fresh (preserve original playlist data) + state['phase'] = 'fresh' + state['status'] = 'parsed' + state['discovery_results'] = [] + state['discovery_progress'] = 0 + state['spotify_matches'] = 0 + state['sync_playlist_id'] = None + state['converted_spotify_playlist_id'] = None + state['sync_progress'] = {} + state['discovery_future'] = None + state['last_accessed'] = time.time() + + print(f"🔄 Reset YouTube playlist to fresh phase: {state['playlist']['name']}") + return jsonify({"success": True, "message": "Playlist reset to fresh state"}) + + except Exception as e: + print(f"❌ Error resetting YouTube playlist: {e}") + return jsonify({"error": str(e)}), 500 + +@app.route('/api/youtube/delete/', methods=['DELETE']) +def delete_youtube_playlist(url_hash): + """Remove YouTube playlist from backend storage entirely""" + try: + if url_hash not in youtube_playlist_states: + return jsonify({"error": "YouTube playlist not found"}), 404 + + state = youtube_playlist_states[url_hash] + + # Stop any active discovery + if 'discovery_future' in state and state['discovery_future']: + state['discovery_future'].cancel() + + # Remove from storage + playlist_name = state['playlist']['name'] + del youtube_playlist_states[url_hash] + + print(f"đŸ—‘ī¸ Deleted YouTube playlist from backend: {playlist_name}") + return jsonify({"success": True, "message": f"Playlist '{playlist_name}' deleted"}) + + except Exception as e: + print(f"❌ Error deleting YouTube playlist: {e}") + return jsonify({"error": str(e)}), 500 + def convert_youtube_results_to_spotify_tracks(discovery_results): """Convert YouTube discovery results to Spotify tracks format for sync""" spotify_tracks = [] diff --git a/webui/static/script.js b/webui/static/script.js index 5b772d90..9dc0fb9b 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1761,6 +1761,9 @@ async function loadSyncData() { if (!spotifyPlaylistsLoaded) { await loadSpotifyPlaylists(); } + + // Load YouTube playlists from backend (always refresh to get latest state) + await loadYouTubePlaylistsFromBackend(); } async function checkForActiveProcesses() { @@ -1773,11 +1776,24 @@ async function checkForActiveProcesses() { if (processes.length > 0) { console.log(`🔄 Found ${processes.length} active process(es) from backend. Rehydrating UI...`); - for (const processInfo of processes) { + + // Separate download batch processes from YouTube playlist processes + const downloadProcesses = processes.filter(p => p.type === 'batch'); + const youtubeProcesses = processes.filter(p => p.type === 'youtube_playlist'); + + console.log(`📊 Process breakdown: ${downloadProcesses.length} download batches, ${youtubeProcesses.length} YouTube playlists`); + + // Rehydrate download modal processes (existing Spotify system) + for (const processInfo of downloadProcesses) { if (!activeDownloadProcesses[processInfo.playlist_id]) { rehydrateModal(processInfo); } } + + // 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. + console.log(`â„šī¸ Skipping ${youtubeProcesses.length} YouTube playlists - handled by full backend loading`); } } catch (error) { console.error('Failed to check for active processes:', error); @@ -1846,6 +1862,360 @@ async function rehydrateModal(processInfo, userRequested = false) { process.modalElement.style.display = 'none'; } +// =================================================================== +// YOUTUBE PLAYLIST BACKEND HYDRATION FUNCTIONS +// =================================================================== + +async function loadYouTubePlaylistsFromBackend() { + // Load all stored YouTube playlists from backend and recreate cards (similar to Spotify hydration) + try { + console.log('📋 Loading YouTube playlists from backend...'); + + const response = await fetch('/api/youtube/playlists'); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to fetch YouTube playlists'); + } + + const data = await response.json(); + const playlists = data.playlists || []; + + console.log(`đŸŽŦ Found ${playlists.length} stored YouTube playlists in backend`); + + if (playlists.length === 0) { + console.log('📋 No YouTube playlists to hydrate'); + return; + } + + const container = document.getElementById('youtube-playlist-container'); + + // Create cards for playlists that don't already exist (avoid duplicates) + for (const playlistInfo of playlists) { + const urlHash = playlistInfo.url_hash; + + // Check if card already exists (from rehydration or previous loading) + if (youtubePlaylistStates[urlHash] && youtubePlaylistStates[urlHash].cardElement && + document.body.contains(youtubePlaylistStates[urlHash].cardElement)) { + console.log(`â­ī¸ Skipping existing YouTube playlist card: ${playlistInfo.playlist.name}`); + + // Update existing state with backend data + const state = youtubePlaylistStates[urlHash]; + state.phase = playlistInfo.phase; + state.discoveryProgress = playlistInfo.discovery_progress; + state.spotifyMatches = playlistInfo.spotify_matches; + state.convertedSpotifyPlaylistId = playlistInfo.converted_spotify_playlist_id; + + // Fetch discovery results for existing cards too if they don't have them + if (playlistInfo.phase !== 'fresh' && playlistInfo.phase !== 'discovering' && + (!state.discoveryResults || state.discoveryResults.length === 0)) { + try { + console.log(`🔍 Fetching missing discovery results for existing card: ${playlistInfo.playlist.name}`); + const stateResponse = await fetch(`/api/youtube/state/${urlHash}`); + if (stateResponse.ok) { + const fullState = await stateResponse.json(); + if (fullState.discovery_results) { + state.discoveryResults = fullState.discovery_results; + state.syncPlaylistId = fullState.sync_playlist_id; + state.syncProgress = fullState.sync_progress || {}; + console.log(`✅ Restored ${state.discoveryResults.length} discovery results for existing card`); + } + } + } catch (error) { + console.warn(`âš ī¸ Error fetching discovery results for existing card:`, error.message); + } + } + + continue; + } + + console.log(`đŸŽŦ Creating YouTube playlist card: ${playlistInfo.playlist.name} (Phase: ${playlistInfo.phase})`); + createYouTubeCardFromBackendState(playlistInfo); + + // Fetch discovery results for non-fresh playlists (same logic as rehydrateYouTubePlaylist) + if (playlistInfo.phase !== 'fresh' && playlistInfo.phase !== 'discovering') { + try { + console.log(`🔍 Fetching discovery results for: ${playlistInfo.playlist.name}`); + const stateResponse = await fetch(`/api/youtube/state/${urlHash}`); + if (stateResponse.ok) { + const fullState = await stateResponse.json(); + console.log(`📋 Retrieved full state with ${fullState.discovery_results?.length || 0} discovery results`); + + // Store discovery results in local state + const state = youtubePlaylistStates[urlHash]; + if (fullState.discovery_results && state) { + state.discoveryResults = fullState.discovery_results; + state.syncPlaylistId = fullState.sync_playlist_id; + state.syncProgress = fullState.sync_progress || {}; + console.log(`✅ Restored ${state.discoveryResults.length} discovery results for: ${playlistInfo.playlist.name}`); + } + } else { + console.warn(`âš ī¸ Could not fetch discovery results for: ${playlistInfo.playlist.name}`); + } + } catch (error) { + console.warn(`âš ī¸ Error fetching discovery results for ${playlistInfo.playlist.name}:`, error.message); + } + } + } + + console.log(`✅ Successfully hydrated ${playlists.length} YouTube playlists from backend`); + + } catch (error) { + console.error('❌ Error loading YouTube playlists from backend:', error); + showToast(`Error loading YouTube playlists: ${error.message}`, 'error'); + } +} + +function createYouTubeCardFromBackendState(playlistInfo) { + // Create YouTube playlist card from backend state data + const urlHash = playlistInfo.url_hash; + const playlist = playlistInfo.playlist; + const phase = playlistInfo.phase; + + const container = document.getElementById('youtube-playlist-container'); + + // Remove placeholder if it exists + const placeholder = container.querySelector('.youtube-playlist-placeholder'); + if (placeholder) { + placeholder.remove(); + } + + // Create card HTML (using EXACT same structure as createYouTubeCard) + const cardHtml = ` +
+
â–ļ
+
+
${escapeHtml(playlist.name)}
+
+ ${playlist.tracks.length} tracks + ${getPhaseText(phase)} +
+
+
+ â™Ē ${playlistInfo.spotify_total} / ✓ ${playlistInfo.spotify_matches} / ✗ ${playlistInfo.spotify_total - playlistInfo.spotify_matches} / ${Math.round(getProgressWidth(playlistInfo))}% +
+ +
+ `; + + container.insertAdjacentHTML('beforeend', cardHtml); + + // Store state for UI management (but backend remains source of truth) + youtubePlaylistStates[urlHash] = { + phase: phase, + url: playlistInfo.url, + playlist: playlist, + cardElement: document.getElementById(`youtube-card-${urlHash}`), + discoveryResults: [], + discoveryProgress: playlistInfo.discovery_progress, + spotifyMatches: playlistInfo.spotify_matches, + convertedSpotifyPlaylistId: playlistInfo.converted_spotify_playlist_id, + backendSynced: true // Flag to indicate this came from backend + }; + + console.log(`🃏 Created YouTube card from backend state: ${playlist.name} (${phase})`); +} + +function getActionButtonText(phase) { + switch (phase) { + case 'fresh': return 'Discover'; + case 'discovering': return 'View Progress'; + case 'discovered': return 'View Results'; + case 'syncing': return 'View Sync'; + case 'sync_complete': return 'Download'; + case 'downloading': return 'View Downloads'; + case 'download_complete': return 'Complete'; + default: return 'Open'; + } +} + +function getPhaseText(phase) { + switch (phase) { + case 'fresh': return 'Ready to discover'; + case 'discovering': return 'Discovering...'; + case 'discovered': return 'Discovery Complete'; + case 'syncing': return 'Syncing...'; + case 'sync_complete': return 'Sync Complete'; + case 'downloading': return 'Downloading...'; + case 'download_complete': return 'Download Complete'; + default: return phase; + } +} + +function getPhaseColor(phase) { + switch (phase) { + case 'fresh': return '#999'; + case 'discovering': case 'syncing': case 'downloading': return '#ffa500'; + case 'discovered': case 'sync_complete': case 'download_complete': return '#1db954'; + default: return '#999'; + } +} + +function getProgressWidth(playlistInfo) { + if (playlistInfo.phase === 'fresh') return 0; + if (playlistInfo.spotify_total === 0) return 0; + return Math.round((playlistInfo.spotify_matches / playlistInfo.spotify_total) * 100); +} + +async function rehydrateYouTubePlaylist(playlistInfo, userRequested = false) { + // Rehydrate a YouTube playlist's discovery modal state (similar to rehydrateModal) + const urlHash = playlistInfo.url_hash; + const playlistName = playlistInfo.playlist_name; + const phase = playlistInfo.phase; + + console.log(`💧 Rehydrating YouTube playlist "${playlistName}" (Phase: ${phase}) - User requested: ${userRequested}`); + + try { + // First, ensure the card exists (create from backend if needed) + if (!youtubePlaylistStates[urlHash] || !youtubePlaylistStates[urlHash].cardElement) { + console.log(`🃏 Creating missing YouTube card for rehydration: ${playlistName}`); + + // Since playlistInfo from active processes doesn't have full playlist data, + // we need to fetch it from the backend first + try { + const stateResponse = await fetch(`/api/youtube/state/${urlHash}`); + if (stateResponse.ok) { + const fullPlaylistState = await stateResponse.json(); + createYouTubeCardFromBackendState(fullPlaylistState); + } else { + console.error(`❌ Could not fetch full playlist state for card creation: ${playlistName}`); + return; // Can't create card without playlist data + } + } catch (error) { + console.error(`❌ Error fetching playlist state for card creation: ${error.message}`); + return; + } + } + + // Fetch full state from backend to get discovery results + let fullState = null; + if (phase !== 'fresh' && phase !== 'discovering') { + try { + console.log(`🔍 Fetching full backend state for: ${playlistName}`); + const stateResponse = await fetch(`/api/youtube/state/${urlHash}`); + if (stateResponse.ok) { + fullState = await stateResponse.json(); + console.log(`📋 Retrieved full state with ${fullState.discovery_results?.length || 0} discovery results`); + } + } catch (error) { + console.warn(`âš ī¸ Could not fetch full state for ${playlistName}:`, error.message); + } + } + + // Update local state to match backend + const state = youtubePlaylistStates[urlHash]; + state.phase = phase; + state.discoveryProgress = playlistInfo.discovery_progress; + state.spotifyMatches = playlistInfo.spotify_matches; + state.convertedSpotifyPlaylistId = playlistInfo.converted_spotify_playlist_id; + + // Restore discovery results if we have them + if (fullState && fullState.discovery_results) { + state.discoveryResults = fullState.discovery_results; + state.syncPlaylistId = fullState.sync_playlist_id; + state.syncProgress = fullState.sync_progress || {}; + console.log(`✅ Restored ${state.discoveryResults.length} discovery results from backend`); + + // Update modal if it already exists + const existingModal = document.getElementById(`youtube-discovery-modal-${urlHash}`); + if (existingModal && !existingModal.classList.contains('hidden')) { + console.log(`🔄 Refreshing existing modal with restored discovery results`); + refreshYouTubeDiscoveryModalTable(urlHash); + } + } + + // Update card display + updateYouTubeCardPhase(urlHash, phase); + updateYouTubeCardProgress(urlHash, playlistInfo); + + // Handle active discovery polling + if (phase === 'discovering') { + console.log(`🔍 Resuming discovery polling for: ${playlistName}`); + startYouTubeDiscoveryPolling(urlHash); + } + + // Open modal if user requested + if (userRequested) { + switch (phase) { + case 'discovering': + case 'discovered': + case 'syncing': + case 'sync_complete': + openYouTubeDiscoveryModal(urlHash); + break; + case 'downloading': + case 'download_complete': + // Open download modal if we have the converted playlist ID + if (playlistInfo.converted_spotify_playlist_id) { + await openDownloadMissingModal(playlistInfo.converted_spotify_playlist_id); + } + break; + } + } + + console.log(`✅ Successfully rehydrated YouTube playlist: ${playlistName}`); + + } catch (error) { + console.error(`❌ Error rehydrating YouTube playlist "${playlistName}":`, error); + } +} + +async function removeYouTubePlaylistFromBackend(event, urlHash) { + // Remove YouTube playlist from backend storage and update UI + event.stopPropagation(); // Prevent card click + + const state = youtubePlaylistStates[urlHash]; + if (!state) return; + + const playlistName = state.playlist.name; + + try { + console.log(`đŸ—‘ī¸ Removing YouTube playlist from backend: ${playlistName}`); + + const response = await fetch(`/api/youtube/delete/${urlHash}`, { + method: 'DELETE' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to delete playlist'); + } + + // Remove card from UI + if (state.cardElement) { + state.cardElement.remove(); + } + + // Remove from client state + delete youtubePlaylistStates[urlHash]; + + // Stop any active polling + if (activeYouTubePollers[urlHash]) { + clearInterval(activeYouTubePollers[urlHash]); + delete activeYouTubePollers[urlHash]; + } + + // Close discovery modal if open + const modal = document.getElementById(`youtube-discovery-modal-${urlHash}`); + if (modal) { + modal.remove(); + } + + // Show placeholder if no cards left + const container = document.getElementById('youtube-playlist-container'); + const cards = container.querySelectorAll('.youtube-playlist-card'); + if (cards.length === 0) { + container.innerHTML = '
No YouTube playlists added yet. Parse a YouTube playlist URL above to get started!
'; + } + + showToast(`Removed "${playlistName}" from backend storage`, 'success'); + console.log(`✅ Successfully removed YouTube playlist: ${playlistName}`); + + } catch (error) { + console.error(`❌ Error removing YouTube playlist "${playlistName}":`, error); + showToast(`Error removing playlist: ${error.message}`, 'error'); + } +} + async function loadSpotifyPlaylists() { const container = document.getElementById('spotify-playlist-container'); const refreshBtn = document.getElementById('spotify-refresh-btn'); @@ -6265,7 +6635,7 @@ function openYouTubeDiscoveryModal(urlHash) {