diff --git a/web_server.py b/web_server.py index 5b902112..280135f4 100644 --- a/web_server.py +++ b/web_server.py @@ -504,611 +504,16 @@ def start_sync(): @app.route('/api/search', methods=['POST']) def search_music(): - """ - Perform real Soulseek search using the actual soulseek_client. - Returns progressive search results matching the GUI's SearchThread implementation. - """ - if not soulseek_client: - return jsonify({"error": "Soulseek client not initialized"}), 500 - + # Placeholder: simulates a music search data = request.get_json() - query = data.get('query', '').strip() - - if not query: - return jsonify({"error": "No search query provided"}), 400 - - print(f"🔍 Starting Soulseek search for: '{query}'") - - try: - import asyncio - - # Create new event loop for this request - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - try: - # Perform the actual search using soulseek_client - results = loop.run_until_complete(soulseek_client.search(query)) - - # Process results to match frontend expectations - if isinstance(results, tuple) and len(results) == 2: - tracks, albums = results - else: - # Fallback for backward compatibility - tracks = results if isinstance(results, list) else [] - albums = [] - - # Convert track results to JSON-serializable format - tracks_json = [] - for track in tracks: - tracks_json.append({ - "type": "track", - "title": getattr(track, 'title', 'Unknown Title'), - "artist": getattr(track, 'artist', 'Unknown Artist'), - "album": getattr(track, 'album', 'Unknown Album'), - "quality": getattr(track, 'quality', 'Unknown'), - "bitrate": getattr(track, 'bitrate', None), - "duration": getattr(track, 'duration', None), - "filename": getattr(track, 'filename', ''), - "username": getattr(track, 'username', ''), - "file_size": getattr(track, 'file_size', 0), - "search_result_data": { - # Store the original object data for download purposes - "filename": getattr(track, 'filename', ''), - "username": getattr(track, 'username', ''), - "file_size": getattr(track, 'file_size', 0), - } - }) - - # Convert album results to JSON-serializable format - albums_json = [] - for album in albums: - albums_json.append({ - "type": "album", - "title": getattr(album, 'album_name', getattr(album, 'title', 'Unknown Album')), - "artist": getattr(album, 'artist', 'Unknown Artist'), - "track_count": getattr(album, 'track_count', 0), - "username": getattr(album, 'username', ''), - "size_mb": getattr(album, 'total_size', 0) / (1024 * 1024) if hasattr(album, 'total_size') else 0, - "tracks": getattr(album, 'tracks', []), - "search_result_data": { - # Store the original object data for download purposes - "album_name": getattr(album, 'album_name', getattr(album, 'title', '')), - "artist": getattr(album, 'artist', ''), - "username": getattr(album, 'username', ''), - "tracks": getattr(album, 'tracks', []) - } - }) - - total_results = len(tracks_json) + len(albums_json) - print(f"✅ Search completed: {len(tracks_json)} tracks, {len(albums_json)} albums ({total_results} total)") - - return jsonify({ - "success": True, - "results": { - "tracks": tracks_json, - "albums": albums_json, - "total_tracks": len(tracks_json), - "total_albums": len(albums_json), - "query": query - } - }) - - finally: - # Clean up the event loop - try: - pending = asyncio.all_tasks(loop) - for task in pending: - task.cancel() - if pending: - loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) - loop.close() - except Exception as e: - print(f"Error cleaning up search event loop: {e}") - - except Exception as e: - import traceback - traceback.print_exc() - print(f"❌ Search failed: {e}") - return jsonify({"error": f"Search failed: {str(e)}"}), 500 - -@app.route('/api/search/cancel', methods=['POST']) -def cancel_search(): - """Cancel any active search operations""" - # Note: In a full implementation, you would track active search operations - # and cancel them here. For now, this is a placeholder. - print("🛑 Search cancellation requested") - return jsonify({"success": True, "message": "Search cancellation requested"}) - -# Global download tracking -active_downloads = {} # Dict to track active downloads -completed_downloads = [] # List to store completed downloads - -@app.route('/api/downloads/start', methods=['POST']) -def start_download(): - """ - Start a regular download using the soulseek_client. - This matches the GUI's start_download functionality. - """ - if not soulseek_client: - return jsonify({"error": "Soulseek client not initialized"}), 500 - - data = request.get_json() - if not data: - return jsonify({"error": "No download data provided"}), 400 - - try: - # Extract search result data - search_data = data.get('search_result_data', data) - filename = search_data.get('filename') - username = search_data.get('username') - - if not filename or not username: - return jsonify({"error": "Missing required download parameters (filename, username)"}), 400 - - print(f"⬇️ Starting download: '{filename}' from '{username}'") - - # Create download item for tracking - download_id = f"{username}_{filename}_{len(active_downloads)}" - download_item = { - "id": download_id, - "title": data.get('title', filename), - "artist": data.get('artist', 'Unknown Artist'), - "filename": filename, - "username": username, - "status": "queued", - "progress": 0, - "file_size": data.get('file_size', 0), - "download_speed": 0, - "eta": None, - "start_time": None, - "spotify_matched": False - } - - active_downloads[download_id] = download_item - - # Start the actual download using asyncio - import asyncio - import threading - - def download_worker(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - # This would call the actual soulseek_client.download method - # For now, we'll simulate the download process - result = loop.run_until_complete(simulate_download(download_item)) - print(f"✅ Download completed: {download_id}") - except Exception as e: - print(f"❌ Download failed: {download_id} - {e}") - download_item["status"] = "failed" - download_item["error"] = str(e) - finally: - loop.close() - - # Start download in background thread - thread = threading.Thread(target=download_worker) - thread.daemon = True - thread.start() - - return jsonify({ - "success": True, - "download_id": download_id, - "message": f"Download started for '{filename}'" - }) - - except Exception as e: - import traceback - traceback.print_exc() - return jsonify({"error": f"Failed to start download: {str(e)}"}), 500 - -@app.route('/api/downloads/start-matched', methods=['POST']) -def start_matched_download(): - """ - Start a download with confirmed Spotify match data. - This matches the GUI's start_matched_download functionality. - """ - if not soulseek_client: - return jsonify({"error": "Soulseek client not initialized"}), 500 - - data = request.get_json() - if not data: - return jsonify({"error": "No download data provided"}), 400 - - try: - # Extract search result data and Spotify match data - search_data = data.get('search_result_data', data) - spotify_match = data.get('spotify_match', {}) - - filename = search_data.get('filename') - username = search_data.get('username') - - if not filename or not username: - return jsonify({"error": "Missing required download parameters"}), 400 - - matched_artist = spotify_match.get('artist', {}) - matched_album = spotify_match.get('album', {}) - - print(f"⬇️🎵 Starting matched download: '{filename}' from '{username}'") - print(f" 🎤 Matched Artist: {matched_artist.get('name', 'Unknown')}") - print(f" 💿 Matched Album: {matched_album.get('name', 'Unknown')}") - - # Create download item for tracking with Spotify match info - download_id = f"{username}_{filename}_{len(active_downloads)}_matched" - download_item = { - "id": download_id, - "title": data.get('title', filename), - "artist": data.get('artist', 'Unknown Artist'), - "filename": filename, - "username": username, - "status": "queued", - "progress": 0, - "file_size": data.get('file_size', 0), - "download_speed": 0, - "eta": None, - "start_time": None, - "spotify_matched": True, - "matched_artist": matched_artist, - "matched_album": matched_album - } - - active_downloads[download_id] = download_item - - # Start the actual download using asyncio - import asyncio - import threading - - def matched_download_worker(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - # This would call the actual soulseek_client.download method - # and then apply metadata enhancement with the Spotify match - result = loop.run_until_complete(simulate_matched_download(download_item)) - print(f"✅ Matched download completed: {download_id}") - except Exception as e: - print(f"❌ Matched download failed: {download_id} - {e}") - download_item["status"] = "failed" - download_item["error"] = str(e) - finally: - loop.close() - - # Start download in background thread - thread = threading.Thread(target=matched_download_worker) - thread.daemon = True - thread.start() - - return jsonify({ - "success": True, - "download_id": download_id, - "message": f"Matched download started for '{filename}'" - }) - - except Exception as e: - import traceback - traceback.print_exc() - return jsonify({"error": f"Failed to start matched download: {str(e)}"}), 500 - -@app.route('/api/downloads/status', methods=['GET']) -def get_download_status(): - """ - Get the current status of all downloads (active and completed). - This matches the GUI's download queue functionality. - """ - try: - # Get real download status from soulseek_client if available - real_downloads = [] - if soulseek_client: - try: - import asyncio - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - # This would call soulseek_client.get_all_downloads() - # For now, we'll use our tracked downloads - pass - finally: - loop.close() - except Exception as e: - print(f"Error getting real download status: {e}") - - # Separate active and completed downloads - active = [] - completed = [] - - for download_id, download in active_downloads.items(): - if download["status"] in ["downloading", "queued"]: - active.append(download) - else: - completed.append(download) - - # Add any completed downloads from our completed list - completed.extend(completed_downloads) - - return jsonify({ - "success": True, - "downloads": { - "active": active, - "completed": completed, - "active_count": len(active), - "completed_count": len(completed) - } - }) - - except Exception as e: - print(f"Error getting download status: {e}") - return jsonify({"error": f"Failed to get download status: {str(e)}"}), 500 - -@app.route('/api/downloads/cancel/', methods=['POST']) -def cancel_download(download_id): - """Cancel a specific download""" - if download_id in active_downloads: - download = active_downloads[download_id] - download["status"] = "cancelled" - print(f"🛑 Download cancelled: {download_id}") - return jsonify({"success": True, "message": f"Download {download_id} cancelled"}) - else: - return jsonify({"error": "Download not found"}), 404 - -@app.route('/api/downloads/clear-completed', methods=['POST']) -def clear_completed_downloads(): - """Clear all completed downloads from the queue""" - global completed_downloads, active_downloads - - # Remove completed downloads from active_downloads - to_remove = [did for did, download in active_downloads.items() - if download["status"] in ["completed", "failed", "cancelled"]] - - for download_id in to_remove: - del active_downloads[download_id] - - # Clear completed downloads list - cleared_count = len(completed_downloads) - completed_downloads.clear() - - print(f"🗑️ Cleared {cleared_count + len(to_remove)} completed downloads") - return jsonify({ - "success": True, - "message": f"Cleared {cleared_count + len(to_remove)} completed downloads" - }) - -# Helper functions for simulating downloads (replace with real implementations) -async def simulate_download(download_item): - """Simulate a download process - replace with real soulseek_client.download()""" - import asyncio - import time - - download_item["status"] = "downloading" - download_item["start_time"] = time.time() - - # Simulate download progress - for progress in range(0, 101, 10): - download_item["progress"] = progress - download_item["download_speed"] = 1024 * 1024 # 1 MB/s simulation - await asyncio.sleep(0.1) # Simulate time - - if download_item["status"] == "cancelled": - return False - - download_item["status"] = "completed" - download_item["progress"] = 100 - - # Move to completed downloads - global completed_downloads - completed_downloads.append(download_item.copy()) - - return True - -async def simulate_matched_download(download_item): - """Simulate a matched download with metadata enhancement""" - # First do the regular download - result = await simulate_download(download_item) - - if result and download_item.get("spotify_matched"): - print(f"🎵 Applying metadata enhancement for: {download_item['title']}") - # Here you would apply the Spotify metadata enhancement - # using the matched_artist and matched_album data - download_item["metadata_enhanced"] = True - - return result - -# ===== SPOTIFY INTEGRATION ENDPOINTS ===== - -@app.route('/api/spotify/search-artist', methods=['POST']) -def spotify_search_artist(): - """ - Search for artists using Spotify API for the matching modal. - This matches the GUI's ArtistSearchThread functionality. - """ - if not spotify_client or not spotify_client.is_authenticated(): - return jsonify({"error": "Spotify client not available or not authenticated"}), 500 - - data = request.get_json() - query = data.get('query', '').strip() - - if not query: - return jsonify({"error": "No search query provided"}), 400 - - try: - print(f"🎵 Searching Spotify for artist: '{query}'") - - # Perform artist search using spotify_client - artists = spotify_client.search_artists(query, limit=6) # Limit to 6 for modal display - - # Convert artists to JSON format matching frontend expectations - artists_json = [] - for artist in artists: - artist_data = { - "id": artist.id, - "name": artist.name, - "genres": getattr(artist, 'genres', []), - "popularity": getattr(artist, 'popularity', 0), - "follower_count": getattr(artist, 'follower_count', 0), - "image_url": getattr(artist, 'image_url', None), - "spotify_url": getattr(artist, 'spotify_url', None), - } - artists_json.append(artist_data) - - print(f"✅ Found {len(artists_json)} artists for '{query}'") - return jsonify({ - "success": True, - "artists": artists_json, - "query": query - }) - - except Exception as e: - import traceback - traceback.print_exc() - print(f"❌ Spotify artist search failed: {e}") - return jsonify({"error": f"Artist search failed: {str(e)}"}), 500 - -@app.route('/api/spotify/search-album', methods=['POST']) -def spotify_search_album(): - """ - Search for albums by a specific artist using Spotify API. - This matches the GUI's AlbumSearchThread functionality. - """ - if not spotify_client or not spotify_client.is_authenticated(): - return jsonify({"error": "Spotify client not available or not authenticated"}), 500 - - data = request.get_json() - artist_id = data.get('artist_id') - query = data.get('query', '').strip() - - if not artist_id: - return jsonify({"error": "No artist ID provided"}), 400 - - try: - print(f"💿 Searching albums for artist ID: {artist_id}") - - # Get albums by artist using spotify_client - albums = spotify_client.get_artist_albums(artist_id, limit=10) - - # If query is provided, filter albums by query - if query: - filtered_albums = [] - query_lower = query.lower() - for album in albums: - if query_lower in album.name.lower(): - filtered_albums.append(album) - albums = filtered_albums - - # Convert albums to JSON format - albums_json = [] - for album in albums: - album_data = { - "id": album.id, - "name": album.name, - "release_date": getattr(album, 'release_date', ''), - "total_tracks": getattr(album, 'total_tracks', 0), - "album_type": getattr(album, 'album_type', 'album'), - "image_url": getattr(album, 'image_url', None), - "spotify_url": getattr(album, 'spotify_url', None), - "artist": { - "id": artist_id, - "name": getattr(album, 'artist_name', 'Unknown Artist') - } - } - albums_json.append(album_data) - - print(f"✅ Found {len(albums_json)} albums for artist {artist_id}") - return jsonify({ - "success": True, - "albums": albums_json, - "artist_id": artist_id, - "query": query - }) - - except Exception as e: - import traceback - traceback.print_exc() - print(f"❌ Spotify album search failed: {e}") - return jsonify({"error": f"Album search failed: {str(e)}"}), 500 - -@app.route('/api/spotify/suggestions', methods=['POST']) -def spotify_generate_suggestions(): - """ - Generate artist suggestions for a search result using Spotify API. - This matches the GUI's generate_auto_artist_suggestions functionality. - """ - if not spotify_client or not spotify_client.is_authenticated(): - return jsonify({"error": "Spotify client not available or not authenticated"}), 500 - - data = request.get_json() - original_title = data.get('title', '').strip() - original_artist = data.get('artist', '').strip() - - if not original_title and not original_artist: - return jsonify({"error": "No title or artist provided for suggestions"}), 400 - - try: - print(f"🎯 Generating Spotify suggestions for: '{original_title}' by '{original_artist}'") - - suggestions = [] - - # Strategy 1: Search by artist name if available - if original_artist and original_artist.lower() != 'unknown artist': - try: - artist_results = spotify_client.search_artists(original_artist, limit=3) - suggestions.extend(artist_results) - print(f" Found {len(artist_results)} artist matches") - except Exception as e: - print(f" Artist search failed: {e}") - - # Strategy 2: Search by track title to find artist - if original_title and len(suggestions) < 3: - try: - track_results = spotify_client.search_tracks(original_title, limit=5) - for track in track_results: - if hasattr(track, 'artist') and track.artist not in [s for s in suggestions]: - suggestions.append(track.artist) - if len(suggestions) >= 6: # Limit to 6 total suggestions - break - print(f" Found {len(suggestions)} total suggestions from track search") - except Exception as e: - print(f" Track search for suggestions failed: {e}") - - # Strategy 3: Combined search if we still need more - if len(suggestions) < 3 and original_artist and original_title: - try: - combined_query = f"{original_artist} {original_title}" - combined_results = spotify_client.search_artists(combined_query, limit=3) - suggestions.extend(combined_results) - print(f" Added {len(combined_results)} from combined search") - except Exception as e: - print(f" Combined search failed: {e}") - - # Remove duplicates and convert to JSON - seen_ids = set() - unique_suggestions = [] - for artist in suggestions[:6]: # Limit to 6 suggestions - if artist.id not in seen_ids: - seen_ids.add(artist.id) - artist_data = { - "id": artist.id, - "name": artist.name, - "genres": getattr(artist, 'genres', []), - "popularity": getattr(artist, 'popularity', 0), - "follower_count": getattr(artist, 'follower_count', 0), - "image_url": getattr(artist, 'image_url', None), - "confidence_score": 0.8 if artist.name.lower() == original_artist.lower() else 0.6, - "match_reason": "Direct name match" if artist.name.lower() == original_artist.lower() else "Related artist" - } - unique_suggestions.append(artist_data) - - print(f"✅ Generated {len(unique_suggestions)} unique suggestions") - return jsonify({ - "success": True, - "suggestions": unique_suggestions, - "original_title": original_title, - "original_artist": original_artist - }) - - except Exception as e: - import traceback - traceback.print_exc() - print(f"❌ Spotify suggestions failed: {e}") - return jsonify({"error": f"Failed to generate suggestions: {str(e)}"}), 500 + query = data.get('query', '') + print(f"Simulating search for: {query}") + # In a real implementation, you would call soulseek_client.search() + mock_results = [ + {"title": "Bohemian Rhapsody", "artist": "Queen", "album": "A Night at the Opera", "type": "track", "quality": "FLAC", "username": "user1", "filename": "Queen - Bohemian Rhapsody.flac", "file_size": 35000000}, + {"title": "A Night at the Opera", "artist": "Queen", "type": "album", "track_count": 12, "size_mb": 350, "username": "user2"} + ] + return jsonify({"results": mock_results}) @app.route('/api/artists') def get_artists(): diff --git a/webui/index.html b/webui/index.html index 27acefab..fbdfcde1 100644 --- a/webui/index.html +++ b/webui/index.html @@ -183,77 +183,21 @@ -
- - - -
- -
- - - - - - - -
- - Ready to search • Enter artist, song, or album name -
- - -
-
- -
- Your search results will appear here. -
-
-
+
+
+ +
- - -
-

Download Manager

-
- Active: 0 - Finished: 0 -
- - - -
- - -
-
-
-
No active downloads.
-
-
-
No finished downloads.
-
-
+
+
-
@@ -542,54 +486,6 @@
- - - \ No newline at end of file diff --git a/webui/static/script.js b/webui/static/script.js index 2747ece5..df57bcd5 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -44,12 +44,7 @@ document.addEventListener('DOMContentLoaded', function() { initializeMediaPlayer(); initializeDonationWidget(); initializeSettings(); - - // Only initialize search if on downloads page - const currentPage = getCurrentPage(); - if (currentPage === 'downloads') { - initializeSearch(); - } + initializeSearch(); // Start periodic updates updateServiceStatus(); @@ -402,13 +397,8 @@ function initializeSettings() { const saveButton = document.getElementById('save-settings'); const mediaServerType = document.getElementById('media-server-type'); - if (saveButton) { - saveButton.addEventListener('click', saveSettings); - } - - if (mediaServerType) { - mediaServerType.addEventListener('change', updateMediaServerFields); - } + saveButton.addEventListener('click', saveSettings); + mediaServerType.addEventListener('change', updateMediaServerFields); } async function loadSettingsData() { @@ -709,1184 +699,580 @@ function browsePath(pathType) { // =============================== function initializeSearch() { - console.log('Initializing search functionality...'); - const searchInput = document.getElementById('search-input'); - const searchButton = document.getElementById('search-btn'); - const cancelButton = document.getElementById('search-cancel-btn'); - const filterToggle = document.getElementById('filter-toggle-btn'); + const searchButton = document.getElementById('search-button'); - console.log('Search elements found:', { - searchInput: !!searchInput, - searchButton: !!searchButton, - cancelButton: !!cancelButton, - filterToggle: !!filterToggle + searchButton.addEventListener('click', performSearch); + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') performSearch(); }); - - // Search event handlers - if (searchButton) { - searchButton.addEventListener('click', performSearch); - console.log('Search button click handler added'); - } - - if (cancelButton) { - cancelButton.addEventListener('click', cancelSearch); - console.log('Cancel button click handler added'); - } - - if (searchInput) { - searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - console.log('Enter key pressed in search input'); - performSearch(); - } - }); - console.log('Search input keypress handler added'); - } - - // Filter toggle handler - if (filterToggle) { - filterToggle.addEventListener('click', toggleFilters); - console.log('Filter toggle handler added'); - } - - // Initialize download queue updates only when on downloads page - const downloadsPage = document.getElementById('downloads-page'); - if (downloadsPage && !downloadsPage.classList.contains('hidden')) { - startDownloadQueueUpdates(); - initializeDownloadTabs(); - console.log('Download queue updates started'); - } } -// Global search state -let currentSearchQuery = ''; -let currentSearchResults = { tracks: [], albums: [] }; -let isSearching = false; -let searchAbortController = null; - async function performSearch() { - const searchInput = document.getElementById('search-input'); - const query = searchInput ? searchInput.value.trim() : ''; - + const query = document.getElementById('search-input').value.trim(); if (!query) { - updateSearchStatus('⚠️ Please enter a search term', '#ffa500'); - return; - } - - if (isSearching) { - console.log('Search already in progress, ignoring new search request'); + showToast('Please enter a search term', 'error'); return; } - console.log(`🔍 Starting search for: "${query}"`); - - // Cancel any existing search - if (searchAbortController) { - searchAbortController.abort(); - } - - // Create new abort controller for this search - searchAbortController = new AbortController(); - try { - // Update UI for searching state - isSearching = true; - currentSearchQuery = query; - startSearchAnimations(); - clearSearchResults(); + showLoadingOverlay('Searching...'); + displaySearchResults([]); // Clear previous results - const searchBtn = document.getElementById('search-btn'); - const cancelBtn = document.getElementById('search-cancel-btn'); - - if (searchBtn) { - searchBtn.style.display = 'none'; - } - if (cancelBtn) { - cancelBtn.classList.remove('hidden'); - } - - updateSearchStatus(`🔍 Searching for "${query}"... Results will appear as they are found`, '#1db954'); - - // Perform the actual search const response = await fetch(API.search, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query }), - signal: searchAbortController.signal + body: JSON.stringify({ query }) }); - if (!response.ok) { - throw new Error(`Search failed: ${response.status} ${response.statusText}`); - } - const data = await response.json(); if (data.error) { - throw new Error(data.error); + showToast(`Search error: ${data.error}`, 'error'); + return; } - // Process search results - if (data.success && data.results) { - currentSearchResults = { - tracks: data.results.tracks || [], - albums: data.results.albums || [] - }; - - const totalResults = currentSearchResults.tracks.length + currentSearchResults.albums.length; - - console.log(`✅ Search completed: ${currentSearchResults.tracks.length} tracks, ${currentSearchResults.albums.length} albums (${totalResults} total)`); - - displaySearchResults(currentSearchResults); - - if (totalResults === 0) { - updateSearchStatus('😞 No results found. Try different search terms.', '#ffa500'); - } else { - updateSearchStatus(`✅ Found ${totalResults} results (${currentSearchResults.tracks.length} tracks, ${currentSearchResults.albums.length} albums)`, '#1db954'); - - // Show filter controls if we have results - const filterContainer = document.getElementById('filter-container'); - if (filterContainer && totalResults > 5) { - filterContainer.classList.remove('hidden'); - initializeFilters(); - } - } - } else { - throw new Error('Invalid response format'); - } + searchResults = data.results || []; + displaySearchResults(searchResults); - } catch (error) { - if (error.name === 'AbortError') { - console.log('🛑 Search was cancelled'); - updateSearchStatus('Search cancelled', '#ffa500'); + if (searchResults.length === 0) { + showToast('No results found', 'error'); } else { - console.error('❌ Search failed:', error); - updateSearchStatus(`❌ Search failed: ${error.message}`, '#e22134'); - showToast(`Search failed: ${error.message}`, 'error'); + showToast(`Found ${searchResults.length} results`, 'success'); } - } finally { - // Reset search state - isSearching = false; - searchAbortController = null; - stopSearchAnimations(); - const searchBtn = document.getElementById('search-btn'); - const cancelBtn = document.getElementById('search-cancel-btn'); - - if (searchBtn) { - searchBtn.style.display = 'inline-block'; - } - if (cancelBtn) { - cancelBtn.classList.add('hidden'); - } - } -} - -async function cancelSearch() { - console.log('🛑 Cancelling search...'); - - if (searchAbortController) { - searchAbortController.abort(); - } - - // Also try to cancel on the backend - try { - await fetch('/api/search/cancel', { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); } catch (error) { - console.error('Error cancelling search on backend:', error); - } - - updateSearchStatus('Search cancelled', '#ffa500'); -} - -function updateSearchStatus(message, color = '#ffffff') { - const statusElement = document.getElementById('search-status-text'); - if (statusElement) { - statusElement.textContent = message; - statusElement.style.color = color; - } -} - -function startSearchAnimations() { - const spinner = document.getElementById('search-spinner'); - if (spinner) { - spinner.classList.remove('hidden'); - } -} - -function stopSearchAnimations() { - const spinner = document.getElementById('search-spinner'); - if (spinner) { - spinner.classList.add('hidden'); - } -} - -function clearSearchResults() { - const resultsContainer = document.getElementById('search-results-container'); - if (resultsContainer) { - resultsContainer.innerHTML = '
Your search results will appear here.
'; + console.error('Error performing search:', error); + showToast('Search failed', 'error'); + } finally { + hideLoadingOverlay(); } } function displaySearchResults(results) { - const resultsContainer = document.getElementById('search-results-container'); - if (!resultsContainer) { - console.error('Search results container not found'); - return; - } - - // Clear existing results - resultsContainer.innerHTML = ''; + const resultsContainer = document.getElementById('search-results'); - if (!results || (!results.tracks && !results.albums)) { - resultsContainer.innerHTML = '
No search results to display.
'; + if (!results.length) { + resultsContainer.innerHTML = '
No search results
'; return; } - const tracks = results.tracks || []; - const albums = results.albums || []; - - // Display tracks first - tracks.forEach((track, index) => { - const trackElement = createSearchResultElement(track, index, 'track'); - resultsContainer.appendChild(trackElement); - }); - - // Display albums - albums.forEach((album, index) => { - const albumElement = createSearchResultElement(album, tracks.length + index, 'album'); - resultsContainer.appendChild(albumElement); - }); - - if (tracks.length === 0 && albums.length === 0) { - resultsContainer.innerHTML = '
No results found for your search.
'; - } -} - -function createSearchResultElement(result, index, type) { - const resultDiv = document.createElement('div'); - resultDiv.className = 'search-result-item'; - resultDiv.setAttribute('data-index', index); - resultDiv.setAttribute('data-type', type); - - // Format file size - const fileSizeFormatted = result.file_size ? formatFileSize(result.file_size) : 'Unknown size'; - const bitrate = result.bitrate ? `${result.bitrate} kbps` : ''; - const duration = result.duration ? formatDuration(result.duration) : ''; - - resultDiv.innerHTML = ` -
-

${escapeHtml(result.title || 'Unknown Title')}

- ${type.toUpperCase()} -
-
-
- 🎤 ${escapeHtml(result.artist || 'Unknown Artist')} + resultsContainer.innerHTML = results.map((result, index) => { + const isAlbum = result.type === 'album'; + const sizeText = isAlbum ? + `${result.track_count || 0} tracks, ${(result.size_mb || 0).toFixed(1)} MB` : + `${(result.file_size / 1024 / 1024).toFixed(1)} MB, ${result.bitrate || 0}kbps`; + + return ` +
+
+
+
${escapeHtml(result.title)}
+
${escapeHtml(result.artist)}
+ ${result.album ? `
${escapeHtml(result.album)}
` : ''} +
+
+ + +
+
+
+ ${sizeText} + by ${escapeHtml(result.username)} + ${result.quality ? `${escapeHtml(result.quality)}` : ''} +
- ${result.album ? `
💿 ${escapeHtml(result.album)}
` : ''} - ${result.quality ? `
🎵 ${result.quality}
` : ''} - ${bitrate ? `
⚡ ${bitrate}
` : ''} - ${duration ? `
⏱️ ${duration}
` : ''} -
- 📂 ${fileSizeFormatted} -
-
- 👤 ${escapeHtml(result.username || 'Unknown User')} -
- ${type === 'album' && result.track_count ? `
🎼 ${result.track_count} tracks
` : ''} -
-
- - ${type === 'track' ? `` : ''} -
- `; - - return resultDiv; -} - -// Utility functions for search results -function formatFileSize(bytes) { - if (!bytes || bytes === 0) return 'Unknown size'; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; -} - -function formatDuration(seconds) { - if (!seconds || seconds === 0) return ''; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + `; + }).join(''); } -function toggleFilters() { - const filterContent = document.getElementById('filter-content'); - const filterToggle = document.getElementById('filter-toggle-btn'); +function selectResult(index) { + const result = searchResults[index]; + if (!result) return; - if (filterContent && filterToggle) { - if (filterContent.classList.contains('hidden')) { - filterContent.classList.remove('hidden'); - filterToggle.textContent = '⏶ Filters'; - } else { - filterContent.classList.add('hidden'); - filterToggle.textContent = '⏷ Filters'; - } - } + console.log('Selected result:', result); + // Could show detailed view or additional actions here } -function initializeFilters() { - // This would implement filter functionality - // For now, it's a placeholder - console.log('Initializing filters...'); -} - -// =============================== -// DOWNLOAD FUNCTIONALITY -// =============================== - -// Global download state -let selectedSearchResult = null; -let currentDownloadModal = null; - -async function startDownloadWithModal(index, type) { - console.log(`⬇️ Starting download with modal for index ${index}, type ${type}`); - - // Get the search result based on type and index - let searchResult; - if (type === 'track') { - searchResult = currentSearchResults.tracks[index]; - } else if (type === 'album') { - searchResult = currentSearchResults.albums[index]; - } - - if (!searchResult) { - showToast('Search result not found', 'error'); - return; - } - - selectedSearchResult = searchResult; - - // Show the Spotify matching modal - openSpotifyMatchingModal(searchResult, type); -} - -async function startDirectDownload(searchResult) { - - if (!searchResult) { - showToast('No search result provided for download', 'error'); +async function startStream(index) { + const result = searchResults[index]; + if (!result || result.type === 'album') { + showToast('Cannot stream albums (yet)', 'error'); return; } try { - console.log(`⬇️ Starting direct download: ${searchResult.title}`); + showLoadingAnimation(); + + const streamData = { + username: result.username, + filename: result.filename, + title: result.title, + artist: result.artist, + album: result.album, + quality: result.quality, + bitrate: result.bitrate, + duration: result.duration, + size_mb: result.file_size / 1024 / 1024 + }; - const response = await fetch('/api/downloads/start', { + const response = await fetch(API.stream.start, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(searchResult) + body: JSON.stringify(streamData) }); const data = await response.json(); - if (data.success) { - showToast(`Download started: ${searchResult.title}`, 'success'); - console.log(`✅ Download started: ${data.download_id}`); + if (data.error) { + hideLoadingAnimation(); + showToast(`Stream error: ${data.error}`, 'error'); } else { - throw new Error(data.error || 'Unknown error'); + setTrackInfo(data.track); + currentStream.status = 'loading'; + showToast('Starting stream...', 'success'); } - } catch (error) { - console.error('❌ Download failed:', error); - showToast(`Download failed: ${error.message}`, 'error'); + hideLoadingAnimation(); + console.error('Error starting stream:', error); + showToast('Failed to start stream', 'error'); } } -async function startMatchedDownload(searchResult, spotifyMatch) { - - if (!searchResult || !spotifyMatch) { - showToast('Missing search result or Spotify match for download', 'error'); - return; - } +async function startDownload(index) { + const result = searchResults[index]; + if (!result) return; try { - console.log(`⬇️🎵 Starting matched download: ${searchResult.title}`); - console.log(` 🎤 Matched to: ${spotifyMatch.artist.name} - ${spotifyMatch.album ? spotifyMatch.album.name : 'Single'}`); - - const downloadData = { - ...searchResult, - spotify_match: spotifyMatch - }; - - const response = await fetch('/api/downloads/start-matched', { + const response = await fetch('/api/downloads/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(downloadData) + body: JSON.stringify(result) }); const data = await response.json(); if (data.success) { - showToast(`Matched download started: ${searchResult.title}`, 'success'); - console.log(`✅ Matched download started: ${data.download_id}`); + showToast('Download started', 'success'); } else { - throw new Error(data.error || 'Unknown error'); + showToast(`Download failed: ${data.error}`, 'error'); } - } catch (error) { - console.error('❌ Matched download failed:', error); - showToast(`Matched download failed: ${error.message}`, 'error'); + console.error('Error starting download:', error); + showToast('Failed to start download', 'error'); } } // =============================== -// DOWNLOAD QUEUE MANAGEMENT +// PAGE DATA LOADING // =============================== -let downloadQueueUpdateInterval = null; - -function startDownloadQueueUpdates() { - // Update immediately - updateDownloadQueues(); - - // Update every 2 seconds - downloadQueueUpdateInterval = setInterval(updateDownloadQueues, 2000); -} - -function stopDownloadQueueUpdates() { - if (downloadQueueUpdateInterval) { - clearInterval(downloadQueueUpdateInterval); - downloadQueueUpdateInterval = null; +async function loadInitialData() { + try { + // Load dashboard data by default + await loadDashboardData(); + } catch (error) { + console.error('Error loading initial data:', error); } } -async function updateDownloadQueues() { +async function loadDashboardData() { try { - const response = await fetch('/api/downloads/status'); + const response = await fetch(API.activity); const data = await response.json(); - if (data.success) { - updateDownloadQueueDisplay(data.downloads); - updateDownloadCounts(data.downloads); + const activityFeed = document.getElementById('activity-feed'); + if (data.activities && data.activities.length) { + activityFeed.innerHTML = data.activities.map(activity => ` +
+ ${activity.time} + ${escapeHtml(activity.text)} +
+ `).join(''); } - } catch (error) { - console.error('Error updating download queues:', error); + console.error('Error loading dashboard data:', error); } } -function updateDownloadQueueDisplay(downloads) { - const activeQueue = document.getElementById('active-queue'); - const finishedQueue = document.getElementById('finished-queue'); - - if (activeQueue) { - displayDownloadQueue(activeQueue, downloads.active, 'active'); - } - - if (finishedQueue) { - displayDownloadQueue(finishedQueue, downloads.completed, 'completed'); - } -} - -function displayDownloadQueue(container, downloads, queueType) { - if (!container) return; - - // Clear existing content - container.innerHTML = ''; - - if (!downloads || downloads.length === 0) { - const emptyMessage = document.createElement('div'); - emptyMessage.className = 'empty-queue-message'; - emptyMessage.textContent = queueType === 'active' ? 'No active downloads.' : 'No finished downloads.'; - container.appendChild(emptyMessage); - return; - } - - downloads.forEach(download => { - const downloadElement = createDownloadQueueItem(download, queueType); - container.appendChild(downloadElement); - }); -} - -function createDownloadQueueItem(download, queueType) { - const itemDiv = document.createElement('div'); - itemDiv.className = 'download-queue-item'; - itemDiv.setAttribute('data-download-id', download.id); - - const progress = download.progress || 0; - const status = download.status || 'unknown'; - const speedFormatted = download.download_speed ? formatSpeed(download.download_speed) : ''; - - itemDiv.innerHTML = ` -
-
${escapeHtml(download.title || 'Unknown Title')}
-
${status.toUpperCase()}
-
-
- 🎤 ${escapeHtml(download.artist || 'Unknown Artist')} • 👤 ${escapeHtml(download.username || 'Unknown User')} - ${speedFormatted ? ` • ⚡ ${speedFormatted}` : ''} - ${download.spotify_matched ? ' • 🎵 Spotify Matched' : ''} -
- ${queueType === 'active' && progress !== undefined ? ` -
-
-
-
${progress}% Complete
- ` : ''} - ${queueType === 'active' ? ` - - ` : ''} - `; - - return itemDiv; -} - -function updateDownloadCounts(downloads) { - const activeCount = document.getElementById('active-downloads-count'); - const finishedCount = document.getElementById('finished-downloads-count'); - - if (activeCount) { - activeCount.textContent = downloads.active_count || 0; - } - - if (finishedCount) { - finishedCount.textContent = downloads.completed_count || 0; - } -} - -async function cancelDownload(downloadId) { +async function loadSyncData() { try { - const response = await fetch(`/api/downloads/cancel/${downloadId}`, { - method: 'POST' - }); - + const response = await fetch(API.playlists); const data = await response.json(); - if (data.success) { - showToast('Download cancelled', 'success'); + const playlistSelector = document.getElementById('playlist-selector'); + if (data.playlists && data.playlists.length) { + playlistSelector.innerHTML = [ + '', + ...data.playlists.map(playlist => + `` + ) + ].join(''); } else { - throw new Error(data.error || 'Failed to cancel download'); + playlistSelector.innerHTML = ''; } - } catch (error) { - console.error('Error cancelling download:', error); - showToast(`Failed to cancel download: ${error.message}`, 'error'); + console.error('Error loading sync data:', error); + document.getElementById('playlist-selector').innerHTML = ''; } } -async function clearCompletedDownloads() { +async function loadDownloadsData() { + // Downloads page loads search results dynamically + console.log('Downloads page loaded'); +} + +async function loadArtistsData() { try { - const response = await fetch('/api/downloads/clear-completed', { - method: 'POST' - }); - + const response = await fetch(API.artists); const data = await response.json(); - if (data.success) { - showToast('Completed downloads cleared', 'success'); - updateDownloadQueues(); // Refresh the display + const artistsGrid = document.getElementById('artists-grid'); + if (data.artists && data.artists.length) { + artistsGrid.innerHTML = data.artists.map(artist => ` +
+
+ ${artist.image ? + `${escapeHtml(artist.name)}` : + '
🎵
' + } +
+
+
${escapeHtml(artist.name)}
+
${artist.album_count || 0} albums
+
+
+ `).join(''); } else { - throw new Error(data.error || 'Failed to clear completed downloads'); + artistsGrid.innerHTML = '
No artists found
'; } - } catch (error) { - console.error('Error clearing completed downloads:', error); - showToast(`Failed to clear downloads: ${error.message}`, 'error'); - } -} - -function initializeDownloadTabs() { - console.log('Initializing download tabs...'); - const tabButtons = document.querySelectorAll('.tab-btn'); - const clearButton = document.getElementById('clear-completed-btn'); - - console.log(`Found ${tabButtons.length} tab buttons`); - - tabButtons.forEach((button, index) => { - const targetTab = button.getAttribute('data-tab'); - console.log(`Tab button ${index}: data-tab="${targetTab}"`); - - button.addEventListener('click', () => { - console.log(`Tab clicked: ${targetTab}`); - switchDownloadTab(targetTab); - - // Update tab button states - tabButtons.forEach(btn => btn.classList.remove('active')); - button.classList.add('active'); - }); - }); - - if (clearButton) { - clearButton.addEventListener('click', clearCompletedDownloads); - console.log('Clear completed button handler added'); - } -} - -function switchDownloadTab(tabName) { - console.log(`Switching to tab: ${tabName}`); - const allQueues = document.querySelectorAll('.download-queue'); - console.log(`Found ${allQueues.length} download queues`); - - allQueues.forEach(queue => { - console.log(`Removing active from: ${queue.id}`); - queue.classList.remove('active'); - }); - - const targetQueue = document.getElementById(tabName); - if (targetQueue) { - console.log(`Adding active to: ${tabName}`); - targetQueue.classList.add('active'); - } else { - console.log(`Target queue not found: ${tabName}`); + console.error('Error loading artists data:', error); + document.getElementById('artists-grid').innerHTML = '
Error loading artists
'; } } -function formatSpeed(bytesPerSecond) { - if (!bytesPerSecond || bytesPerSecond === 0) return ''; - const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; - const i = Math.floor(Math.log(bytesPerSecond) / Math.log(1024)); - return Math.round(bytesPerSecond / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; -} - // =============================== -// SPOTIFY MATCHING MODAL +// UTILITY FUNCTIONS // =============================== -// Global Spotify modal state -let selectedArtist = null; -let selectedAlbum = null; -let modalStage = 'artist'; // 'artist' or 'album' -let isForAlbumDownload = false; - -async function openSpotifyMatchingModal(searchResult, type) { - console.log(`🎵 Opening Spotify matching modal for: ${searchResult.title} by ${searchResult.artist}`); - - selectedSearchResult = searchResult; - isForAlbumDownload = (type === 'album'); - selectedArtist = null; - selectedAlbum = null; - modalStage = 'artist'; - - // Show the modal - const modalOverlay = document.getElementById('spotify-matching-modal-overlay'); - if (modalOverlay) { - modalOverlay.classList.remove('hidden'); - } - - // Update modal title and subtitle - updateModalHeader(); - - // Generate artist suggestions - await generateSpotifyArtistSuggestions(searchResult); - - // Setup manual search - setupManualSearch(); +function showLoadingOverlay(message = 'Loading...') { + const overlay = document.getElementById('loading-overlay'); + const messageElement = overlay.querySelector('.loading-message'); + messageElement.textContent = message; + overlay.classList.remove('hidden'); } -function closeSpotifyMatchingModal() { - const modalOverlay = document.getElementById('spotify-matching-modal-overlay'); - if (modalOverlay) { - modalOverlay.classList.add('hidden'); - } - - // Reset modal state - selectedArtist = null; - selectedAlbum = null; - modalStage = 'artist'; - selectedSearchResult = null; - isForAlbumDownload = false; - - // Clear suggestions - clearModalSuggestions(); - clearManualSearchResults(); +function hideLoadingOverlay() { + document.getElementById('loading-overlay').classList.add('hidden'); } -function skipSpotifyMatching() { - console.log('⏭️ Skipping Spotify matching, starting direct download'); +function showToast(message, type = 'success') { + const container = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; - if (selectedSearchResult) { - startDirectDownload(selectedSearchResult); - } + container.appendChild(toast); - closeSpotifyMatchingModal(); + // Auto-remove after 3 seconds + setTimeout(() => { + if (container.contains(toast)) { + container.removeChild(toast); + } + }, 3000); } -function updateModalHeader() { - const titleElement = document.getElementById('spotify-modal-title'); - const subtitleElement = document.getElementById('spotify-modal-subtitle'); - - if (isForAlbumDownload) { - if (modalStage === 'artist') { - if (titleElement) titleElement.textContent = 'Match Album to Spotify'; - if (subtitleElement) subtitleElement.textContent = 'Step 1: Select the correct Artist'; - } else { - if (titleElement) titleElement.textContent = 'Match Album to Spotify'; - if (subtitleElement) subtitleElement.textContent = 'Step 2: Select the correct Album'; - } - } else { - if (titleElement) titleElement.textContent = 'Match Track to Spotify'; - if (subtitleElement) subtitleElement.textContent = 'Select the correct Artist for this Track'; - } +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } -async function generateSpotifyArtistSuggestions(searchResult) { - const suggestionsGrid = document.getElementById('auto-suggestions-grid'); - if (!suggestionsGrid) return; - - // Show loading state - suggestionsGrid.innerHTML = ` -
-
- Generating suggestions... -
- `; - +async function showVersionInfo() { try { - const response = await fetch('/api/spotify/suggestions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - title: searchResult.title, - artist: searchResult.artist - }) - }); + console.log('Fetching version info...'); - const data = await response.json(); - - if (data.success && data.suggestions) { - displayArtistSuggestions(data.suggestions); - } else { - throw new Error(data.error || 'Failed to generate suggestions'); + // Fetch version data from API + const response = await fetch('/api/version-info'); + if (!response.ok) { + throw new Error('Failed to fetch version info'); } + const versionData = await response.json(); + console.log('Version data received:', versionData); + + // Populate modal content + populateVersionModal(versionData); + + // Show modal + const modalOverlay = document.getElementById('version-modal-overlay'); + modalOverlay.classList.remove('hidden'); + + console.log('Version modal opened'); + } catch (error) { - console.error('Error generating artist suggestions:', error); - suggestionsGrid.innerHTML = ` -
- ⚠️ Failed to load suggestions -
- `; + console.error('Error showing version info:', error); + showToast('Failed to load version information', 'error'); } } -function displayArtistSuggestions(artists) { - const suggestionsGrid = document.getElementById('auto-suggestions-grid'); - if (!suggestionsGrid) return; - - suggestionsGrid.innerHTML = ''; - - artists.forEach(artist => { - const artistCard = createSpotifyCard(artist, 'artist'); - suggestionsGrid.appendChild(artistCard); - }); -} - -function displayAlbumSuggestions(albums) { - const suggestionsGrid = document.getElementById('auto-suggestions-grid'); - if (!suggestionsGrid) return; - - suggestionsGrid.innerHTML = ''; - - albums.forEach(album => { - const albumCard = createSpotifyCard(album, 'album'); - suggestionsGrid.appendChild(albumCard); - }); -} - -function createSpotifyCard(item, type) { - const cardDiv = document.createElement('div'); - cardDiv.className = 'spotify-card'; - cardDiv.setAttribute('data-id', item.id); - cardDiv.setAttribute('data-type', type); - - // Add click handler - cardDiv.addEventListener('click', () => selectSpotifyItem(item, type, cardDiv)); - - let details = ''; - if (type === 'artist') { - const genres = item.genres && item.genres.length > 0 ? item.genres.slice(0, 2).join(', ') : 'No genres'; - const followers = item.follower_count ? formatNumber(item.follower_count) + ' followers' : ''; - details = `${genres}${followers ? ' • ' + followers : ''}`; - } else if (type === 'album') { - const releaseYear = item.release_date ? new Date(item.release_date).getFullYear() : ''; - const trackCount = item.total_tracks ? `${item.total_tracks} tracks` : ''; - details = `${releaseYear ? releaseYear + ' • ' : ''}${trackCount}`; - } - - cardDiv.innerHTML = ` -
- ${item.image_url ? - `${escapeHtml(item.name)} - ` : - `
🎵
` - } -
-
${escapeHtml(item.name)}
-
${details}
- `; - - return cardDiv; +function closeVersionModal() { + const modalOverlay = document.getElementById('version-modal-overlay'); + modalOverlay.classList.add('hidden'); + console.log('Version modal closed'); } -function selectSpotifyItem(item, type, cardElement) { - console.log(`🎯 Selected ${type}: ${item.name}`); - - // Update selection state - if (type === 'artist') { - selectedArtist = item; - - // Update visual selection - document.querySelectorAll('.spotify-card').forEach(card => card.classList.remove('selected')); - cardElement.classList.add('selected'); - - // Enable confirm button or move to album selection - if (isForAlbumDownload) { - // For albums, proceed to album selection - proceedToAlbumSelection(); - } else { - // For tracks, enable confirm button - enableConfirmButton(); - } - } else if (type === 'album') { - selectedAlbum = item; - - // Update visual selection - document.querySelectorAll('.spotify-card').forEach(card => card.classList.remove('selected')); - cardElement.classList.add('selected'); - - // Enable confirm button - enableConfirmButton(); +function populateVersionModal(versionData) { + const container = document.getElementById('version-content-container'); + if (!container) { + console.error('Version content container not found'); + return; } -} - -async function proceedToAlbumSelection() { - console.log(`🎵 Proceeding to album selection for artist: ${selectedArtist.name}`); - modalStage = 'album'; - updateModalHeader(); + // Update header with dynamic data + const titleElement = document.querySelector('.version-modal-title'); + const subtitleElement = document.querySelector('.version-modal-subtitle'); - // Clear manual search - clearManualSearchResults(); - const manualSearch = document.getElementById('spotify-manual-search'); - if (manualSearch) { - manualSearch.value = ''; - manualSearch.placeholder = 'Manually search for an album...'; - } + if (titleElement) titleElement.textContent = versionData.title; + if (subtitleElement) subtitleElement.textContent = versionData.subtitle; - // Load albums for selected artist - await loadArtistAlbums(selectedArtist.id); -} - -async function loadArtistAlbums(artistId) { - const suggestionsGrid = document.getElementById('auto-suggestions-grid'); - if (!suggestionsGrid) return; - - // Show loading state - suggestionsGrid.innerHTML = ` -
-
- Loading albums... -
- `; + // Clear existing content + container.innerHTML = ''; - try { - const response = await fetch('/api/spotify/search-album', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - artist_id: artistId - }) + // Create sections + versionData.sections.forEach(section => { + const sectionDiv = document.createElement('div'); + sectionDiv.className = 'version-feature-section'; + + // Section title + const titleDiv = document.createElement('div'); + titleDiv.className = 'version-section-title'; + titleDiv.textContent = section.title; + sectionDiv.appendChild(titleDiv); + + // Section description + const descDiv = document.createElement('div'); + descDiv.className = 'version-section-description'; + descDiv.textContent = section.description; + sectionDiv.appendChild(descDiv); + + // Features list + const featuresList = document.createElement('ul'); + featuresList.className = 'version-feature-list'; + + section.features.forEach(feature => { + const featureItem = document.createElement('li'); + featureItem.className = 'version-feature-item'; + featureItem.textContent = feature; + featuresList.appendChild(featureItem); }); - const data = await response.json(); + sectionDiv.appendChild(featuresList); - if (data.success && data.albums) { - displayAlbumSuggestions(data.albums); - } else { - throw new Error(data.error || 'Failed to load albums'); + // Usage note (if present) + if (section.usage_note) { + const usageDiv = document.createElement('div'); + usageDiv.className = 'version-usage-note'; + usageDiv.textContent = `💡 ${section.usage_note}`; + sectionDiv.appendChild(usageDiv); } - } catch (error) { - console.error('Error loading artist albums:', error); - suggestionsGrid.innerHTML = ` -
- ⚠️ Failed to load albums -
- `; - } + container.appendChild(sectionDiv); + }); + + console.log('Version modal content populated'); } -function setupManualSearch() { - const manualSearch = document.getElementById('spotify-manual-search'); - if (!manualSearch) return; - - // Clear previous listeners - manualSearch.replaceWith(manualSearch.cloneNode(true)); - const newManualSearch = document.getElementById('spotify-manual-search'); - - let searchTimeout; - newManualSearch.addEventListener('input', (e) => { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - performManualSpotifySearch(e.target.value.trim()); - }, 500); // Debounce search - }); +// =============================== +// ADDITIONAL STYLES FOR SEARCH RESULTS +// =============================== + +// Add dynamic styles for search results (since they're created dynamically) +const additionalStyles = ` + +`; + +// Inject additional styles +document.head.insertAdjacentHTML('beforeend', additionalStyles); + +// Global functions (for onclick handlers) +window.navigateToPage = navigateToPage; +window.openKofi = openKofi; +window.copyAddress = copyAddress; +window.showVersionInfo = showVersionInfo; +window.closeVersionModal = closeVersionModal; +window.testConnection = testConnection; +window.autoDetectPlex = autoDetectPlex; +window.autoDetectJellyfin = autoDetectJellyfin; +window.autoDetectSlskd = autoDetectSlskd; +window.toggleServer = toggleServer; +window.authenticateTidal = authenticateTidal; +window.browsePath = browsePath; +window.selectResult = selectResult; +window.startStream = startStream; +window.startDownload = startDownload; \ No newline at end of file diff --git a/webui/static/style.css b/webui/static/style.css index edd74db8..06b29aca 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -618,211 +618,83 @@ body { letter-spacing: -0.5px; } -/* ===================================== - DOWNLOADS (SEARCH) PAGE STYLES - ===================================== */ - -#downloads-page .page-header p { - font-size: 14px; - color: rgba(255, 255, 255, 0.7); - margin-top: 4px; -} - -.downloads-content-splitter { +/* Dashboard Page Styling */ +.dashboard-content { display: flex; - gap: 16px; - height: calc(100% - 100px); /* Adjust based on header height */ + gap: 40px; + height: calc(100vh - 150px); } -.search-results-panel { - flex: 3; /* Takes up more space */ - display: flex; - flex-direction: column; - gap: 12px; +.activity-section { + flex: 1; background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 12px; - padding: 16px; -} - -.download-manager-panel { - flex: 2; /* Takes up less space */ - display: flex; - flex-direction: column; - gap: 12px; - background: rgba(255, 255, 255, 0.02); + padding: 24px; border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 12px; - padding: 16px; } -.download-manager-panel h4 { +.activity-section h3 { + font-family: 'SF Pro Text', -apple-system, sans-serif; font-size: 16px; font-weight: 600; - color: #ffffff; - margin-bottom: 4px; -} - -/* Elegant Search Bar */ -.elegant-search-bar { - display: flex; - gap: 10px; - align-items: center; - background: rgba(0, 0, 0, 0.2); - padding: 8px; - border-radius: 8px; -} - -#search-input { - flex-grow: 1; - background: #2a2a2a; - border: 1px solid #444; - border-radius: 6px; - color: #fff; - padding: 10px 14px; - font-size: 14px; -} -#search-input:focus { - outline: none; - border-color: #1ed760; -} - -.search-btn, .cancel-search-btn { - padding: 10px 20px; - border: none; - border-radius: 6px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: background-color 0.2s; -} - -.search-btn { - background-color: #1db954; - color: #000; -} -.search-btn:hover { - background-color: #1ed760; -} - -.cancel-search-btn { - background-color: #d32f2f; - color: #fff; -} -.cancel-search-btn:hover { - background-color: #f44336; -} - -/* Search Status */ -.search-status { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: rgba(29, 185, 84, 0.08); - border: 1px solid rgba(29, 185, 84, 0.2); - border-radius: 6px; - font-size: 12px; - color: rgba(255, 255, 255, 0.7); -} - -.spinner { - width: 16px; - height: 16px; - border: 2px solid rgba(255, 255, 255, 0.2); - border-top-color: #1ed760; - border-radius: 50%; - animation: spin 0.8s linear infinite; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 16px; } -/* Search Results Area */ -.search-results-area { - flex-grow: 1; +.activity-feed { + max-height: 400px; overflow-y: auto; - background: rgba(0, 0, 0, 0.2); - border-radius: 8px; - padding: 8px; -} - -.no-results-placeholder { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: rgba(255, 255, 255, 0.4); - font-style: italic; } -/* Download Manager */ -.download-stats { +.activity-item { display: flex; - gap: 16px; - font-size: 12px; - color: #b3b3b3; -} -.download-stats b { - color: #fff; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); } -.clear-completed-btn { - background: rgba(220, 53, 69, 0.2); - color: #dc3545; - border: 1px solid rgba(220, 53, 69, 0.4); - padding: 6px 12px; - border-radius: 6px; +.activity-time { font-size: 11px; - cursor: pointer; - align-self: flex-start; -} -.clear-completed-btn:hover { - background: rgba(220, 53, 69, 0.3); -} - -.download-tabs { - display: flex; - border-bottom: 1px solid #444; -} - -.tab-btn { - padding: 8px 16px; - background: transparent; - border: none; - color: #b3b3b3; - cursor: pointer; - border-bottom: 2px solid transparent; - font-size: 13px; + color: rgba(255, 255, 255, 0.5); + min-width: 80px; } -.tab-btn.active { - color: #1ed760; - border-bottom-color: #1ed760; +.activity-text { + font-size: 12px; + color: rgba(255, 255, 255, 0.8); + flex: 1; } -.download-queue-container { - flex-grow: 1; - overflow-y: auto; - position: relative; +.stats-grid { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + width: 300px; } -.download-queue { - display: none; - flex-direction: column; - gap: 8px; - padding: 8px; +.stat-card { + background: linear-gradient(135deg, rgba(29, 185, 84, 0.1) 0%, rgba(255, 255, 255, 0.02) 100%); + border: 1px solid rgba(29, 185, 84, 0.2); + border-radius: 12px; + padding: 24px; + text-align: center; } -.download-queue.active { - display: flex; +.stat-value { + font-family: 'SF Pro Display', -apple-system, sans-serif; + font-size: 32px; + font-weight: 700; + color: #1ed760; + line-height: 1; + margin-bottom: 8px; } -.empty-queue-message { - text-align: center; - padding: 20px; - color: rgba(255, 255, 255, 0.4); - font-style: italic; +.stat-label { + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; } - /* Settings Page Styling - Two Column Layout */ .settings-content { max-width: 100%; @@ -1756,588 +1628,4 @@ body { .version-modal-overlay:not(.hidden) .version-modal { animation: modalFadeIn 0.3s ease-out; -} - -/* ===== SPOTIFY MATCHING MODAL STYLES ===== */ - -.spotify-matching-modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(8px); - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - opacity: 1; - transition: opacity 0.3s ease-in-out; -} - -.spotify-matching-modal-overlay.hidden { - opacity: 0; - pointer-events: none; -} - -.spotify-matching-modal { - background: #121212; - border-radius: 12px; - border: 1px solid rgba(29, 185, 84, 0.2); - width: 1100px; - max-width: 95vw; - height: 750px; - max-height: 90vh; - display: flex; - flex-direction: column; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8); - transform: scale(1); - transition: transform 0.3s ease-in-out; -} - -.spotify-matching-modal-overlay.hidden .spotify-matching-modal { - transform: scale(0.9); -} - -/* Modal Header */ -.spotify-modal-header { - padding: 20px 20px 15px 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - background: #121212; -} - -.spotify-modal-title { - color: #1DB954; - font-size: 22px; - font-weight: 700; - margin: 0 0 8px 0; - font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif; -} - -.spotify-modal-subtitle { - color: #B3B3B3; - font-size: 16px; - font-weight: 400; - margin: 0; - font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif; -} - -/* Modal Content */ -.spotify-modal-content { - flex: 1; - overflow-y: auto; - background: #121212; - padding: 20px; -} - -.spotify-modal-content::-webkit-scrollbar { - width: 8px; -} - -.spotify-modal-content::-webkit-scrollbar-track { - background: #2A2A2A; - border-radius: 4px; -} - -.spotify-modal-content::-webkit-scrollbar-thumb { - background: #535353; - border-radius: 4px; -} - -.spotify-modal-content::-webkit-scrollbar-thumb:hover { - background: #6A6A6A; -} - -.spotify-content-container { - display: flex; - flex-direction: column; - gap: 30px; -} - -/* Suggestions Section */ -.suggestions-section { - display: flex; - flex-direction: column; - gap: 15px; -} - -.suggestions-title, .manual-search-title { - color: #B3B3B3; - font-size: 16px; - font-weight: 500; - margin: 0; - font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif; -} - -.suggestions-grid { - display: flex; - gap: 20px; - flex-wrap: wrap; - min-height: 120px; - align-items: flex-start; -} - -.loading-suggestions { - display: flex; - align-items: center; - gap: 12px; - color: #B3B3B3; - font-size: 14px; - padding: 20px; -} - -.suggestion-loading-spinner { - width: 20px; - height: 20px; - border: 2px solid rgba(29, 185, 84, 0.2); - border-top-color: #1DB954; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -/* Artist/Album Cards */ -.spotify-card { - background: #1E1E1E; - border: 2px solid #2A2A2A; - border-radius: 12px; - padding: 15px; - width: 180px; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - position: relative; -} - -.spotify-card:hover { - border-color: #1DB954; - background: #282828; - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(29, 185, 84, 0.2); -} - -.spotify-card.selected { - border-color: #1DB954; - background: rgba(29, 185, 84, 0.1); - box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.3); -} - -.spotify-card-image { - width: 120px; - height: 120px; - border-radius: 8px; - background: #2A2A2A; - margin-bottom: 12px; - overflow: hidden; - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - -.spotify-card-image img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.spotify-card-image-placeholder { - color: #535353; - font-size: 24px; -} - -.spotify-card-name { - color: #FFFFFF; - font-size: 14px; - font-weight: 600; - margin-bottom: 4px; - line-height: 1.2; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif; -} - -.spotify-card-details { - color: #B3B3B3; - font-size: 12px; - font-weight: 400; - line-height: 1.3; - font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif; -} - -/* Manual Search Section */ -.manual-search-section { - display: flex; - flex-direction: column; - gap: 15px; - margin-top: 15px; - padding-top: 15px; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -.manual-search-bar { - display: flex; - flex-direction: column; - gap: 15px; -} - -#spotify-manual-search { - background: #2A2A2A; - border: 2px solid #535353; - border-radius: 15px; - color: white; - padding: 12px 16px; - font-size: 14px; - transition: border-color 0.3s ease; - font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif; -} - -#spotify-manual-search:focus { - outline: none; - border-color: #1DB954; -} - -.manual-search-results { - display: flex; - gap: 20px; - flex-wrap: wrap; - min-height: 60px; - align-items: flex-start; -} - -/* Modal Footer */ -.spotify-modal-footer { - padding: 15px 20px; - border-top: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.02); - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.spotify-modal-btn { - border: none; - border-radius: 15px; - padding: 10px 20px; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: background-color 0.3s ease; - font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif; -} - -.spotify-modal-btn.primary { - background: #1DB954; - color: #FFFFFF; -} - -.spotify-modal-btn.primary:hover:not(:disabled) { - background: #1ED760; -} - -.spotify-modal-btn.primary:disabled { - background: #2A2A2A; - color: #535353; - cursor: not-allowed; -} - -.spotify-modal-btn.secondary { - background: #535353; - color: #FFFFFF; -} - -.spotify-modal-btn.secondary:hover { - background: #6A6A6A; -} - -/* ===== SEARCH RESULTS & DOWNLOAD QUEUE STYLES ===== */ - -/* Search Result Items */ -.search-result-item { - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 8px; - padding: 12px; - margin-bottom: 8px; - transition: all 0.2s ease; -} - -.search-result-item:hover { - background: rgba(255, 255, 255, 0.05); - border-color: rgba(29, 185, 84, 0.3); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} - -.result-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -.result-title { - font-size: 16px; - font-weight: 600; - color: #ffffff; - margin: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - margin-right: 12px; -} - -.result-type-badge { - padding: 4px 8px; - border-radius: 4px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.result-type-badge.track { - background: rgba(29, 185, 84, 0.2); - color: #1ed760; - border: 1px solid rgba(29, 185, 84, 0.3); -} - -.result-type-badge.album { - background: rgba(255, 165, 0, 0.2); - color: #ffa500; - border: 1px solid rgba(255, 165, 0, 0.3); -} - -.result-details { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-bottom: 12px; - font-size: 12px; - color: #b3b3b3; -} - -.result-detail-item { - display: flex; - align-items: center; - gap: 4px; -} - -.result-actions { - display: flex; - gap: 8px; - align-items: center; -} - -.result-btn { - padding: 6px 12px; - border: none; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.result-btn.download { - background: #1db954; - color: #000; -} - -.result-btn.download:hover { - background: #1ed760; -} - -.result-btn.stream { - background: rgba(255, 255, 255, 0.1); - color: #fff; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.result-btn.stream:hover { - background: rgba(255, 255, 255, 0.2); -} - -/* Download Queue Items */ -.download-queue-item { - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 6px; - padding: 10px; - margin-bottom: 6px; - transition: all 0.2s ease; -} - -.download-queue-item:hover { - background: rgba(255, 255, 255, 0.05); -} - -.download-item-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -.download-item-title { - font-size: 13px; - font-weight: 500; - color: #ffffff; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - margin-right: 8px; -} - -.download-item-status { - font-size: 10px; - padding: 2px 6px; - border-radius: 3px; - font-weight: 600; - text-transform: uppercase; -} - -.download-item-status.downloading { - background: rgba(29, 185, 84, 0.2); - color: #1ed760; -} - -.download-item-status.queued { - background: rgba(255, 165, 0, 0.2); - color: #ffa500; -} - -.download-item-status.completed { - background: rgba(72, 191, 227, 0.2); - color: #48bfe3; -} - -.download-item-status.failed { - background: rgba(220, 53, 69, 0.2); - color: #dc3545; -} - -.download-item-details { - font-size: 11px; - color: #b3b3b3; - margin-bottom: 6px; -} - -.download-progress { - height: 4px; - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - overflow: hidden; - margin-bottom: 4px; -} - -.download-progress-fill { - height: 100%; - background: linear-gradient(90deg, #1db954, #1ed760); - transition: width 0.3s ease; - border-radius: 2px; -} - -.download-progress-text { - font-size: 10px; - color: #b3b3b3; - text-align: right; -} - -/* Filter Controls (for search results) */ -.filter-controls { - background: rgba(0, 0, 0, 0.2); - border-radius: 6px; - padding: 8px; -} - -.filter-toggle-btn { - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - color: #b3b3b3; - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 12px; -} - -.filter-toggle-btn:hover { - background: rgba(255, 255, 255, 0.05); - color: #fff; -} - -.filter-content { - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - gap: 12px; - align-items: center; -} - -.filter-group { - display: flex; - gap: 4px; - align-items: center; -} - -.filter-label { - font-size: 11px; - color: #b3b3b3; - margin-right: 6px; -} - -.filter-btn { - padding: 4px 8px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - color: #b3b3b3; - font-size: 10px; - cursor: pointer; - transition: all 0.2s ease; -} - -.filter-btn:hover { - background: rgba(255, 255, 255, 0.1); - color: #fff; -} - -.filter-btn.active { - background: rgba(29, 185, 84, 0.2); - border-color: rgba(29, 185, 84, 0.4); - color: #1ed760; -} - -/* Loading and Empty States */ -.loading-results { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px 20px; - color: #b3b3b3; - gap: 12px; -} - -.loading-results-spinner { - width: 32px; - height: 32px; - border: 3px solid rgba(29, 185, 84, 0.2); - border-top-color: #1db954; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -/* Animations */ -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } } \ No newline at end of file