// Web Player JavaScript let tracks = []; let currentTrack = null; let currentTrackIndex = -1; let playQueue = []; let isShuffled = false; let isRepeating = false; let audioPlayer = null; // Pagination variables for track library let libraryCurrentPage = 1; let libraryTracksPerPage = 20; let filteredLibraryTracks = []; document.addEventListener('DOMContentLoaded', function() { audioPlayer = document.getElementById('audio-player'); loadTracks(); loadPlaylists(); setupEventListeners(); updatePlayerDisplay(); updateVolume() }); function setupEventListeners() { // Search document.getElementById('search-tracks').addEventListener('input', filterTracks); // Player controls document.getElementById('play-pause-btn').addEventListener('click', togglePlayPause); document.getElementById('prev-btn').addEventListener('click', playPrevious); document.getElementById('next-btn').addEventListener('click', playNext); document.getElementById('shuffle-btn').addEventListener('click', toggleShuffle); document.getElementById('repeat-btn').addEventListener('click', toggleRepeat); // Volume control document.getElementById('volume-slider').addEventListener('input', updateVolume); // Audio player events if (audioPlayer) { audioPlayer.addEventListener('loadedmetadata', updateTimeDisplay); audioPlayer.addEventListener('timeupdate', updateTimeDisplay); audioPlayer.addEventListener('ended', handleTrackEnd); audioPlayer.addEventListener('play', () => updatePlayButton('⏸️ Pause')); audioPlayer.addEventListener('pause', () => updatePlayButton('▶️ Play')); } // Playlist controls document.getElementById('create-playlist').addEventListener('click', createPlaylist); document.getElementById('clear-queue').addEventListener('click', clearQueue); document.getElementById('save-queue').addEventListener('click', saveQueueAsPlaylist); } async function loadTracks() { try { const response = await fetch('/asteroid/api/tracks'); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); if (data.status === 'success') { tracks = data.tracks || []; displayTracks(tracks); } } catch (error) { console.error('Error loading tracks:', error); document.getElementById('track-list').innerHTML = '
Error loading tracks
'; } } function displayTracks(trackList) { filteredLibraryTracks = trackList; libraryCurrentPage = 1; renderLibraryPage(); } function renderLibraryPage() { const container = document.getElementById('track-list'); const paginationControls = document.getElementById('library-pagination-controls'); if (filteredLibraryTracks.length === 0) { container.innerHTML = '
No tracks found
'; paginationControls.style.display = 'none'; return; } // Calculate pagination const totalPages = Math.ceil(filteredLibraryTracks.length / libraryTracksPerPage); const startIndex = (libraryCurrentPage - 1) * libraryTracksPerPage; const endIndex = startIndex + libraryTracksPerPage; const tracksToShow = filteredLibraryTracks.slice(startIndex, endIndex); // Render tracks for current page const tracksHtml = tracksToShow.map((track, pageIndex) => { // Find the actual index in the full tracks array const actualIndex = tracks.findIndex(t => t.id === track.id); return `
${track.title[0] || 'Unknown Title'}
${track.artist[0] || 'Unknown Artist'} • ${track.album[0] || 'Unknown Album'}
`}).join(''); container.innerHTML = tracksHtml; // Update pagination controls document.getElementById('library-page-info').textContent = `Page ${libraryCurrentPage} of ${totalPages} (${filteredLibraryTracks.length} tracks)`; paginationControls.style.display = totalPages > 1 ? 'block' : 'none'; } // Library pagination functions function libraryGoToPage(page) { const totalPages = Math.ceil(filteredLibraryTracks.length / libraryTracksPerPage); if (page >= 1 && page <= totalPages) { libraryCurrentPage = page; renderLibraryPage(); } } function libraryPreviousPage() { if (libraryCurrentPage > 1) { libraryCurrentPage--; renderLibraryPage(); } } function libraryNextPage() { const totalPages = Math.ceil(filteredLibraryTracks.length / libraryTracksPerPage); if (libraryCurrentPage < totalPages) { libraryCurrentPage++; renderLibraryPage(); } } function libraryGoToLastPage() { const totalPages = Math.ceil(filteredLibraryTracks.length / libraryTracksPerPage); libraryCurrentPage = totalPages; renderLibraryPage(); } function changeLibraryTracksPerPage() { libraryTracksPerPage = parseInt(document.getElementById('library-tracks-per-page').value); libraryCurrentPage = 1; renderLibraryPage(); } function filterTracks() { const query = document.getElementById('search-tracks').value.toLowerCase(); const filtered = tracks.filter(track => (track.title[0] || '').toLowerCase().includes(query) || (track.artist[0] || '').toLowerCase().includes(query) || (track.album[0] || '').toLowerCase().includes(query) ); displayTracks(filtered); } function playTrack(index) { if (index < 0 || index >= tracks.length) return; currentTrack = tracks[index]; currentTrackIndex = index; // Load track into audio player audioPlayer.src = `/asteroid/tracks/${currentTrack.id}/stream`; audioPlayer.load(); audioPlayer.play().catch(error => { console.error('Playback error:', error); alert('Error playing track. The track may not be available.'); }); updatePlayerDisplay(); // Update server-side player state fetch(`/api/play?track-id=${currentTrack.id}`, { method: 'POST' }) .catch(error => console.error('API update error:', error)); } function togglePlayPause() { if (!currentTrack) { alert('Please select a track to play'); return; } if (audioPlayer.paused) { audioPlayer.play(); } else { audioPlayer.pause(); } } function playPrevious() { if (playQueue.length > 0) { // Play from queue const prevIndex = Math.max(0, currentTrackIndex - 1); playTrack(prevIndex); } else { // Play previous track in library const prevIndex = currentTrackIndex > 0 ? currentTrackIndex - 1 : tracks.length - 1; playTrack(prevIndex); } } function playNext() { if (playQueue.length > 0) { // Play from queue const nextTrack = playQueue.shift(); playTrack(tracks.findIndex(t => t.id === nextTrack.id)); updateQueueDisplay(); } else { // Play next track in library const nextIndex = isShuffled ? Math.floor(Math.random() * tracks.length) : (currentTrackIndex + 1) % tracks.length; playTrack(nextIndex); } } function handleTrackEnd() { if (isRepeating) { audioPlayer.currentTime = 0; audioPlayer.play(); } else { playNext(); } } function toggleShuffle() { isShuffled = !isShuffled; const btn = document.getElementById('shuffle-btn'); btn.textContent = isShuffled ? '🔀 Shuffle ON' : '🔀 Shuffle'; btn.classList.toggle('active', isShuffled); } function toggleRepeat() { isRepeating = !isRepeating; const btn = document.getElementById('repeat-btn'); btn.textContent = isRepeating ? '🔁 Repeat ON' : '🔁 Repeat'; btn.classList.toggle('active', isRepeating); } function updateVolume() { const volume = document.getElementById('volume-slider').value / 100; if (audioPlayer) { audioPlayer.volume = volume; } } function updateTimeDisplay() { const current = formatTime(audioPlayer.currentTime); const total = formatTime(audioPlayer.duration); document.getElementById('current-time').textContent = current; document.getElementById('total-time').textContent = total; } function formatTime(seconds) { if (isNaN(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } function updatePlayButton(text) { document.getElementById('play-pause-btn').textContent = text; } function updatePlayerDisplay() { if (currentTrack) { document.getElementById('current-title').textContent = currentTrack.title[0] || 'Unknown Title'; document.getElementById('current-artist').textContent = currentTrack.artist[0] || 'Unknown Artist'; document.getElementById('current-album').textContent = currentTrack.album[0] || 'Unknown Album'; } } function addToQueue(index) { if (index < 0 || index >= tracks.length) return; playQueue.push(tracks[index]); updateQueueDisplay(); } function updateQueueDisplay() { const container = document.getElementById('play-queue'); if (playQueue.length === 0) { container.innerHTML = '
Queue is empty
'; return; } const queueHtml = playQueue.map((track, index) => `
${track.title[0] || 'Unknown Title'}
${track.artist[0] || 'Unknown Artist'}
`).join(''); container.innerHTML = queueHtml; } function removeFromQueue(index) { playQueue.splice(index, 1); updateQueueDisplay(); } function clearQueue() { playQueue = []; updateQueueDisplay(); } async function createPlaylist() { const name = document.getElementById('new-playlist-name').value.trim(); if (!name) { alert('Please enter a playlist name'); return; } try { const formData = new FormData(); formData.append('name', name); formData.append('description', ''); const response = await fetch('/asteroid/api/playlists/create', { method: 'POST', body: formData }); const result = await response.json(); console.log('Create playlist result:', result); if (result.status === 'success') { alert(`Playlist "${name}" created successfully!`); document.getElementById('new-playlist-name').value = ''; // Wait a moment then reload playlists await new Promise(resolve => setTimeout(resolve, 500)); loadPlaylists(); } else { alert('Error creating playlist: ' + result.message); } } catch (error) { console.error('Error creating playlist:', error); alert('Error creating playlist: ' + error.message); } } async function saveQueueAsPlaylist() { if (playQueue.length === 0) { alert('Queue is empty'); return; } const name = prompt('Enter playlist name:'); if (!name) return; try { // First create the playlist const formData = new FormData(); formData.append('name', name); formData.append('description', `Created from queue with ${playQueue.length} tracks`); const createResponse = await fetch('/asteroid/api/playlists/create', { method: 'POST', body: formData }); const createResult = await createResponse.json(); console.log('Create playlist result:', createResult); if (createResult.status === 'success') { // Wait a moment for database to update await new Promise(resolve => setTimeout(resolve, 500)); // Get the new playlist ID by fetching playlists const playlistsResponse = await fetch('/asteroid/api/playlists'); const playlistsResult = await playlistsResponse.json(); console.log('Playlists result:', playlistsResult); if (playlistsResult.status === 'success' && playlistsResult.playlists.length > 0) { // Find the playlist with matching name (most recent) const newPlaylist = playlistsResult.playlists.find(p => p.name === name) || playlistsResult.playlists[playlistsResult.playlists.length - 1]; console.log('Found playlist:', newPlaylist); // Add all tracks from queue to playlist let addedCount = 0; for (const track of playQueue) { const trackId = track.id || (Array.isArray(track.id) ? track.id[0] : null); console.log('Adding track to playlist:', track, 'ID:', trackId); if (trackId) { const addFormData = new FormData(); addFormData.append('playlist-id', newPlaylist.id); addFormData.append('track-id', trackId); const addResponse = await fetch('/asteroid/api/playlists/add-track', { method: 'POST', body: addFormData }); const addResult = await addResponse.json(); console.log('Add track result:', addResult); if (addResult.status === 'success') { addedCount++; } } else { console.error('Track has no valid ID:', track); } } alert(`Playlist "${name}" created with ${addedCount} tracks!`); loadPlaylists(); } else { alert('Playlist created but could not add tracks. Error: ' + (playlistsResult.message || 'Unknown')); } } else { alert('Error creating playlist: ' + createResult.message); } } catch (error) { console.error('Error saving queue as playlist:', error); alert('Error saving queue as playlist: ' + error.message); } } async function loadPlaylists() { try { const response = await fetch('/asteroid/api/playlists'); const result = await response.json(); console.log('Load playlists result:', result); if (result.status === 'success') { displayPlaylists(result.playlists || []); } else { console.error('Error loading playlists:', result.message); displayPlaylists([]); } } catch (error) { console.error('Error loading playlists:', error); displayPlaylists([]); } } function displayPlaylists(playlists) { const container = document.getElementById('playlists-container'); if (!playlists || playlists.length === 0) { container.innerHTML = '
No playlists created yet.
'; return; } const playlistsHtml = playlists.map(playlist => `
${playlist.name}
${playlist['track-count']} tracks
`).join(''); container.innerHTML = playlistsHtml; } async function loadPlaylist(playlistId) { try { const response = await fetch(`/asteroid/api/playlists/${playlistId}`); const result = await response.json(); console.log('Load playlist result:', result); if (result.status === 'success' && result.playlist) { const playlist = result.playlist; // Clear current queue playQueue = []; // Add all playlist tracks to queue if (playlist.tracks && playlist.tracks.length > 0) { playlist.tracks.forEach(track => { // Find the full track object from our tracks array const fullTrack = tracks.find(t => t.id === track.id); if (fullTrack) { playQueue.push(fullTrack); } }); updateQueueDisplay(); alert(`Loaded ${playQueue.length} tracks from "${playlist.name}" into queue!`); // Optionally start playing the first track if (playQueue.length > 0) { const firstTrack = playQueue.shift(); const trackIndex = tracks.findIndex(t => t.id === firstTrack.id); if (trackIndex >= 0) { playTrack(trackIndex); } } } else { alert(`Playlist "${playlist.name}" is empty`); } } else { alert('Error loading playlist: ' + (result.message || 'Unknown error')); } } catch (error) { console.error('Error loading playlist:', error); alert('Error loading playlist: ' + error.message); } } // Stream quality configuration (same as front page) const liveStreamConfig = { aac: { url: 'http://localhost:8000/asteroid.aac', type: 'audio/aac', mount: 'asteroid.aac' }, mp3: { url: 'http://localhost:8000/asteroid.mp3', type: 'audio/mpeg', mount: 'asteroid.mp3' }, low: { url: 'http://localhost:8000/asteroid-low.mp3', type: 'audio/mpeg', mount: 'asteroid-low.mp3' } }; // Change live stream quality function changeLiveStreamQuality() { const selector = document.getElementById('live-stream-quality'); const config = liveStreamConfig[selector.value]; // Update audio player const audioElement = document.getElementById('live-stream-audio'); const sourceElement = document.getElementById('live-stream-source'); const wasPlaying = !audioElement.paused; sourceElement.src = config.url; sourceElement.type = config.type; audioElement.load(); // Resume playback if it was playing if (wasPlaying) { audioElement.play().catch(e => console.log('Autoplay prevented:', e)); } } // Live stream functionality async function updateLiveStream() { try { const response = await fetch('/asteroid/api/icecast-status') if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); console.log('Live stream data:', data); // Debug log if (data.icestats && data.icestats.source) { const sources = Array.isArray(data.icestats.source) ? data.icestats.source : [data.icestats.source]; const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3')); if (mainStream && mainStream.title) { const titleParts = mainStream.title.split(' - '); const artist = titleParts.length > 1 ? titleParts[0] : 'Unknown Artist'; const track = titleParts.length > 1 ? titleParts.slice(1).join(' - ') : mainStream.title; const nowPlayingEl = document.getElementById('live-now-playing'); const listenersEl = document.getElementById('live-listeners'); if (nowPlayingEl) nowPlayingEl.textContent = `${artist} - ${track}`; if (listenersEl) listenersEl.textContent = mainStream.listeners || '0'; console.log('Updated live stream info:', `${artist} - ${track}`, 'Listeners:', mainStream.listeners); } else { console.log('No main stream found or no title'); } } else { console.log('No icestats or source in response'); } } catch (error) { console.error('Live stream update error:', error); const nowPlayingEl = document.getElementById('live-now-playing'); if (nowPlayingEl) nowPlayingEl.textContent = 'Stream unavailable'; } } // Update live stream info every 10 seconds setTimeout(updateLiveStream, 1000); // Initial update after 1 second setInterval(updateLiveStream, 10000);