From bb6be656e0c6a2714e7edee2341773d3cd07ecf6 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sat, 23 Aug 2025 09:53:07 -0700 Subject: [PATCH] progress --- web_server.py | 95 +++++++++++++++++++++++--- webui/static/script.js | 152 ++++++++++++++++++++++++++++++++++++++++- webui/static/style.css | 32 +++++++++ 3 files changed, 269 insertions(+), 10 deletions(-) diff --git a/web_server.py b/web_server.py index 280135f4..682b821c 100644 --- a/web_server.py +++ b/web_server.py @@ -504,16 +504,93 @@ def start_sync(): @app.route('/api/search', methods=['POST']) def search_music(): - # Placeholder: simulates a music search + """Real search using soulseek_client""" data = request.get_json() - 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}) + query = data.get('query') + if not query: + return jsonify({"error": "No search query provided."}), 400 + + print(f"Web UI Search for: '{query}'") + + try: + tracks, albums = asyncio.run(soulseek_client.search(query)) + + # Convert to dictionaries for JSON response + processed_albums = [] + for album in albums: + album_dict = album.__dict__.copy() + album_dict["tracks"] = [track.__dict__ for track in album.tracks] + album_dict["result_type"] = "album" + processed_albums.append(album_dict) + + processed_tracks = [] + for track in tracks: + track_dict = track.__dict__.copy() + track_dict["result_type"] = "track" + processed_tracks.append(track_dict) + + # Sort by quality score + all_results = sorted(processed_albums + processed_tracks, key=lambda x: x.get('quality_score', 0), reverse=True) + + return jsonify({"results": all_results}) + + except Exception as e: + print(f"Search error: {e}") + return jsonify({"error": str(e)}), 500 + +@app.route('/api/download', methods=['POST']) +def start_download(): + """Simple download route""" + data = request.get_json() + if not data: + return jsonify({"error": "No download data provided."}), 400 + + try: + result_type = data.get('result_type', 'track') + + if result_type == 'album': + tracks = data.get('tracks', []) + if not tracks: + return jsonify({"error": "No tracks found in album."}), 400 + + started_downloads = 0 + for track_data in tracks: + try: + download_id = asyncio.run(soulseek_client.download( + track_data.get('username'), + track_data.get('filename'), + track_data.get('size', 0) + )) + if download_id: + started_downloads += 1 + except Exception as e: + print(f"Failed to start track download: {e}") + continue + + return jsonify({ + "success": True, + "message": f"Started {started_downloads} downloads from album" + }) + + else: + # Single track download + username = data.get('username') + filename = data.get('filename') + file_size = data.get('size', 0) + + if not username or not filename: + return jsonify({"error": "Missing username or filename."}), 400 + + download_id = asyncio.run(soulseek_client.download(username, filename, file_size)) + + if download_id: + return jsonify({"success": True, "message": "Download started"}) + else: + return jsonify({"error": "Failed to start download"}), 500 + + except Exception as e: + print(f"Download error: {e}") + return jsonify({"error": str(e)}), 500 @app.route('/api/artists') def get_artists(): diff --git a/webui/static/script.js b/webui/static/script.js index df57bcd5..6310ea56 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -924,6 +924,154 @@ async function loadSyncData() { async function loadDownloadsData() { // Downloads page loads search results dynamically console.log('Downloads page loaded'); + + // Connect downloads search button + const searchButton = document.getElementById('downloads-search-btn'); + const searchInput = document.getElementById('downloads-search-input'); + + if (searchButton && searchInput) { + searchButton.addEventListener('click', performDownloadsSearch); + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') performDownloadsSearch(); + }); + } +} + +async function performDownloadsSearch() { + const query = document.getElementById('downloads-search-input').value.trim(); + if (!query) { + showToast('Please enter a search term', 'error'); + return; + } + + try { + showLoadingOverlay('Searching...'); + + const response = await fetch('/api/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }) + }); + + const data = await response.json(); + + if (data.error) { + showToast(`Search error: ${data.error}`, 'error'); + return; + } + + const results = data.results || []; + displayDownloadsResults(results); + + if (results.length === 0) { + showToast('No results found', 'error'); + } else { + showToast(`Found ${results.length} results`, 'success'); + } + + } catch (error) { + console.error('Search failed:', error); + showToast('Search failed', 'error'); + } finally { + hideLoadingOverlay(); + } +} + +function displayDownloadsResults(results) { + const resultsArea = document.getElementById('search-results-area'); + if (!resultsArea) return; + + if (!results.length) { + resultsArea.innerHTML = '

No search results found.

'; + return; + } + + let html = ''; + results.forEach((result, index) => { + const isAlbum = result.result_type === 'album'; + + if (isAlbum) { + const trackCount = result.tracks ? result.tracks.length : 0; + html += ` +
+
+ 🎵 ${escapeHtml(result.album_title || result.title || 'Unknown Album')}
+ by ${escapeHtml(result.artist || 'Unknown Artist')}
+ ${trackCount} tracks • ${escapeHtml(result.quality || 'Mixed')} +
+ +
+ `; + } else { + const sizeText = result.size ? `${(result.size / 1024 / 1024).toFixed(1)} MB` : 'Unknown size'; + html += ` +
+
+ ${escapeHtml(result.title || 'Unknown Title')}
+ by ${escapeHtml(result.artist || 'Unknown Artist')}
+ ${sizeText} • ${escapeHtml(result.quality || 'Unknown')} ${result.bitrate ? `${result.bitrate}kbps` : ''} +
+ +
+ `; + } + }); + + resultsArea.innerHTML = html; + // Store results globally for download functions + window.currentSearchResults = results; +} + +async function downloadTrack(index) { + const results = window.currentSearchResults; + if (!results || !results[index]) return; + + const track = results[index]; + + try { + const response = await fetch('/api/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(track) + }); + + const data = await response.json(); + + if (data.success) { + showToast(`Download started: ${track.title}`, 'success'); + } else { + showToast(`Download failed: ${data.error}`, 'error'); + } + } catch (error) { + console.error('Download error:', error); + showToast('Failed to start download', 'error'); + } +} + +async function downloadAlbum(index) { + const results = window.currentSearchResults; + if (!results || !results[index]) return; + + const album = results[index]; + + try { + const response = await fetch('/api/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(album) + }); + + const data = await response.json(); + + if (data.success) { + showToast(data.message, 'success'); + } else { + showToast(`Album download failed: ${data.error}`, 'error'); + } + } catch (error) { + console.error('Album download error:', error); + showToast('Failed to start album download', 'error'); + } } async function loadArtistsData() { @@ -1275,4 +1423,6 @@ window.authenticateTidal = authenticateTidal; window.browsePath = browsePath; window.selectResult = selectResult; window.startStream = startStream; -window.startDownload = startDownload; \ No newline at end of file +window.startDownload = startDownload; +window.downloadTrack = downloadTrack; +window.downloadAlbum = downloadAlbum; \ No newline at end of file diff --git a/webui/static/style.css b/webui/static/style.css index e3644db2..9a059a9e 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -2006,4 +2006,36 @@ body { .version-modal-overlay:not(.hidden) .version-modal { animation: modalFadeIn 0.3s ease-out; +} + +/* Simple Downloads Results Styling */ +.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; + display: flex; + justify-content: space-between; + align-items: center; +} + +.search-result-item .result-info { + flex: 1; +} + +.search-result-item .download-btn { + background: rgba(29, 185, 84, 0.1); + color: #1ed760; + border: 1px solid rgba(29, 185, 84, 0.3); + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.search-result-item .download-btn:hover { + background: rgba(29, 185, 84, 0.2); + border-color: rgba(29, 185, 84, 0.5); } \ No newline at end of file