let currentTrack = null; let isPlaying = false; let playlist = []; let currentView = 'all'; let playlists = JSON.parse(localStorage.getItem('playlists')) || {}; let currentLyrics = null; let isPlayingPlaylist = false; // Add these cache objects at the top of the file const ALBUM_CACHE = new Map(); const ARTIST_CACHE = new Map(); let lastStructureUpdate = 0; const audioPlayer = document.getElementById('audio-player'); const playBtn = document.getElementById('play-btn'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); const seekSlider = document.getElementById('seek-slider'); const volumeSlider = document.getElementById('volume-slider'); const currentTimeSpan = document.getElementById('current-time'); const durationSpan = document.getElementById('duration'); // Add this constant at the top of the file const DEFAULT_COVER = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNlOWVjZWYiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgc3R5bGU9ImZvbnQtZmFtaWx5OiBBcmlhbDsgZm9udC1zaXplOiAyMHB4OyBmaWxsOiAjNmM3NTdkOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyI+Tm8gQWxidW0gQXJ0PC90ZXh0Pjwvc3ZnPg=='; // Add this function to check if we need to rebuild caches function shouldRebuildCaches() { const now = Date.now(); if (now - lastStructureUpdate > 5000) { // Only rebuild every 5 seconds max lastStructureUpdate = now; return true; } return false; } // Fetch music structure from the server fetch('/api/music-structure') .then(response => response.json()) .then(data => { playlist = flattenPlaylist(data); initializeViews(); }); function flattenPlaylist(structure, prefix = '') { let flat = []; structure.forEach(item => { if (item.type === 'file') { item.fullPath = prefix + item.path; flat.push(item); } else if (item.type === 'folder') { flat = flat.concat(flattenPlaylist(item.contents, item.path + '/')); } }); return flat; } function playTrack(index) { // If playing directly from main playlist, clear playlist context if (!isPlayingPlaylist) { currentPlaylist = null; } currentTrack = index; const track = playlist[index]; if (!track) return; audioPlayer.src = `/music/${track.path}`; audioPlayer.play() .then(() => { updatePlayerInfo(track); updatePlayButton(true); highlightCurrentTrack(); updateActiveLyricLine(audioPlayer.currentTime); }) .catch(error => { console.error('Error playing track:', error); showNotification('Error playing track'); playNext(); }); } function updatePlayerInfo(track) { document.getElementById('track-title').textContent = track.metadata?.title || track.name; document.getElementById('track-artist').textContent = track.metadata?.artist || 'Unknown'; document.getElementById('track-album').textContent = track.metadata?.album || 'Unknown'; // Update album art with error handling const albumArt = document.getElementById('album-art'); if (track.metadata?.artwork) { albumArt.src = track.metadata.artwork; // Add error handling for the album art albumArt.onerror = () => { albumArt.src = DEFAULT_COVER; }; } else { albumArt.src = DEFAULT_COVER; } // Update lyrics currentLyrics = track.metadata?.synchronized_lyrics || null; const lyricsContent = document.getElementById('lyrics-content'); if (track.metadata?.lyrics || currentLyrics) { if (currentLyrics) { displaySyncedLyrics(currentLyrics); } else { lyricsContent.innerHTML = `
${track.metadata.lyrics || ''}
`; } } else { lyricsContent.innerHTML = '
No lyrics available
'; } } function displaySyncedLyrics(lyrics) { const lyricsContent = document.getElementById('lyrics-content'); lyricsContent.innerHTML = lyrics .map((line, index) => `
${line.text}
`) .join(''); // Add click handlers for each line lyricsContent.querySelectorAll('.lyrics-line').forEach(line => { line.addEventListener('click', () => { const time = parseFloat(line.dataset.time); if (!isNaN(time)) { audioPlayer.currentTime = time; audioPlayer.play() .then(() => updatePlayButton(true)) .catch(error => console.error('Error playing:', error)); } }); }); } function updateActiveLyricLine(currentTime) { if (!currentLyrics || !currentLyrics.length) return; // Find the current line by looking for the last lyric before current time let currentLineIndex = -1; for (let i = 0; i < currentLyrics.length; i++) { if (currentLyrics[i].time <= currentTime) { currentLineIndex = i; } else { break; } } // Remove active class from all lines document.querySelectorAll('.lyrics-line').forEach(line => { line.classList.remove('active'); }); // If we found a current line, highlight it and scroll to it if (currentLineIndex >= 0) { const lineElement = document.getElementById(`lyric-line-${currentLineIndex}`); if (lineElement) { lineElement.classList.add('active'); // Improved scrolling logic const container = document.getElementById('lyrics-content'); const containerRect = container.getBoundingClientRect(); const lineRect = lineElement.getBoundingClientRect(); // Only scroll if the line is outside the visible area if (lineRect.top < containerRect.top || lineRect.bottom > containerRect.bottom) { lineElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } } } function updatePlayButton(playing) { isPlaying = playing; playBtn.innerHTML = playing ? '' : ''; } function highlightCurrentTrack() { document.querySelectorAll('.track-list .music-item').forEach((item, index) => { item.classList.toggle('playing', index === currentTrack); }); } // Event Listeners playBtn.onclick = () => { if (currentTrack === null && playlist.length > 0) { playTrack(0); } else if (audioPlayer.paused) { audioPlayer.play(); updatePlayButton(true); } else { audioPlayer.pause(); updatePlayButton(false); } }; prevBtn.onclick = () => { playPrevious(); }; nextBtn.onclick = () => { playNext(); }; volumeSlider.oninput = (e) => { audioPlayer.volume = e.target.value / 100; }; seekSlider.oninput = (e) => { const time = (audioPlayer.duration * e.target.value) / 100; audioPlayer.currentTime = time; }; audioPlayer.ontimeupdate = () => { const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100; seekSlider.value = percent; currentTimeSpan.textContent = formatTime(audioPlayer.currentTime); updateActiveLyricLine(audioPlayer.currentTime); }; audioPlayer.onloadedmetadata = () => { durationSpan.textContent = formatTime(audioPlayer.duration); }; audioPlayer.onended = () => { playNext(); }; function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } function initializeViews() { document.querySelectorAll('.nav-menu a').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const view = e.target.dataset.view; switchView(view); }); }); document.getElementById('search-input').addEventListener('input', (e) => { filterContent(e.target.value); }); document.getElementById('new-playlist-btn').addEventListener('click', showPlaylistModal); document.getElementById('save-playlist').addEventListener('click', savePlaylist); document.getElementById('cancel-playlist').addEventListener('click', hidePlaylistModal); renderPlaylists(); // Initialize with all songs view renderAllSongs(); } function switchView(view) { currentView = view; document.querySelectorAll('.nav-menu a').forEach(link => { link.classList.toggle('active', link.dataset.view === view); }); switch(view) { case 'albums': renderAlbumView(); break; case 'artists': renderArtistView(); break; case 'playlists': renderPlaylistView(); break; default: renderAllSongs(); } } // Optimize renderAlbumView function renderAlbumView() { if (shouldRebuildCaches()) { ALBUM_CACHE.clear(); playlist.forEach(track => { if (track.metadata) { const albumName = track.metadata.album || 'Unknown Album'; const artistName = track.metadata.artist || 'Unknown Artist'; const albumKey = `${albumName}___${artistName}`; if (!ALBUM_CACHE.has(albumKey)) { ALBUM_CACHE.set(albumKey, { name: albumName, artist: artistName, artwork: track.metadata.artwork || DEFAULT_COVER, tracks: [] }); } ALBUM_CACHE.get(albumKey).tracks.push(track); } }); } const container = document.getElementById('view-container'); const albumsHtml = Array.from(ALBUM_CACHE.values()) .map(album => { const escapedName = escapeHtml(album.name); const escapedArtist = escapeHtml(album.artist); return `

${escapedName}

${album.artist}

${album.tracks.length} tracks

`; }).join(''); container.innerHTML = `
${albumsHtml}
`; } // Optimize renderArtistView function renderArtistView() { if (shouldRebuildCaches()) { ARTIST_CACHE.clear(); playlist.forEach(track => { if (track.metadata) { const artistName = track.metadata.artist || 'Unknown Artist'; if (!ARTIST_CACHE.has(artistName)) { ARTIST_CACHE.set(artistName, { name: artistName, artwork: track.metadata.artwork || DEFAULT_COVER, tracks: [] }); } const artist = ARTIST_CACHE.get(artistName); artist.tracks.push(track); if (!artist.artwork && track.metadata.artwork) { artist.artwork = track.metadata.artwork; } } }); } const container = document.getElementById('view-container'); const artistsHtml = Array.from(ARTIST_CACHE.values()) .map(artist => { const escapedName = escapeHtml(artist.name); return `
${escapedName}

${escapedName}

${artist.tracks.length} tracks

`; }).join(''); container.innerHTML = `
${artistsHtml}
`; } // Optimize track filtering functions function getAlbumTracks(albumName, artistName) { const albumKey = `${albumName}___${artistName}`; return ALBUM_CACHE.get(albumKey)?.tracks || []; } function getArtistTracks(artistName) { return ARTIST_CACHE.get(artistName)?.tracks || []; } function showAlbumTracks(albumName, artistName) { const tracks = getAlbumTracks(albumName, artistName); const container = document.getElementById('view-container'); const viewTitle = document.getElementById('view-title'); viewTitle.textContent = `Album: ${albumName}`; const albumArtwork = tracks[0]?.metadata?.artwork || DEFAULT_COVER; container.innerHTML = `
${escapeHtml(albumName)}

${escapeHtml(albumName)}

${artistName}

${tracks.length} tracks

${tracks.map((track, index) => `
${(index + 1).toString().padStart(2, '0')}
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown Artist'}
`).join('')}
`; } function showArtistTracks(artistName) { const tracks = getArtistTracks(artistName); renderTrackList(tracks, `Artist: ${artistName}`); } function renderTrackList(tracks, title) { const container = document.getElementById('view-container'); document.getElementById('view-title').textContent = title; container.innerHTML = `
${tracks.map((track, index) => `
${track.metadata?.title || track.name}
${track.metadata?.artist || 'Unknown'}
`).join('')}
`; } function showTrackMenu(event, trackIndex) { event.stopPropagation(); // Remove any existing menus document.querySelectorAll('.context-menu').forEach(m => m.remove()); const menu = document.createElement('div'); menu.className = 'context-menu'; const track = playlist[trackIndex]; menu.innerHTML = ` `; document.body.appendChild(menu); // Position the menu const rect = event.target.closest('.menu-trigger').getBoundingClientRect(); menu.style.position = 'fixed'; menu.style.left = `${rect.left - menu.offsetWidth + rect.width}px`; menu.style.top = `${rect.bottom}px`; // Close menu when clicking outside document.addEventListener('click', function closeMenu(e) { if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) { menu.remove(); document.removeEventListener('click', closeMenu); } }); } function showPlaylistModal(trackIndex = null) { const modal = document.getElementById('playlist-modal'); modal.dataset.trackIndex = trackIndex !== null ? trackIndex : ''; // Clear previous input document.getElementById('playlist-name').value = ''; // Update modal title and render existing playlists const modalTitle = modal.querySelector('h2'); modalTitle.textContent = trackIndex !== null ? 'Add to Playlist' : 'Create Playlist'; // Render existing playlists const existingPlaylists = modal.querySelector('.existing-playlists'); if (trackIndex !== null) { existingPlaylists.innerHTML = Object.values(playlists).map(playlist => `
${playlist.name} ${playlist.tracks.length} tracks
`).join('') || '
No playlists yet
'; } else { existingPlaylists.innerHTML = ''; } modal.style.display = 'flex'; } function addToExistingPlaylist(playlistName, trackIndex) { const track = playlist[trackIndex]; if (!track) return; if (!playlists[playlistName].tracks.some(t => t.path === track.path)) { playlists[playlistName].tracks.push(track); localStorage.setItem('playlists', JSON.stringify(playlists)); showNotification(`Added to ${playlistName}`); renderPlaylists(); // If we're in playlist view, refresh it if (currentView === 'playlists') { renderPlaylistView(); } } else { showNotification('Track already in playlist'); } hidePlaylistModal(); } function hidePlaylistModal() { document.getElementById('playlist-modal').style.display = 'none'; document.getElementById('playlist-name').value = ''; } function savePlaylist() { const name = document.getElementById('playlist-name').value.trim(); const trackIndex = document.getElementById('playlist-modal').dataset.trackIndex; if (!name) { showNotification('Please enter a playlist name'); return; } if (playlists[name]) { showNotification('Playlist already exists'); return; } // Create new playlist playlists[name] = { name: name, tracks: [] }; // If we're adding a specific track if (trackIndex !== '') { const track = playlist[parseInt(trackIndex)]; playlists[name].tracks.push(track); } localStorage.setItem('playlists', JSON.stringify(playlists)); renderPlaylists(); hidePlaylistModal(); showNotification('Playlist created'); // If we're in playlist view, refresh it if (currentView === 'playlists') { renderPlaylistView(); } } function renderPlaylists() { const container = document.getElementById('playlist-list'); container.innerHTML = Object.values(playlists).map(playlist => `
  • ${playlist.name} ${playlist.tracks.length}
  • `).join(''); } function showPlaylist(name) { if (!playlists[name]) return; currentPlaylist = playlists[name]; isPlayingPlaylist = true; currentView = 'playlist-detail'; const container = document.getElementById('view-container'); const viewTitle = document.getElementById('view-title'); viewTitle.textContent = name; container.innerHTML = `

    ${name}

    ${currentPlaylist.tracks.length} tracks

    ${currentPlaylist.tracks.map((track, index) => `
    ${track.metadata?.title || track.name}
    ${track.metadata?.artist || 'Unknown'}
    `).join('')}
    `; } function playPlaylist(name) { if (!playlists[name] || !playlists[name].tracks.length) return; currentPlaylist = playlists[name]; isPlayingPlaylist = true; // Find the first track in the main playlist const firstTrack = currentPlaylist.tracks[0]; const mainPlaylistIndex = playlist.findIndex(t => t.path === firstTrack.path); if (mainPlaylistIndex !== -1) { playTrack(mainPlaylistIndex); showNotification(`Playing playlist: ${name}`); } } function playPlaylistTrack(playlistName, index) { if (!playlists[playlistName]) return; currentPlaylist = playlists[playlistName]; isPlayingPlaylist = true; const track = currentPlaylist.tracks[index]; // Find the track in the main playlist const mainPlaylistIndex = playlist.findIndex(t => t.path === track.path); if (mainPlaylistIndex !== -1) { playTrack(mainPlaylistIndex); } } function showPlaylistTrackMenu(event, playlistName, index) { event.stopPropagation(); const menu = document.createElement('div'); menu.className = 'context-menu'; menu.innerHTML = ` `; // Remove any existing menus document.querySelectorAll('.context-menu').forEach(m => m.remove()); document.body.appendChild(menu); // Position the menu const rect = event.target.getBoundingClientRect(); menu.style.position = 'fixed'; menu.style.left = `${rect.left}px`; menu.style.top = `${rect.bottom}px`; // Close menu when clicking outside document.addEventListener('click', function closeMenu(e) { if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) { menu.remove(); document.removeEventListener('click', closeMenu); } }); } function playNext() { if (isPlayingPlaylist && currentPlaylist) { // Find current track index in playlist const currentTrackPath = playlist[currentTrack].path; const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath); if (playlistTrackIndex < currentPlaylist.tracks.length - 1) { // Play next track in playlist const nextTrack = currentPlaylist.tracks[playlistTrackIndex + 1]; const mainPlaylistIndex = playlist.findIndex(t => t.path === nextTrack.path); if (mainPlaylistIndex !== -1) { playTrack(mainPlaylistIndex); return; } } else { // End of playlist reached currentPlaylist = null; isPlayingPlaylist = false; updatePlayButton(false); return; } } // If no playlist or playlist ended, play next in main playlist if (currentTrack < playlist.length - 1) { playTrack(currentTrack + 1); } else { updatePlayButton(false); } } // Add this function to handle previous track function playPrevious() { if (isPlayingPlaylist && currentPlaylist) { // Find current track index in playlist const currentTrackPath = playlist[currentTrack].path; const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath); if (playlistTrackIndex > 0) { // Play previous track in playlist const prevTrack = currentPlaylist.tracks[playlistTrackIndex - 1]; const mainPlaylistIndex = playlist.findIndex(t => t.path === prevTrack.path); if (mainPlaylistIndex !== -1) { playTrack(mainPlaylistIndex); return; } } } // If no playlist or at start of playlist, play previous in main playlist if (currentTrack > 0) { playTrack(currentTrack - 1); } } // Add this function to initialize audio player event listeners function initializeAudioPlayer() { audioPlayer.addEventListener('ended', () => { playNext(); }); audioPlayer.addEventListener('error', (e) => { console.error('Audio player error:', e); showNotification('Error playing track'); playNext(); // Skip to next track on error }); // Add this timeupdate listener here instead of in DOMContentLoaded audioPlayer.addEventListener('timeupdate', () => { const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100; seekSlider.value = percent; currentTimeSpan.textContent = formatTime(audioPlayer.currentTime); updateActiveLyricLine(audioPlayer.currentTime); }); } // Update the initialization code document.addEventListener('DOMContentLoaded', function() { initializeAudioPlayer(); document.querySelectorAll('.queue-item-actions button').forEach(button => { button.addEventListener('click', (e) => { e.stopPropagation(); }); }); // Add lyrics toggle handler document.getElementById('lyrics-toggle').addEventListener('click', () => { syncedLyricsMode = !syncedLyricsMode; if (currentTrack !== null) { updatePlayerInfo(playlist[currentTrack]); } }); }); function renderAllSongs() { const container = document.getElementById('view-container'); const viewTitle = document.getElementById('view-title'); viewTitle.textContent = 'All Songs'; container.innerHTML = `
    ${playlist.map((track, index) => `
    ${track.metadata?.title || track.name}
    ${track.metadata?.artist || 'Unknown'}
    `).join('')}
    `; } function filterContent(query) { query = query.toLowerCase(); const filteredTracks = playlist.filter(track => (track.metadata?.title || track.name).toLowerCase().includes(query) || (track.metadata?.artist || 'Unknown').toLowerCase().includes(query) || (track.metadata?.album || 'Unknown').toLowerCase().includes(query) ); const container = document.getElementById('view-container'); const viewTitle = document.getElementById('view-title'); viewTitle.textContent = 'Search Results'; container.innerHTML = `
    ${filteredTracks.map((track, index) => `
    ${track.metadata?.title || track.name}
    ${track.metadata?.artist || 'Unknown'}
    `).join('')}
    `; } function renderPlaylistView() { const container = document.getElementById('view-container'); const viewTitle = document.getElementById('view-title'); viewTitle.textContent = 'Playlists'; container.innerHTML = `
    ${Object.values(playlists).map(playlist => `

    ${playlist.name}

    ${playlist.tracks.length} tracks

    `).join('')}
    `; } function deletePlaylist(event, playlistName) { event.stopPropagation(); if (confirm(`Delete playlist "${playlistName}"?`)) { delete playlists[playlistName]; localStorage.setItem('playlists', JSON.stringify(playlists)); renderPlaylistView(); renderPlaylists(); showNotification('Playlist deleted'); } } function removeFromPlaylist(playlistName, index) { if (!playlists[playlistName]) return; playlists[playlistName].tracks.splice(index, 1); localStorage.setItem('playlists', JSON.stringify(playlists)); // Refresh the current playlist view showPlaylist(playlistName); renderPlaylists(); showNotification('Track removed from playlist'); } function showNotification(message) { const notification = document.createElement('div'); notification.className = 'notification'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('fade-out'); setTimeout(() => notification.remove(), 300); }, 2000); } // Add these new functions to handle playing albums and artists function playAlbum(albumName, artistName) { const albumTracks = getAlbumTracks(albumName, artistName); if (albumTracks.length > 0) { currentPlaylist = { name: albumName, tracks: [...albumTracks] }; isPlayingPlaylist = true; const mainPlaylistIndex = playlist.findIndex(track => track.path === albumTracks[0].path); if (mainPlaylistIndex !== -1) { playTrack(mainPlaylistIndex); showNotification(`Playing album: ${albumName}`); } } } function playArtist(artistName) { const tracks = getArtistTracks(artistName); if (tracks.length > 0) { currentPlaylist = { name: artistName, tracks: [...tracks] }; isPlayingPlaylist = true; const mainPlaylistIndex = playlist.findIndex(track => track.path === tracks[0].path); if (mainPlaylistIndex !== -1) { playTrack(mainPlaylistIndex); showNotification(`Playing artist: ${artistName}`); } } } // Add this new function to handle playing from main playlist function playFromMainPlaylist(index) { isPlayingPlaylist = false; currentPlaylist = null; playTrack(index); } // Add this helper function to escape HTML special characters function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }