Music

Connecting...
music_note
No song playing
Select a song from your library
0:00 0:00
volume_up

Your Music Library

Loading...
🎵
Connect to your Music Server
Configure your Jellyfin/Navidrome server in Settings to access your library
Downloads
⬇️
No Downloads Yet
Songs you download will appear here with real-time progress

Settings

Configure your music server connection
Enter your Jellyfin server URL (protocol will be auto-detected)
cloud_off Not connected

About

This app combines Spotify search and download capabilities with your personal music library. Configure your Jellyfin or Navidrome server to access your existing music collection.

container.innerHTML = `
⬇️
No Downloads Yet
Songs you download will appear here
`; return; } container.innerHTML = ''; allDownloads.forEach(download => { const div = document.createElement('div'); div.className = 'download-item'; div.setAttribute('data-download-url', download.url); const statusClass = `status-${download.status.toLowerCase().replace(/[^a-z]/g, '')}`; const typeIcon = getTypeIcon(download.type); div.innerHTML = `
${typeIcon}
${escapeHtml(download.name || 'Unknown')}
${escapeHtml(download.artist || '')}
${Math.round(download.progress)}% ${escapeHtml(download.status)}
`; applyStageAnimation(div, download.stage); container.appendChild(div); }); } function getTypeIcon(type) { const icons = { track: '🎵', album: '💿', artist: '👤', playlist: '📋' }; return icons[type] || '🎵'; } function clearDownloads() { if (socket) { socket.emit('cancel_all'); } } function openFinamp() { // Try to open Finamp window.location.href = 'finamp://'; // Fallback message setTimeout(() => { showNotification('Open Finamp', 'Launch Finamp to play your music'); }, 1000); } function showNotification(title, message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const iconMap = { 'success': '✓', 'error': '✕', 'warning': '⚠', 'info': 'ℹ' }; toast.innerHTML = `
${iconMap[type] || iconMap.success}
${escapeHtml(title)}
${escapeHtml(message)}
`; container.appendChild(toast); // Trigger animation requestAnimationFrame(() => { toast.classList.add('show'); }); // Auto-remove after 4 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentNode) { container.removeChild(toast); } }, 400); }, 4000); // Add haptic feedback simulation if (navigator.vibrate) { navigator.vibrate(type === 'error' ? [100, 50, 100] : [50]); } } // Enhanced download progress handler with detailed status updates function updateDownloadProgress(data) { const { id, url, name = '', artist = '', type = 'track', status = 'pending', progress = 0, message = '', speed = 0, downloaded = 0, total = 0, timeRemaining = 0, stages = {} } = data; const key = url || id; if (!downloadsMap[key]) { downloadsMap[key] = { id, url, name, artist, type, progress: 0, status: 'pending', stages: { queued: {}, downloading: {}, complete: {} } }; } const download = downloadsMap[key]; // Update download status and progress download.status = status || download.status; download.statusText = message || download.statusText; download.progress = progress; download.speed = speed; download.downloaded = downloaded; download.total = total; download.timeRemaining = timeRemaining; download.timestamp = new Date().toISOString(); // Update individual stage statuses if provided if (stages) { Object.entries(stages).forEach(([stage, stageData]) => { if (download.stages[stage]) { download.stages[stage] = { ...download.stages[stage], ...stageData }; } }); } // Update UI renderDownloads(); // Show notifications for important state changes if (status === 'completed') { showNotification('Download Complete', `"${download.name}" is ready to play`, 'success'); } else if (status === 'failed') { showNotification('Download Failed', `Failed to download "${download.name}"`, 'error'); } else if (status === 'processing') { showNotification('Processing Complete', `Processing "${download.name}" for playback`, 'info'); } } function updateDownloadHistory(history) { console.log('📊 Updating download history:', history); if (Array.isArray(history)) { history.forEach(item => { if (item.url) { updateDownloadProgress(item); } }); } } function renderDownloads() { const container = document.getElementById('downloadsList'); const allDownloads = Object.values(downloadsMap).sort((a, b) => b.progress - a.progress); if (allDownloads.length === 0) { container.innerHTML = `
⬇️
No Downloads Yet
Songs you download will appear here
`; return; } container.innerHTML = ''; allDownloads.forEach(download => { const div = document.createElement('div'); div.className = 'download-item'; div.setAttribute('data-download-url', download.url); const statusClass = `status-${download.status.toLowerCase().replace(/[^a-z]/g, '')}`; const typeIcon = getTypeIcon(download.type); div.innerHTML = `
${typeIcon}
${escapeHtml(download.name || 'Unknown')}
${escapeHtml(download.artist || '')}
${Math.round(download.progress)}% ${escapeHtml(download.status)}
`; applyStageAnimation(div, download.stage); container.appendChild(div); }); } function getTypeIcon(type) { const icons = { track: '🎵', album: '💿', artist: '👤', playlist: '📋' }; return icons[type] || '🎵'; } function clearDownloads() { if (socket) { socket.emit('cancel_all'); } } function openFinamp() { // Try to open Finamp window.location.href = 'finamp://'; // Fallback message setTimeout(() => { showNotification('Open Finamp', 'Launch Finamp to play your music'); }, 1000); } function showNotification(title, message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const iconMap = { 'success': '✓', 'error': '✕', 'warning': '⚠', 'info': 'ℹ' }; toast.innerHTML = `
${iconMap[type] || iconMap.success}
${escapeHtml(title)}
${escapeHtml(message)}
`; container.appendChild(toast); // Trigger animation requestAnimationFrame(() => { toast.classList.add('show'); }); // Auto-remove after 4 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentNode) { container.removeChild(toast); } }, 400); }, 4000); // Add haptic feedback simulation if (navigator.vibrate) { navigator.vibrate(type === 'error' ? [100, 50, 100] : [50]); } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Enhanced download item function with detailed progress tracking function downloadItem(url, type, name, artist) { console.log(`Downloading ${type}: ${name} by ${artist} (${url})`); // Generate a unique ID for this download const id = 'dl_' + Date.now(); // Create download item with detailed status const downloadItem = { id: id, url: url, type: type, name: name, artist: artist, status: 'queued', statusText: 'Waiting in queue', progress: 0, speed: 0, downloaded: 0, total: 0, timeRemaining: 0, timestamp: new Date().toISOString(), stages: { download: { status: 'pending', progress: 0 }, convert: { status: 'pending', progress: 0 }, upload: { status: 'pending', progress: 0 }, scan: { status: 'pending', progress: 0 } } }; // Add to downloads map downloadsMap[url] = downloadItem; // Emit download event to server – ensure socket is connected first const emitDownload = () => { socket.emit('download_item', { url, type, name, artist, id, timestamp: downloadItem.timestamp }); }; if (!socket || socket.disconnected) { console.warn('Socket not connected. Initialising and retrying emit...'); if (!socket) { socket = initializeWebSocket(); registerSocketEventHandlers(socket); window.socket = socket; } socket.once('connect', emitDownload); } else { emitDownload(); } // Show downloads tab and render showTab('downloads'); renderDownloads(); // Show notification showNotification('Download Started', `Added "${name}" to download queue`, 'info'); } function showTab(tabName) { console.log('🔄 Switching to tab:', tabName); // Update top navigation tabs (.nav-tab) document.querySelectorAll('.nav-tab').forEach(tab => { tab.classList.remove('active'); }); // Update bottom navigation items (.nav-item) document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); // Activate the correct bottom nav item using the exact IDs from HTML const bottomNavItem = document.getElementById(`nav${tabName.charAt(0).toUpperCase() + tabName.slice(1)}`); if (bottomNavItem) { bottomNavItem.classList.add('active'); console.log('✅ Activated bottom nav item:', bottomNavItem.id); } else { console.error('❌ Bottom nav item not found for tab:', tabName); // Debug: log all nav items document.querySelectorAll('.nav-item').forEach(item => { console.log('Available nav item:', item.id); }); } // Use event delegation for keyboard accessibility if (window.event && window.event.target && window.event.target.classList && window.event.target.classList.contains('nav-tab')) { window.event.target.classList.add('active'); } else { // Fallback: activate the correct tab button in top nav const tabBtn = Array.from(document.querySelectorAll('.nav-tab')).find(btn => btn.textContent.trim().toLowerCase() === tabName); if (tabBtn) { tabBtn.classList.add('active'); console.log('✅ Activated top nav tab:', tabBtn.textContent); } } // Update tab content document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); content.style.display = 'none'; }); const tabContent = document.getElementById(tabName); if (tabContent) { tabContent.classList.add('active'); tabContent.style.display = 'block'; console.log('✅ Activated tab content:', tabContent.id); } else { console.error('❌ Tab content not found for tab:', tabName); // Debug: log all tab contents document.querySelectorAll('.tab-content').forEach(content => { console.log('Available tab content:', content.id); }); } // Refresh downloads if needed if (tabName === 'downloads' && window.socket) { window.socket.emit('get_status'); } // Load library if switching to library tab if (tabName === 'library') { loadLibrary(); } } // Music Player Functions let currentAudio = null; let currentTrack = null; let isPlaying = false; let currentPlaylist = []; let currentTrackIndex = 0; function playTrack(track) { const audioPlayer = document.getElementById('audioPlayer'); const playPauseBtn = document.getElementById('playPauseBtn'); const trackTitle = document.querySelector('.now-playing-info .track-title'); const trackArtist = document.querySelector('.now-playing-info .track-artist'); const artworkContainer = document.querySelector('.now-playing-artwork'); if (currentTrack && currentTrack.id === track.id && !audioPlayer.paused) { // Pause current track audioPlayer.pause(); isPlaying = false; playPauseBtn.innerHTML = 'play_arrow'; artworkContainer.classList.remove('playing'); return; } currentTrack = track; trackTitle.textContent = track.name || 'Unknown Track'; trackArtist.textContent = track.artist || 'Unknown Artist'; // Update artwork with smooth transition artworkContainer.style.transition = 'all 0.4s ease'; if (track.image && track.image !== '') { artworkContainer.innerHTML = `${track.name}`; } else { artworkContainer.innerHTML = `
music_note
`; } // Load and play the track if (track.url) { audioPlayer.src = track.url; audioPlayer.load(); // Show loading state playPauseBtn.innerHTML = 'hourglass_empty'; audioPlayer.play().then(() => { isPlaying = true; playPauseBtn.innerHTML = 'pause'; artworkContainer.classList.add('playing'); showNotification('♪ Now Playing', `${track.name} by ${track.artist}`, 'info'); // Auto-switch to Listen tab with smooth transition showTab('listen'); }).catch(error => { console.error('Error playing track:', error); playPauseBtn.innerHTML = 'play_arrow'; showNotification('Playback Error', 'Could not play the selected track', 'error'); }); } else { showNotification('Error', 'No stream URL available for this track', 'error'); } } // Music Player State let isShuffleMode = false; let repeatMode = 'off'; // 'off', 'one', 'all' function togglePlayPause() { const audioPlayer = document.getElementById('audioPlayer'); const playPauseBtn = document.getElementById('playPauseBtn'); const artworkContainer = document.querySelector('.now-playing-artwork'); if (!currentTrack) { showNotification('No Track', 'Please select a song from your library', 'info'); return; } if (audioPlayer.paused) { audioPlayer.play().then(() => { isPlaying = true; playPauseBtn.innerHTML = 'pause'; if (artworkContainer) artworkContainer.classList.add('playing'); updateMediaSession(); }).catch(error => { console.error('Error playing:', error); showNotification('Playback Error', 'Could not resume playback', 'error'); }); } else { audioPlayer.pause(); isPlaying = false; playPauseBtn.innerHTML = 'play_arrow'; if (artworkContainer) artworkContainer.classList.remove('playing'); } } function playTrack(track) { if (!track) return; console.log('🎵 Playing track:', track); currentTrack = track; const audioPlayer = document.getElementById('audioPlayer'); const trackTitle = document.querySelector('.track-title'); const trackArtist = document.querySelector('.track-artist'); const artworkContainer = document.querySelector('.now-playing-artwork'); const playPauseBtn = document.getElementById('playPauseBtn'); // Update UI if (trackTitle) trackTitle.textContent = track.Name || track.title || 'Unknown Title'; if (trackArtist) trackArtist.textContent = track.Artists?.[0] || track.artist || 'Unknown Artist'; // Update artwork if (artworkContainer) { const artwork = artworkContainer.querySelector('.artwork-placeholder'); if (track.ImageTags?.Primary && track.ServerId) { const serverUrl = localStorage.getItem('musicServerUrl'); const imageUrl = `${serverUrl}/Items/${track.Id}/Images/Primary?maxHeight=400&maxWidth=400&quality=96`; const img = document.createElement('img'); img.src = imageUrl; img.onload = () => { artwork.innerHTML = ''; artwork.appendChild(img); }; img.onerror = () => { artwork.innerHTML = 'music_note'; }; } else { artwork.innerHTML = 'music_note'; } } // Set audio source if (audioPlayer && track.Id) { const serverUrl = localStorage.getItem('musicServerUrl'); const accessToken = localStorage.getItem('jellyfinAccessToken'); audioPlayer.src = `${serverUrl}/Audio/${track.Id}/stream?api_key=${accessToken}`; audioPlayer.load(); audioPlayer.play().then(() => { isPlaying = true; if (playPauseBtn) playPauseBtn.innerHTML = 'pause'; if (artworkContainer) artworkContainer.classList.add('playing'); updateMediaSession(); }).catch(error => { console.error('Error playing track:', error); showNotification('Playback Error', 'Could not play this track', 'error'); }); } } function addToQueue(track) { if (!currentPlaylist.find(t => t.Id === track.Id)) { currentPlaylist.push(track); showNotification('Added to Queue', `"${track.Name}" added to queue`, 'info'); } } function playNext(track) { const nextIndex = currentTrackIndex + 1; currentPlaylist.splice(nextIndex, 0, track); showNotification('Playing Next', `"${track.Name}" will play next`, 'info'); } function previousTrack() { if (currentPlaylist.length === 0) { showNotification('No Playlist', 'Add songs to create a playlist', 'info'); return; } const audioPlayer = document.getElementById('audioPlayer'); // If more than 3 seconds into the song, restart it if (audioPlayer.currentTime > 3) { audioPlayer.currentTime = 0; return; } // Otherwise go to previous track if (isShuffleMode) { currentTrackIndex = Math.floor(Math.random() * currentPlaylist.length); } else { currentTrackIndex = (currentTrackIndex - 1 + currentPlaylist.length) % currentPlaylist.length; } playTrack(currentPlaylist[currentTrackIndex]); } function nextTrack() { if (currentPlaylist.length === 0) { showNotification('No Playlist', 'Add songs to create a playlist', 'info'); return; } if (isShuffleMode) { currentTrackIndex = Math.floor(Math.random() * currentPlaylist.length); } else { currentTrackIndex = (currentTrackIndex + 1) % currentPlaylist.length; } playTrack(currentPlaylist[currentTrackIndex]); } function onTrackEnded() { const audioPlayer = document.getElementById('audioPlayer'); const playPauseBtn = document.getElementById('playPauseBtn'); const artworkContainer = document.querySelector('.now-playing-artwork'); if (repeatMode === 'one') { audioPlayer.currentTime = 0; audioPlayer.play(); return; } if (repeatMode === 'all' || currentTrackIndex < currentPlaylist.length - 1) { nextTrack(); } else { // End of playlist isPlaying = false; if (playPauseBtn) playPauseBtn.innerHTML = 'play_arrow'; if (artworkContainer) artworkContainer.classList.remove('playing'); } } function updateMediaSession() { if ('mediaSession' in navigator && currentTrack) { navigator.mediaSession.metadata = new MediaMetadata({ title: currentTrack.Name || 'Unknown Title', artist: currentTrack.Artists?.[0] || 'Unknown Artist', album: currentTrack.Album || 'Unknown Album', artwork: currentTrack.ImageTags?.Primary ? [{ src: `${localStorage.getItem('musicServerUrl')}/Items/${currentTrack.Id}/Images/Primary?maxHeight=512&maxWidth=512&quality=96`, sizes: '512x512', type: 'image/jpeg' }] : [] }); navigator.mediaSession.setActionHandler('play', togglePlayPause); navigator.mediaSession.setActionHandler('pause', togglePlayPause); navigator.mediaSession.setActionHandler('previoustrack', previousTrack); navigator.mediaSession.setActionHandler('nexttrack', nextTrack); } } function formatTime(seconds) { if (isNaN(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return mins + ':' + (secs < 10 ? '0' : '') + secs; } // Shuffle and Repeat Functions function toggleShuffle() { isShuffleMode = !isShuffleMode; const shuffleBtn = document.getElementById('shuffleBtn'); if (shuffleBtn) { if (isShuffleMode) { shuffleBtn.classList.add('active'); showNotification('Shuffle On', 'Songs will play in random order', 'info'); } else { shuffleBtn.classList.remove('active'); showNotification('Shuffle Off', 'Songs will play in order', 'info'); } } // Save shuffle preference localStorage.setItem('playerShuffle', isShuffleMode.toString()); } function toggleRepeat() { const modes = ['off', 'all', 'one']; const currentIndex = modes.indexOf(repeatMode); repeatMode = modes[(currentIndex + 1) % modes.length]; const repeatBtn = document.getElementById('repeatBtn'); if (repeatBtn) { repeatBtn.classList.remove('active', 'repeat-one'); switch (repeatMode) { case 'all': repeatBtn.classList.add('active'); showNotification('Repeat All', 'All songs will repeat', 'info'); break; case 'one': repeatBtn.classList.add('active', 'repeat-one'); repeatBtn.innerHTML = 'repeat_one'; showNotification('Repeat One', 'Current song will repeat', 'info'); break; default: repeatBtn.innerHTML = 'repeat'; showNotification('Repeat Off', 'Songs will not repeat', 'info'); break; } } // Save repeat preference localStorage.setItem('playerRepeat', repeatMode); } // Restore player preferences function restorePlayerPreferences() { const savedShuffle = localStorage.getItem('playerShuffle'); const savedRepeat = localStorage.getItem('playerRepeat'); if (savedShuffle === 'true') { isShuffleMode = true; const shuffleBtn = document.getElementById('shuffleBtn'); if (shuffleBtn) shuffleBtn.classList.add('active'); } if (savedRepeat) { repeatMode = savedRepeat; const repeatBtn = document.getElementById('repeatBtn'); if (repeatBtn) { repeatBtn.classList.remove('active', 'repeat-one'); if (repeatMode === 'all') { repeatBtn.classList.add('active'); } else if (repeatMode === 'one') { repeatBtn.classList.add('active', 'repeat-one'); repeatBtn.innerHTML = 'repeat_one'; } } } } // Library Functions // Global library data storage window.libraryData = []; function loadLibrary() { const libraryContent = document.getElementById('libraryContent'); const libraryCount = document.getElementById('libraryCount'); // Check if we have server settings const serverUrl = localStorage.getItem('musicServerUrl'); const username = localStorage.getItem('musicServerUsername'); const password = localStorage.getItem('musicServerPassword'); if (!serverUrl || !username) { libraryContent.innerHTML = `
🎵
Connect to your Music Server
Configure your Jellyfin/Navidrome server in Settings to access your library
`; return; } libraryContent.innerHTML = `
Connecting to your music server...
`; // Always attempt authentication first if we have credentials if (password) { console.log('🔐 Attempting authentication before loading library...'); autoAuthenticateAndLoadLibrary(); } else { showAuthenticationRequired('Password not found in storage'); } } async function loadLibraryData() { const libraryContent = document.getElementById('libraryContent'); const libraryCount = document.getElementById('libraryCount'); if (!libraryContent || !libraryCount) { console.error('Library elements not found'); return; } libraryContent.innerHTML = `
Loading your music library...
`; try { console.log('🎵 Starting library data load...'); // Load both songs (Audio) and albums (MusicAlbum) concurrently const [audioItems, albumItems] = await Promise.all([ fetchLibraryItems('Audio'), fetchLibraryItems('MusicAlbum') ]); console.log('📁 Loaded audio items:', audioItems ? audioItems.length : 0); console.log('📁 Loaded album items:', albumItems ? albumItems.length : 0); // Combine all items safely const safeAudioItems = Array.isArray(audioItems) ? audioItems : []; const safeAlbumItems = Array.isArray(albumItems) ? albumItems : []; // Update the global library data variable window.libraryData = [...safeAudioItems, ...safeAlbumItems]; console.log('🎵 Combined library items:', window.libraryData.length); if (window.libraryData.length > 0) { // Display based on the current filter (default to songs) const activeFilter = document.querySelector('[data-library-type].active'); const filterType = activeFilter ? activeFilter.getAttribute('data-library-type') : 'songs'; console.log('📺 Displaying items with filter:', filterType); displayLibraryItems(window.libraryData, filterType); // Update the count libraryCount.textContent = `${safeAudioItems.length} songs, ${safeAlbumItems.length} albums`; } else { console.log('📭 No library items found'); displayLibraryItems([], 'songs'); libraryCount.textContent = '0 items'; } } catch (error) { console.error('Error loading library data:', error); console.error('Error stack:', error.stack); // Safe fallback if (typeof displayLibraryItems === 'function') { displayLibraryItems([], 'songs'); } if (libraryCount) { libraryCount.textContent = '0 items'; } if (typeof showConnectionError === 'function') { showConnectionError(); } } } function fetchLibraryItems(itemType) { return new Promise((resolve, reject) => { // Connect directly to real Jellyfin server fetch(`/api/jellyfin/library?type=${itemType}&limit=200`) .then(response => response.json()) .then(data => { if (data.success && data.items) { console.log(`✅ Loaded ${data.items.length} ${itemType} items from Jellyfin`); resolve(data.items); } else { console.warn(`No ${itemType} items found:`, data.error || 'Unknown error'); resolve([]); } }) .catch(error => { console.error(`Error fetching ${itemType} items:`, error); resolve([]); }); }); } function autoAuthenticateAndLoadLibrary() { const serverUrl = localStorage.getItem('musicServerUrl'); const username = localStorage.getItem('musicServerUsername'); const password = localStorage.getItem('musicServerPassword'); if (!serverUrl || !username || !password) { showAuthenticationRequired(); return; } fetch('/api/jellyfin/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ server_url: serverUrl, username: username, password: password }) }) .then(response => response.json()) .then(data => { if (data.success) { console.log('✅ Auto-authentication successful, loading library...'); localStorage.setItem('musicServerConnected', 'true'); if (data.server_info && data.server_info.name) { localStorage.setItem('musicServerName', data.server_info.name); } // Show demo account notification if using demo credentials if (localStorage.getItem('isDemoAccount') === 'true') { setTimeout(() => { showNotification( 'Demo Account Active', 'You\'re using demo credentials. Configure your own Jellyfin server in Settings for full access.', 'info', 8000 // Show for 8 seconds ); }, 2000); // Wait 2 seconds after library loads } loadLibraryData(); } else { console.warn('❌ Auto-authentication failed:', data.error); localStorage.removeItem('musicServerConnected'); showAuthenticationRequired(data.error); } }) .catch(error => { console.error('Auto-authentication error:', error); localStorage.removeItem('musicServerConnected'); showConnectionError(); }); } function showAuthenticationRequired(errorMessage = null) { const libraryContent = document.getElementById('libraryContent'); libraryContent.innerHTML = `
🔧
Authentication Required
${errorMessage || 'Please go to Settings and connect to your server first'}
`; } function showConnectionError() { const libraryContent = document.getElementById('libraryContent'); libraryContent.innerHTML = `
❌
Connection Error
Unable to connect to your music server. Please check your settings.
`; } function displayLibraryItems(items, filter = 'songs') { const libraryContent = document.getElementById('libraryContent'); if (!items || items.length === 0) { libraryContent.innerHTML = `
🎵
No Music Found
Your library appears to be empty
`; return; } let filteredItems = items; // Filter the items based on the selected category and Jellyfin API structure filteredItems = items.filter(item => { if (!item || !item.Type) { return false; } switch (filter) { case 'songs': return item.Type === 'Audio'; case 'albums': return item.Type === 'MusicAlbum'; case 'artists': return item.Type === 'MusicArtist'; case 'playlists': return item.Type === 'Playlist'; default: return item.Type === 'Audio'; // Default to songs } }); if (filteredItems.length === 0) { libraryContent.innerHTML = `
🎵
No ${filter} Found
Try selecting a different category
`; return; } console.log(`📱 Displaying ${filteredItems.length} ${filter}`); let html = '
'; filteredItems.forEach(item => { const serverUrl = localStorage.getItem('musicServerUrl'); const itemName = item.Name || 'Unknown'; const itemArtist = item.Artists?.[0] || item.AlbumArtist || 'Unknown Artist'; const itemId = item.Id; // Get artwork URL let artworkUrl = ''; if (item.ImageTags?.Primary) { artworkUrl = `${serverUrl}/Items/${itemId}/Images/Primary?maxHeight=300&maxWidth=300&quality=96`; } // Create item based on type if (filter === 'songs' && item.Type === 'Audio') { html += `
${artworkUrl ? `${itemName}` : 'music_note'}
play_arrow
${escapeHtml(itemName)}
${escapeHtml(itemArtist)}
${item.RunTimeTicks ? `
${formatDuration(Math.round(item.RunTimeTicks / 10000))}
` : ''}
`; } else if (filter === 'albums' && item.Type === 'MusicAlbum') { html += `
${artworkUrl ? `${itemName}` : 'album'}
play_arrow
${escapeHtml(itemName)}
${escapeHtml(itemArtist)}
${item.ChildCount ? `
${item.ChildCount} tracks
` : ''}
`; } }); html += '
'; libraryContent.innerHTML = html; } function filterLibrary(type) { document.querySelectorAll('[data-library-type]').forEach(btn => { btn.classList.remove('active'); }); document.querySelector(`[data-library-type="${type}"]`).classList.add('active'); displayLibraryItems(libraryData, type); } function filterLibrary(type) { console.log('🔍 Filtering library by type:', type); // Update active filter button document.querySelectorAll('[data-library-type]').forEach(btn => { btn.classList.remove('active'); }); const activeBtn = document.querySelector(`[data-library-type="${type}"]`); if (activeBtn) { activeBtn.classList.add('active'); } // Display filtered items if (window.libraryData && window.libraryData.length > 0) { displayLibraryItems(window.libraryData, type); } else { loadLibrary(); // Reload if no data } } // Library Playback Functions async function playLibrarySong(itemId) { console.log('🎵 Playing library song:', itemId); try { const track = window.libraryData.find(item => item.Id === itemId); if (!track) { showNotification('Error', 'Track not found in library', 'error'); return; } // Add to current playlist if not already there if (!currentPlaylist.find(t => t.Id === itemId)) { currentPlaylist = [track]; currentTrackIndex = 0; } playTrack(track); } catch (error) { console.error('Error playing library song:', error); showNotification('Playback Error', 'Could not play the selected song', 'error'); } } async function playAlbum(albumId) { console.log('💿 Playing album:', albumId); try { // Get album tracks const response = await fetch(`/api/jellyfin/album/${albumId}/tracks`); if (!response.ok) { throw new Error('Failed to load album tracks'); } const tracks = await response.json(); if (!tracks || tracks.length === 0) { showNotification('Error', 'No tracks found in this album', 'error'); return; } // Set current playlist to album tracks and start playing currentPlaylist = tracks; currentTrackIndex = 0; playTrack(tracks[0]); showNotification('Now Playing', `Playing album: ${tracks[0].Album}`, 'success'); } catch (error) { console.error('Error playing album:', error); showNotification('Playback Error', 'Could not play the selected album', 'error'); } } function addToQueueById(itemId) { console.log('➕ Adding to queue:', itemId); const track = window.libraryData.find(item => item.Id === itemId); if (track) { addToQueue(track); } else { showNotification('Error', 'Track not found in library', 'error'); } } function playNextById(itemId) { console.log('⏭️ Playing next:', itemId); const track = window.libraryData.find(item => item.Id === itemId); if (track) { playNext(track); } else { showNotification('Error', 'Track not found in library', 'error'); } } // Settings Functions function saveSettings() { const serverType = document.getElementById('serverType').value; const serverUrl = document.getElementById('serverUrl').value; const username = document.getElementById('username').value; const password = document.getElementById('password').value; if (!serverUrl || !username || !password) { showNotification('Error', 'Please fill in all required fields', 'error'); return; } // Save to localStorage localStorage.setItem('musicServerType', serverType); localStorage.setItem('musicServerUrl', serverUrl); localStorage.setItem('musicServerUsername', username); localStorage.setItem('musicServerPassword', password); localStorage.setItem('userConfiguredServer', 'true'); // Mark that user configured their own server localStorage.removeItem('isDemoAccount'); // Remove demo account flag showNotification('Settings Saved', 'Your music server settings have been saved', 'success'); // Auto-test connection after saving testConnection(); } function resetSettings() { if (confirm('Are you sure you want to reset all settings? This will clear your saved server credentials.')) { // Clear all music server related localStorage localStorage.removeItem('musicServerType'); localStorage.removeItem('musicServerUrl'); localStorage.removeItem('musicServerUsername'); localStorage.removeItem('musicServerPassword'); localStorage.removeItem('musicServerConnected'); localStorage.removeItem('musicServerName'); localStorage.removeItem('userConfiguredServer'); // Clear user configuration flag localStorage.removeItem('isDemoAccount'); // Clear demo account flag // Reload the settings form with defaults loadSettings(); // Update connection status const statusIndicator = document.querySelector('#settingsConnectionStatus .status-indicator'); statusIndicator.innerHTML = ` cloud_off Not connected `; statusIndicator.classList.remove('connected'); showNotification('Settings Reset', 'All settings have been cleared', 'info'); // Reload the page to recheck for demo mode setTimeout(() => { window.location.reload(); }, 1000); } } function loadSettings() { // Default server configuration from environment const defaultServerUrl = 'https://jellyfin.samcloud.ca'; const serverType = localStorage.getItem('musicServerType') || 'jellyfin'; const serverUrl = localStorage.getItem('musicServerUrl') || defaultServerUrl; const username = localStorage.getItem('musicServerUsername') || ''; const password = localStorage.getItem('musicServerPassword') || ''; const isConnected = localStorage.getItem('musicServerConnected') === 'true'; const serverName = localStorage.getItem('musicServerName') || ''; document.getElementById('serverType').value = serverType; document.getElementById('serverUrl').value = serverUrl; document.getElementById('username').value = username; document.getElementById('password').value = password; // Update connection status if we have saved credentials const statusIndicator = document.querySelector('#settingsConnectionStatus .status-indicator'); if (isConnected && serverUrl && username && password && serverName) { statusIndicator.innerHTML = ` check_circle Connected to ${serverName} `; statusIndicator.classList.add('connected'); // Auto-authenticate on page load to refresh the session setTimeout(() => { autoAuthenticate(); }, 1000); } else { statusIndicator.innerHTML = ` cloud_off Not connected `; statusIndicator.classList.remove('connected'); } } function autoAuthenticate() { const serverUrl = localStorage.getItem('musicServerUrl'); const username = localStorage.getItem('musicServerUsername'); const password = localStorage.getItem('musicServerPassword'); if (!serverUrl || !username || !password) { return; } fetch('/api/jellyfin/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ server_url: serverUrl, username: username, password: password }) }) .then(response => response.json()) .then(data => { if (data.success) { console.log('✅ Auto-authentication successful'); localStorage.setItem('musicServerConnected', 'true'); localStorage.setItem('musicServerName', data.server_info.name); } else { console.warn('❌ Auto-authentication failed:', data.error); localStorage.removeItem('musicServerConnected'); // Update UI to show disconnected state const statusIndicator = document.querySelector('#settingsConnectionStatus .status-indicator'); if (statusIndicator) { statusIndicator.innerHTML = ` error Connection expired - please reconnect `; statusIndicator.classList.remove('connected'); } } }) .catch(error => { console.error('Auto-authentication error:', error); localStorage.removeItem('musicServerConnected'); }); } function testConnection() { const serverUrl = document.getElementById('serverUrl').value; const username = document.getElementById('username').value; const password = document.getElementById('password').value; const statusIndicator = document.querySelector('#settingsConnectionStatus .status-indicator'); if (!serverUrl || !username || !password) { showNotification('Error', 'Please fill in all required fields first', 'error'); return; } statusIndicator.innerHTML = ` sync Testing connection... `; statusIndicator.classList.remove('connected'); // Show detailed progress const progressSteps = [ 'Detecting protocol...', 'Connecting to server...', 'Verifying credentials...', 'Authenticating...' ]; let stepIndex = 0; const progressInterval = setInterval(() => { if (stepIndex < progressSteps.length) { statusIndicator.innerHTML = ` sync ${progressSteps[stepIndex]} `; stepIndex++; } else { clearInterval(progressInterval); } }, 1000); fetch('/api/jellyfin/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ server_url: serverUrl, username: username, password: password }) }) .then(response => response.json()) .then(data => { clearInterval(progressInterval); if (data.success) { statusIndicator.innerHTML = ` check_circle Connected to ${data.server_info.name} `; statusIndicator.classList.add('connected'); showNotification('🎉 Connected!', `Successfully connected to ${data.server_info.name} (${data.server_info.version})`, 'success'); // Save credentials to localStorage for persistence localStorage.setItem('musicServerUrl', serverUrl); localStorage.setItem('musicServerUsername', username); localStorage.setItem('musicServerPassword', password); localStorage.setItem('musicServerConnected', 'true'); localStorage.setItem('musicServerName', data.server_info.name); // Refresh library if we're on the library tab if (document.getElementById('library').classList.contains('active')) { loadLibrary(); } } else { statusIndicator.innerHTML = ` error Connection failed `; statusIndicator.classList.remove('connected'); showNotification('❌ Connection Failed', data.error || 'Unknown error occurred', 'error'); // Clear stored credentials on failure localStorage.removeItem('musicServerConnected'); } }) .catch(error => { clearInterval(progressInterval); console.error('Connection test error:', error); statusIndicator.innerHTML = ` cloud_off Connection error `; statusIndicator.classList.remove('connected'); showNotification('Error', 'Failed to test connection - check your network', 'error'); }); } // Enhanced Download Functions let downloadHistory = []; let downloadStatsVisible = false; function loadDownloadStats() { // This would typically fetch from the backend // For now, we'll calculate from the current download history updateDownloadStats(); } function updateDownloadStats() { const stats = calculateDownloadStats(); document.getElementById('totalDownloads').textContent = stats.total; document.getElementById('completedDownloads').textContent = stats.completed; document.getElementById('failedDownloads').textContent = stats.failed; document.getElementById('successRate').textContent = stats.successRate + '%'; } function calculateDownloadStats() { const total = downloadHistory.length; const completed = downloadHistory.filter(d => d.stage === 'completed').length; const failed = downloadHistory.filter(d => d.stage === 'error').length; const pending = downloadHistory.filter(d => d.stage === 'queued').length; const downloading = downloadHistory.filter(d => d.stage === 'downloading').length; return { total, completed, failed, pending, downloading, successRate: total > 0 ? Math.round((completed / total) * 100) : 0 }; } function showDownloadStats() { const statsElement = document.getElementById('downloadStats'); downloadStatsVisible = !downloadStatsVisible; if (downloadStatsVisible) { statsElement.style.display = 'block'; updateDownloadStats(); } else { statsElement.style.display = 'none'; } } function clearCompletedDownloads() { if (confirm('Clear all completed and failed downloads?')) { downloadHistory = downloadHistory.filter(d => d.stage !== 'completed' && d.stage !== 'error' ); displayDownloads(downloadHistory); updateDownloadStats(); showNotification('Cleared', 'Completed downloads have been cleared', 'success'); } } function filterDownloads(filter) { document.querySelectorAll('[data-download-filter]').forEach(btn => { btn.classList.remove('active'); }); document.querySelector(`[data-download-filter="${filter}"]`).classList.add('active'); let filteredDownloads = downloadHistory; if (filter !== 'all') { filteredDownloads = downloadHistory.filter(download => { switch (filter) { case 'downloading': return download.stage === 'downloading'; case 'completed': return download.stage === 'completed'; case 'failed': return download.stage === 'error'; case 'pending': return download.stage === 'queued'; default: return true; } }); } displayDownloads(filteredDownloads); } function displayDownloads(downloads) { const downloadsList = document.getElementById('downloadsList'); if (!downloads || downloads.length === 0) { downloadsList.innerHTML = `
⬇️
No Downloads
Downloads will appear here with real-time progress
`; return; } const downloadsHtml = downloads.map(download => { const progress = download.progress || 0; const timestamp = download.timestamp ? new Date(download.timestamp * 1000).toLocaleString() : ''; const statusClass = getStatusClass(download.stage); const statusText = getStatusText(download.stage); return `
music_note
${download.name || 'Unknown Track'}
${download.artist || 'Unknown Artist'}
${download.status || statusText} ${progress}%
${timestamp ? `
${timestamp}
` : ''}
${statusText}
`; }).join(''); downloadsList.innerHTML = downloadsHtml; } function getStatusClass(stage) { switch (stage) { case 'downloading': return 'status-downloading'; case 'completed': return 'status-completed'; case 'error': return 'status-failed'; case 'queued': return 'status-pending'; default: return 'status-pending'; } } function getStatusText(stage) { switch (stage) { case 'downloading': return 'Downloading'; case 'completed': return 'Completed'; case 'error': return 'Failed'; case 'queued': return 'Pending'; default: return 'Unknown'; } } function applyStageAnimation(element, stage) { if (!element) return; // Remove existing stage classes element.classList.remove('stage-queued', 'stage-downloading', 'stage-completed', 'stage-error'); // Add current stage class element.classList.add(`stage-${stage}`); // Apply specific animations based on stage switch (stage) { case 'downloading': element.classList.add('pulse-animation'); break; case 'completed': element.classList.remove('pulse-animation'); element.classList.add('success-flash'); setTimeout(() => element.classList.remove('success-flash'), 1000); break; case 'error': element.classList.remove('pulse-animation'); element.classList.add('error-shake'); setTimeout(() => element.classList.remove('error-shake'), 500); break; case 'queued': default: element.classList.remove('pulse-animation'); break; } } // Update the existing WebSocket event handlers to use the new download display if (window.socket) { socket.on('download_progress', function(data) { // Update or add to download history const existingIndex = downloadHistory.findIndex(d => d.id === data.id); if (existingIndex >= 0) { downloadHistory[existingIndex] = { ...downloadHistory[existingIndex], ...data }; } else { downloadHistory.unshift(data); } // Update display if downloads tab is active if (document.getElementById('downloads').classList.contains('active')) { const currentFilter = document.querySelector('[data-download-filter].active').dataset.downloadFilter; filterDownloads(currentFilter); } updateDownloadStats(); }); socket.on('status', function(data) { if (data.downloads) { downloadHistory = data.downloads; if (document.getElementById('downloads').classList.contains('active')) { const currentFilter = document.querySelector('[data-download-filter].active').dataset.downloadFilter; filterDownloads(currentFilter); } updateDownloadStats(); } }); }