From bd1b993a03159aa8eb1e10c49c83e46e83e4b33b Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Sat, 4 Oct 2025 17:41:41 +0100 Subject: [PATCH] feat: move player javascript code to own file --- static/js/player.js | 603 +++++++++++++++++++++++++++++++++++++++++ template/player.chtml | 616 +----------------------------------------- 2 files changed, 604 insertions(+), 615 deletions(-) create mode 100644 static/js/player.js diff --git a/static/js/player.js b/static/js/player.js new file mode 100644 index 0000000..57c455e --- /dev/null +++ b/static/js/player.js @@ -0,0 +1,603 @@ +// 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); diff --git a/template/player.chtml b/template/player.chtml index 473c9f4..7a85188 100644 --- a/template/player.chtml +++ b/template/player.chtml @@ -5,6 +5,7 @@ +
@@ -126,620 +127,5 @@
- -