// SoulSync WebUI JavaScript - Replicating PyQt6 GUI Functionality // Global state management let currentPage = 'dashboard'; let currentTrack = null; let isPlaying = false; let mediaPlayerExpanded = false; let donationAddressesVisible = false; let searchResults = []; let currentStream = { status: 'stopped', progress: 0, track: null }; // Streaming state management (new functionality) let streamStatusPoller = null; let audioPlayer = null; let allSearchResults = []; let currentFilterType = 'all'; let currentFilterFormat = 'all'; let currentSortBy = 'quality_score'; let isSortReversed = false; // API endpoints const API = { status: '/status', config: '/config', settings: '/api/settings', testConnection: '/api/test-connection', playlists: '/api/playlists', sync: '/api/sync', search: '/api/search', artists: '/api/artists', activity: '/api/activity', stream: { start: '/api/stream/start', status: '/api/stream/status', toggle: '/api/stream/toggle', stop: '/api/stream/stop' } }; // =============================== // INITIALIZATION // =============================== document.addEventListener('DOMContentLoaded', function() { console.log('SoulSync WebUI initializing...'); // Initialize components initializeNavigation(); initializeMediaPlayer(); initializeDonationWidget(); // Start periodic updates updateServiceStatus(); setInterval(updateServiceStatus, 5000); // Every 5 seconds // Load initial data loadInitialData(); // Handle window resize to re-check track title scrolling window.addEventListener('resize', function() { if (currentTrack) { const trackTitleElement = document.getElementById('track-title'); const trackTitle = currentTrack.title || 'Unknown Track'; setTimeout(() => { checkAndEnableScrolling(trackTitleElement, trackTitle); }, 100); // Small delay to allow layout to settle } }); console.log('SoulSync WebUI initialized successfully!'); }); // =============================== // NAVIGATION SYSTEM // =============================== function initializeNavigation() { const navButtons = document.querySelectorAll('.nav-button'); navButtons.forEach(button => { button.addEventListener('click', () => { const page = button.getAttribute('data-page'); navigateToPage(page); }); }); } function navigateToPage(pageId) { if (pageId === currentPage) return; // Update navigation buttons document.querySelectorAll('.nav-button').forEach(btn => { btn.classList.remove('active'); }); document.querySelector(`[data-page="${pageId}"]`).classList.add('active'); // Update pages document.querySelectorAll('.page').forEach(page => { page.classList.remove('active'); }); document.getElementById(`${pageId}-page`).classList.add('active'); currentPage = pageId; // Load page-specific data loadPageData(pageId); } // REPLACE your old loadPageData function with this one: // REPLACE your old loadPageData function with this corrected one async function loadPageData(pageId) { try { switch (pageId) { case 'dashboard': stopDownloadPolling(); await loadDashboardData(); break; case 'sync': stopDownloadPolling(); await loadSyncData(); break; case 'downloads': // --- FIX: Initialize first, THEN load data. This is the correct order. --- initializeSearch(); initializeFilters(); await loadDownloadsData(); break; case 'artists': stopDownloadPolling(); await loadArtistsData(); break; case 'settings': initializeSettings(); stopDownloadPolling(); await loadSettingsData(); break; } } catch (error) { console.error(`Error loading ${pageId} data:`, error); showToast(`Failed to load ${pageId} data`, 'error'); } } // =============================== // SERVICE STATUS MONITORING // =============================== async function updateServiceStatus() { try { const response = await fetch(API.status); const data = await response.json(); // Update sidebar status indicators updateStatusIndicator('spotify', data.spotify); updateStatusIndicator('media-server', data.media_server); updateStatusIndicator('soulseek', data.soulseek); // Update media server name const serverName = data.active_media_server === 'plex' ? 'Plex' : 'Jellyfin'; document.getElementById('media-server-name').textContent = serverName; } catch (error) { console.error('Error fetching status:', error); // Set all to disconnected on error updateStatusIndicator('spotify', false); updateStatusIndicator('media-server', false); updateStatusIndicator('soulseek', false); } } function updateStatusIndicator(service, connected) { const indicator = document.getElementById(`${service}-indicator`); const dot = indicator.querySelector('.status-dot'); if (connected) { dot.classList.remove('disconnected'); dot.classList.add('connected'); } else { dot.classList.remove('connected'); dot.classList.add('disconnected'); } } // =============================== // MEDIA PLAYER FUNCTIONALITY // =============================== function initializeMediaPlayer() { const trackTitle = document.getElementById('track-title'); const playButton = document.getElementById('play-button'); const stopButton = document.getElementById('stop-button'); const volumeSlider = document.getElementById('volume-slider'); // Initialize HTML5 audio player audioPlayer = document.getElementById('audio-player'); if (audioPlayer) { // Set up audio event listeners audioPlayer.addEventListener('timeupdate', updateAudioProgress); audioPlayer.addEventListener('ended', onAudioEnded); audioPlayer.addEventListener('error', onAudioError); audioPlayer.addEventListener('loadstart', onAudioLoadStart); audioPlayer.addEventListener('canplay', onAudioCanPlay); // Set initial volume audioPlayer.volume = 0.7; // 70% volumeSlider.value = 70; } // Track title click - toggle expansion trackTitle.addEventListener('click', toggleMediaPlayerExpansion); // Media controls playButton.addEventListener('click', handlePlayPause); stopButton.addEventListener('click', handleStop); volumeSlider.addEventListener('input', handleVolumeChange); // Update volume slider styling volumeSlider.addEventListener('input', updateVolumeSliderAppearance); } function toggleMediaPlayerExpansion() { if (!currentTrack) return; const mediaPlayer = document.getElementById('media-player'); const expandedContent = document.getElementById('media-expanded'); const noTrackMessage = document.getElementById('no-track-message'); mediaPlayerExpanded = !mediaPlayerExpanded; if (mediaPlayerExpanded) { mediaPlayer.style.minHeight = '145px'; expandedContent.classList.remove('hidden'); noTrackMessage.classList.add('hidden'); } else { mediaPlayer.style.minHeight = '85px'; expandedContent.classList.add('hidden'); } } function extractTrackTitle(filename) { if (!filename) return null; // Remove file extension let title = filename.replace(/\.[^/.]+$/, ''); // Remove path components, keep only the filename title = title.split('/').pop().split('\\').pop(); // Clean up common filename patterns title = title .replace(/^\d+\.?\s*/, '') // Remove track numbers at start .replace(/^\d+\s*-\s*/, '') // Remove "01 - " patterns .replace(/\s*-\s*\d{4}\s*$/, '') // Remove years at end .replace(/\s*\[\d+kbps\].*$/, '') // Remove bitrate info .replace(/\s*\(.*?\)\s*$/, '') // Remove parenthetical info at end .trim(); return title || null; } function setTrackInfo(track) { currentTrack = track; const trackTitleElement = document.getElementById('track-title'); const trackTitle = track.title || 'Unknown Track'; // Set up the HTML structure for scrolling trackTitleElement.innerHTML = `${escapeHtml(trackTitle)}`; document.getElementById('artist-name').textContent = track.artist || 'Unknown Artist'; document.getElementById('album-name').textContent = track.album || 'Unknown Album'; // Check if title needs scrolling (similar to GUI app) setTimeout(() => { checkAndEnableScrolling(trackTitleElement, trackTitle); }, 100); // Allow DOM to settle // Enable controls document.getElementById('play-button').disabled = false; document.getElementById('stop-button').disabled = false; // Hide no track message document.getElementById('no-track-message').classList.add('hidden'); // Auto-expand if collapsed if (!mediaPlayerExpanded) { toggleMediaPlayerExpansion(); } } function checkAndEnableScrolling(element, text) { // Remove any existing scrolling class and reset styles element.classList.remove('scrolling'); element.style.removeProperty('--scroll-distance'); // Force a layout to get accurate measurements element.offsetWidth; // Get the inner text element const titleTextElement = element.querySelector('.title-text'); if (!titleTextElement) return; // Check if text is wider than container const containerWidth = element.offsetWidth; const textWidth = titleTextElement.scrollWidth; // Enable scrolling if text is significantly wider than container if (textWidth > containerWidth + 15) { const scrollDistance = containerWidth - textWidth; element.style.setProperty('--scroll-distance', `${scrollDistance}px`); element.classList.add('scrolling'); console.log(`đ Enabled scrolling for title: "${text}"`); console.log(`đ Container: ${containerWidth}px, Text: ${textWidth}px, Scroll: ${scrollDistance}px`); } } function clearTrack() { currentTrack = null; isPlaying = false; const trackTitleElement = document.getElementById('track-title'); trackTitleElement.innerHTML = 'No track'; trackTitleElement.classList.remove('scrolling'); // Remove scrolling animation trackTitleElement.style.removeProperty('--scroll-distance'); // Clear CSS variable document.getElementById('artist-name').textContent = 'Unknown Artist'; document.getElementById('album-name').textContent = 'Unknown Album'; document.getElementById('play-button').textContent = 'âˇ'; document.getElementById('play-button').disabled = true; document.getElementById('stop-button').disabled = true; // Hide loading animation hideLoadingAnimation(); // Show no track message and collapse if expanded document.getElementById('no-track-message').classList.remove('hidden'); if (mediaPlayerExpanded) { toggleMediaPlayerExpansion(); } } function setPlayingState(playing) { isPlaying = playing; const playButton = document.getElementById('play-button'); playButton.textContent = playing ? 'â¸ī¸' : 'âˇ'; } async function handlePlayPause() { // Use new streaming system toggle function togglePlayback(); } async function handleStop() { // Use new streaming system stop function await stopStream(); clearTrack(); } function handleVolumeChange(event) { const volume = event.target.value; updateVolumeSliderAppearance(); // Update HTML5 audio player volume if (audioPlayer) { audioPlayer.volume = volume / 100; } } function updateVolumeSliderAppearance() { const slider = document.getElementById('volume-slider'); const value = slider.value; slider.style.setProperty('--volume-percent', `${value}%`); } function showLoadingAnimation() { document.getElementById('loading-animation').classList.remove('hidden'); } function hideLoadingAnimation() { document.getElementById('loading-animation').classList.add('hidden'); } function setLoadingProgress(percentage) { const loadingAnimation = document.getElementById('loading-animation'); const progressBar = loadingAnimation.querySelector('.loading-progress'); const loadingText = loadingAnimation.querySelector('.loading-text'); loadingAnimation.classList.remove('hidden'); progressBar.style.width = `${percentage}%`; loadingText.textContent = `${Math.round(percentage)}%`; } // =============================== // STREAMING FUNCTIONALITY // =============================== async function startStream(searchResult) { // Start streaming a track - handles same track toggle and new track streaming try { console.log(`đŽ startStream() called with data:`, searchResult); // Check if this is the same track that's currently playing/loading const currentTrackId = currentTrack ? `${currentTrack.username}:${currentTrack.filename}` : null; const newTrackId = `${searchResult.username}:${searchResult.filename}`; console.log(`đŽ startStream() called for: ${searchResult.filename}`); console.log(`đŽ Current track ID: ${currentTrackId}`); console.log(`đŽ New track ID: ${newTrackId}`); if (currentTrackId === newTrackId && audioPlayer && !audioPlayer.paused) { // Same track clicked while playing - toggle pause console.log("đ Toggling playback for same track"); togglePlayback(); return; } // Different track or no current track - start new stream console.log("đĩ Starting new stream"); // Stop current streaming/playback if any await stopStream(); // Set track info and show loading state setTrackInfo({ title: extractTrackTitle(searchResult.filename) || searchResult.title || 'Unknown Track', artist: searchResult.artist || searchResult.username || 'Unknown Artist', album: searchResult.album || 'Unknown Album', username: searchResult.username, filename: searchResult.filename }); showLoadingAnimation(); setLoadingProgress(0); // Start streaming request const response = await fetch(API.stream.start, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(searchResult) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to start streaming'); } console.log("â Stream started successfully"); // Start status polling startStreamStatusPolling(); } catch (error) { console.error('Error starting stream:', error); showToast(`Failed to start stream: ${error.message}`, 'error'); hideLoadingAnimation(); clearTrack(); } } function startStreamStatusPolling() { // Start polling for stream status updates if (streamStatusPoller) { clearInterval(streamStatusPoller); } console.log('đ Starting stream status polling (1-second interval)'); updateStreamStatus(); // Initial check streamStatusPoller = setInterval(updateStreamStatus, 1000); } function stopStreamStatusPolling() { // Stop polling for stream status updates if (streamStatusPoller) { clearInterval(streamStatusPoller); streamStatusPoller = null; console.log('âšī¸ Stopped stream status polling'); } } async function updateStreamStatus() { // Poll server for streaming progress and handle state changes try { const response = await fetch(API.stream.status); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); // Update current stream state currentStream.status = data.status; currentStream.progress = data.progress; switch (data.status) { case 'loading': setLoadingProgress(data.progress); break; case 'queued': // Show queue status const loadingText = document.querySelector('.loading-text'); if (loadingText) { loadingText.textContent = 'Queued...'; } break; case 'ready': // Stream is ready - start audio playback console.log('đĩ Stream ready, starting audio playback'); stopStreamStatusPolling(); await startAudioPlayback(); break; case 'error': console.error('â Streaming error:', data.error_message); stopStreamStatusPolling(); hideLoadingAnimation(); showToast(`Streaming error: ${data.error_message}`, 'error'); clearTrack(); break; } } catch (error) { console.error('Error updating stream status:', error); // Don't clear everything on network errors - might be temporary } } async function startAudioPlayback() { // Start HTML5 audio playback of the streamed file try { if (!audioPlayer) { throw new Error('Audio player not initialized'); } // Set audio source with cache-busting timestamp const audioUrl = `/stream/audio?t=${new Date().getTime()}`; audioPlayer.src = audioUrl; console.log(`đĩ Loading audio from: ${audioUrl}`); // Start playback await audioPlayer.play(); // Update UI to playing state hideLoadingAnimation(); setPlayingState(true); console.log('â Audio playback started successfully'); } catch (error) { console.error('â Error starting audio playback:', error); hideLoadingAnimation(); showToast(`Playback error: ${error.message}`, 'error'); clearTrack(); } } async function stopStream() { // Stop streaming and clean up all state try { // Stop status polling stopStreamStatusPolling(); // Stop audio playback if (audioPlayer) { audioPlayer.pause(); audioPlayer.src = ''; } // Call backend stop endpoint const response = await fetch(API.stream.stop, { method: 'POST' }); if (response.ok) { const data = await response.json(); console.log('đ Stream stopped:', data.message); } // Reset UI state hideLoadingAnimation(); setPlayingState(false); // Reset stream state currentStream = { status: 'stopped', progress: 0, track: null }; } catch (error) { console.error('Error stopping stream:', error); } } function togglePlayback() { // Toggle play/pause for currently loaded audio if (!audioPlayer || !currentTrack) { console.log('â ī¸ No audio player or track to toggle'); return; } if (audioPlayer.paused) { audioPlayer.play() .then(() => { setPlayingState(true); console.log('âļī¸ Resumed playback'); }) .catch(error => { console.error('Error resuming playback:', error); showToast('Failed to resume playback', 'error'); }); } else { audioPlayer.pause(); setPlayingState(false); console.log('â¸ī¸ Paused playback'); } } // =============================== // AUDIO EVENT HANDLERS // =============================== function updateAudioProgress() { // Update progress bar based on audio playback time if (!audioPlayer || !audioPlayer.duration) return; const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100; // TODO: Update progress bar in sidebar when implemented // Update time display if elements exist const currentTimeElement = document.getElementById('current-time'); const totalTimeElement = document.getElementById('total-time'); if (currentTimeElement) { currentTimeElement.textContent = formatTime(audioPlayer.currentTime); } if (totalTimeElement) { totalTimeElement.textContent = formatTime(audioPlayer.duration); } } function onAudioEnded() { // Handle audio playback completion console.log('đ Audio playback ended'); setPlayingState(false); // TODO: Auto-advance to next track if queue exists } function onAudioError(event) { // Handle audio playback errors console.error('â Audio error:', event.target.error); hideLoadingAnimation(); showToast('Audio playback error occurred', 'error'); clearTrack(); } function onAudioLoadStart() { // Handle audio load start console.log('đ Audio loading started'); } function onAudioCanPlay() { // Handle when audio can start playing console.log('â Audio ready to play'); } function formatTime(seconds) { // Format seconds as MM:SS if (!seconds || !isFinite(seconds)) return '0:00'; const minutes = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${minutes}:${secs.toString().padStart(2, '0')}`; } // =============================== // DONATION WIDGET // =============================== function initializeDonationWidget() { const toggleButton = document.getElementById('donation-toggle'); toggleButton.addEventListener('click', toggleDonationAddresses); } function toggleDonationAddresses() { const addresses = document.getElementById('donation-addresses'); const toggleButton = document.getElementById('donation-toggle'); donationAddressesVisible = !donationAddressesVisible; if (donationAddressesVisible) { addresses.classList.remove('hidden'); toggleButton.textContent = 'Hide'; } else { addresses.classList.add('hidden'); toggleButton.textContent = 'Show'; } } function openKofi() { window.open('https://ko-fi.com/boulderbadgedad', '_blank'); console.log('Opening Ko-fi link'); } async function copyAddress(address, cryptoName) { try { await navigator.clipboard.writeText(address); showToast(`${cryptoName} address copied to clipboard`, 'success'); console.log(`Copied ${cryptoName} address: ${address}`); } catch (error) { console.error('Failed to copy address:', error); showToast(`Failed to copy ${cryptoName} address`, 'error'); } } // =============================== // SETTINGS FUNCTIONALITY // =============================== function initializeSettings() { const saveButton = document.getElementById('save-settings'); const mediaServerType = document.getElementById('media-server-type'); saveButton.addEventListener('click', saveSettings); mediaServerType.addEventListener('change', updateMediaServerFields); } async function loadSettingsData() { try { const response = await fetch(API.settings); const settings = await response.json(); // Populate Spotify settings document.getElementById('spotify-client-id').value = settings.spotify?.client_id || ''; document.getElementById('spotify-client-secret').value = settings.spotify?.client_secret || ''; // Populate Tidal settings document.getElementById('tidal-client-id').value = settings.tidal?.client_id || ''; document.getElementById('tidal-client-secret').value = settings.tidal?.client_secret || ''; // Populate Plex settings document.getElementById('plex-url').value = settings.plex?.base_url || ''; document.getElementById('plex-token').value = settings.plex?.token || ''; // Populate Jellyfin settings document.getElementById('jellyfin-url').value = settings.jellyfin?.base_url || ''; document.getElementById('jellyfin-api-key').value = settings.jellyfin?.api_key || ''; // Set active server and toggle visibility const activeServer = settings.active_media_server || 'plex'; toggleServer(activeServer); // Populate Soulseek settings document.getElementById('soulseek-url').value = settings.soulseek?.slskd_url || ''; document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || ''; // Populate Download settings (right column) document.getElementById('preferred-quality').value = settings.settings?.audio_quality || 'flac'; document.getElementById('download-path').value = settings.soulseek?.download_path || './downloads'; document.getElementById('transfer-path').value = settings.soulseek?.transfer_path || './Transfer'; // Populate Database settings document.getElementById('max-workers').value = settings.database?.max_workers || '5'; // Populate Metadata Enhancement settings document.getElementById('metadata-enabled').checked = settings.metadata_enhancement?.enabled !== false; document.getElementById('embed-album-art').checked = settings.metadata_enhancement?.embed_album_art !== false; // Populate Playlist Sync settings document.getElementById('create-backup').checked = settings.playlist_sync?.create_backup !== false; // Populate Logging information (read-only) document.getElementById('log-level-display').textContent = settings.logging?.level || 'INFO'; document.getElementById('log-path-display').textContent = settings.logging?.path || 'logs/app.log'; } catch (error) { console.error('Error loading settings:', error); showToast('Failed to load settings', 'error'); } } function updateMediaServerFields() { const serverType = document.getElementById('media-server-type').value; const urlInput = document.getElementById('media-server-url'); const tokenInput = document.getElementById('media-server-token'); if (serverType === 'plex') { urlInput.placeholder = 'http://localhost:32400'; tokenInput.placeholder = 'Plex Token'; } else { urlInput.placeholder = 'http://localhost:8096'; tokenInput.placeholder = 'Jellyfin API Key'; } } function toggleServer(serverType) { // Update toggle buttons document.getElementById('plex-toggle').classList.remove('active'); document.getElementById('jellyfin-toggle').classList.remove('active'); document.getElementById(`${serverType}-toggle`).classList.add('active'); // Show/hide server containers document.getElementById('plex-container').classList.toggle('hidden', serverType !== 'plex'); document.getElementById('jellyfin-container').classList.toggle('hidden', serverType !== 'jellyfin'); } async function saveSettings() { // Determine active server from toggle buttons const activeServer = document.getElementById('plex-toggle').classList.contains('active') ? 'plex' : 'jellyfin'; const settings = { active_media_server: activeServer, spotify: { client_id: document.getElementById('spotify-client-id').value, client_secret: document.getElementById('spotify-client-secret').value }, tidal: { client_id: document.getElementById('tidal-client-id').value, client_secret: document.getElementById('tidal-client-secret').value }, plex: { base_url: document.getElementById('plex-url').value, token: document.getElementById('plex-token').value }, jellyfin: { base_url: document.getElementById('jellyfin-url').value, api_key: document.getElementById('jellyfin-api-key').value }, soulseek: { slskd_url: document.getElementById('soulseek-url').value, api_key: document.getElementById('soulseek-api-key').value, download_path: document.getElementById('download-path').value, transfer_path: document.getElementById('transfer-path').value }, settings: { audio_quality: document.getElementById('preferred-quality').value }, database: { max_workers: parseInt(document.getElementById('max-workers').value) }, metadata_enhancement: { enabled: document.getElementById('metadata-enabled').checked, embed_album_art: document.getElementById('embed-album-art').checked }, playlist_sync: { create_backup: document.getElementById('create-backup').checked } }; try { showLoadingOverlay('Saving settings...'); const response = await fetch(API.settings, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); const result = await response.json(); if (result.success) { showToast('Settings saved successfully', 'success'); // Trigger immediate status update setTimeout(updateServiceStatus, 1000); } else { showToast(`Failed to save settings: ${result.error}`, 'error'); } } catch (error) { console.error('Error saving settings:', error); showToast('Failed to save settings', 'error'); } finally { hideLoadingOverlay(); } } async function testConnection(service) { try { showLoadingOverlay(`Testing ${service} connection...`); const response = await fetch(API.testConnection, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ service }) }); const result = await response.json(); if (result.success) { showToast(`${service} connection successful`, 'success'); } else { showToast(`${service} connection failed: ${result.error}`, 'error'); } } catch (error) { console.error(`Error testing ${service} connection:`, error); showToast(`Failed to test ${service} connection`, 'error'); } finally { hideLoadingOverlay(); } } // Individual Auto-detect functions - same as GUI async function autoDetectPlex() { try { showLoadingOverlay('Auto-detecting Plex server...'); const response = await fetch('/api/detect-media-server', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server_type: 'plex' }) }); const result = await response.json(); if (result.success) { document.getElementById('plex-url').value = result.found_url; showToast(`Plex server detected: ${result.found_url}`, 'success'); } else { showToast(result.error, 'error'); } } catch (error) { console.error('Error auto-detecting Plex:', error); showToast('Failed to auto-detect Plex server', 'error'); } finally { hideLoadingOverlay(); } } async function autoDetectJellyfin() { try { showLoadingOverlay('Auto-detecting Jellyfin server...'); const response = await fetch('/api/detect-media-server', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server_type: 'jellyfin' }) }); const result = await response.json(); if (result.success) { document.getElementById('jellyfin-url').value = result.found_url; showToast(`Jellyfin server detected: ${result.found_url}`, 'success'); } else { showToast(result.error, 'error'); } } catch (error) { console.error('Error auto-detecting Jellyfin:', error); showToast('Failed to auto-detect Jellyfin server', 'error'); } finally { hideLoadingOverlay(); } } async function autoDetectSlskd() { try { showLoadingOverlay('Auto-detecting Soulseek (slskd) server...'); const response = await fetch('/api/detect-soulseek', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const result = await response.json(); if (result.success) { document.getElementById('soulseek-url').value = result.found_url; showToast(`Soulseek server detected: ${result.found_url}`, 'success'); } else { showToast(result.error, 'error'); } } catch (error) { console.error('Error auto-detecting Soulseek:', error); showToast('Failed to auto-detect Soulseek server', 'error'); } finally { hideLoadingOverlay(); } } function cancelDetection(service) { const progressDiv = document.getElementById(`${service}-detection-progress`); progressDiv.classList.add('hidden'); showToast(`${service} detection cancelled`, 'error'); } function updateStatusDisplays() { // Update status displays based on current service status // This would be called after status updates const services = ['spotify', 'media-server', 'soulseek']; services.forEach(service => { const display = document.getElementById(`${service}-status-display`); if (display) { // Status will be updated by the regular status monitoring } }); } async function authenticateTidal() { try { showLoadingOverlay('Starting Tidal authentication...'); // This would trigger the OAuth flow showToast('Tidal authentication started', 'success'); // In a real implementation, this would open the OAuth URL window.open('/auth/tidal', '_blank'); } catch (error) { console.error('Error authenticating Tidal:', error); showToast('Failed to start Tidal authentication', 'error'); } finally { hideLoadingOverlay(); } } function browsePath(pathType) { showToast(`Path browser not available in web interface. Please enter path manually.`, 'error'); } // =============================== // SEARCH FUNCTIONALITY // =============================== function initializeSearch() { // --- FIX: Corrected the element IDs to match the HTML --- const searchInput = document.getElementById('downloads-search-input'); const searchButton = document.getElementById('downloads-search-btn'); if (searchButton && searchInput) { searchButton.addEventListener('click', performDownloadsSearch); searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') performDownloadsSearch(); }); } } async function performSearch() { const query = document.getElementById('search-input').value.trim(); if (!query) { showToast('Please enter a search term', 'error'); return; } try { showLoadingOverlay('Searching...'); displaySearchResults([]); // Clear previous results 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; } searchResults = data.results || []; displaySearchResults(searchResults); if (searchResults.length === 0) { showToast('No results found', 'error'); } else { showToast(`Found ${searchResults.length} results`, 'success'); } } catch (error) { console.error('Error performing search:', error); showToast('Search failed', 'error'); } finally { hideLoadingOverlay(); } } function displaySearchResults(results) { const resultsContainer = document.getElementById('search-results'); if (!results.length) { resultsContainer.innerHTML = '
No search results found.