From d4edb8bfecc5489a5b071b68b4ed0f90ec909729 Mon Sep 17 00:00:00 2001 From: glenneth Date: Tue, 14 Oct 2025 14:41:25 +0300 Subject: [PATCH] Add admin UI for stream queue management - Queue management section with add/remove/clear controls - Add to Queue button on each track in library browser - Search tracks and add to queue - Add 10 random tracks button - Live stream monitor with Now Playing display - Toast notifications for user feedback - Real-time queue updates --- static/asteroid.css | 83 +++++++++++++ static/asteroid.lass | 71 +++++++++++ static/js/admin.js | 289 +++++++++++++++++++++++++++++++++++++++++++ template/admin.chtml | 35 ++++++ 4 files changed, 478 insertions(+) diff --git a/static/asteroid.css b/static/asteroid.css index 3453b09..08875d9 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -431,6 +431,89 @@ body .queue-item:last-child{ margin-bottom: 0; } +body .queue-position{ + background: #00ff00; + color: #000; + padding: 4px 8px; + border-radius: 3px; + font-weight: bold; + margin-right: 10px; + min-width: 30px; + text-align: center; + display: inline-block; +} + +body .queue-track-info{ + flex: 1; + margin-right: 10px; +} + +body .queue-track-info.track-title{ + font-weight: bold; + margin-bottom: 2px; +} + +body .queue-track-info.track-artist{ + font-size: 0.9em; + color: #888; +} + +body .queue-actions{ + margin-top: 20px; + padding: 15px; + background: #0a0a0a; + border: 1px solid #2a3441; + border-radius: 4px; +} + +body .queue-list{ + border: 1px solid #2a3441; + background: #0a0a0a; + min-height: 200px; + max-height: 400px; + overflow-y: auto; + padding: 10px; + margin-bottom: 20px; +} + +body .search-results{ + margin-top: 10px; + max-height: 300px; + overflow-y: auto; +} + +body .search-result-item{ + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + border: 1px solid #2a3441; + margin-bottom: 5px; + background: #0a0a0a; + border-radius: 3px; +} + +body .search-result-item:hover{ + background: #1a1a1a; + border-color: #00ff00; +} + +body .search-result-item.track-info{ + flex: 1; +} + +body .search-result-item.track-actions{ + display: flex; + gap: 5px; +} + +body .empty-state{ + text-align: center; + color: #666; + padding: 30px; + font-style: italic; +} + body .empty-queue{ text-align: center; color: #666; diff --git a/static/asteroid.lass b/static/asteroid.lass index b95102d..f71752f 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -347,6 +347,77 @@ :border-bottom none :margin-bottom 0) + (.queue-position + :background "#00ff00" + :color "#000" + :padding "4px 8px" + :border-radius "3px" + :font-weight bold + :margin-right "10px" + :min-width "30px" + :text-align center + :display inline-block) + + (.queue-track-info + :flex 1 + :margin-right "10px") + + ((:and .queue-track-info .track-title) + :font-weight bold + :margin-bottom "2px") + + ((:and .queue-track-info .track-artist) + :font-size "0.9em" + :color "#888") + + (.queue-actions + :margin-top "20px" + :padding "15px" + :background "#0a0a0a" + :border "1px solid #2a3441" + :border-radius "4px") + + (.queue-list + :border "1px solid #2a3441" + :background "#0a0a0a" + :min-height "200px" + :max-height "400px" + :overflow-y auto + :padding "10px" + :margin-bottom "20px") + + (.search-results + :margin-top "10px" + :max-height "300px" + :overflow-y auto) + + (.search-result-item + :display flex + :justify-content space-between + :align-items center + :padding "10px" + :border "1px solid #2a3441" + :margin-bottom "5px" + :background "#0a0a0a" + :border-radius "3px") + + ((:and .search-result-item :hover) + :background "#1a1a1a" + :border-color "#00ff00") + + ((:and .search-result-item .track-info) + :flex 1) + + ((:and .search-result-item .track-actions) + :display flex + :gap "5px") + + (.empty-state + :text-align center + :color "#666" + :padding "30px" + :font-style italic) + (.empty-queue :text-align center :color "#666" diff --git a/static/js/admin.js b/static/js/admin.js index ba5548d..27cfb00 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -25,6 +25,30 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('player-pause').addEventListener('click', pausePlayer); document.getElementById('player-stop').addEventListener('click', stopPlayer); document.getElementById('player-resume').addEventListener('click', resumePlayer); + + // Queue controls + const refreshQueueBtn = document.getElementById('refresh-queue'); + const clearQueueBtn = document.getElementById('clear-queue-btn'); + const addRandomBtn = document.getElementById('add-random-tracks'); + const queueSearchInput = document.getElementById('queue-track-search'); + + if (refreshQueueBtn) refreshQueueBtn.addEventListener('click', loadStreamQueue); + if (clearQueueBtn) clearQueueBtn.addEventListener('click', clearStreamQueue); + if (addRandomBtn) addRandomBtn.addEventListener('click', addRandomTracks); + if (queueSearchInput) queueSearchInput.addEventListener('input', searchTracksForQueue); + + // Load initial queue + loadStreamQueue(); + + // Setup live stream monitor + const liveAudio = document.getElementById('live-stream-audio'); + if (liveAudio) { + liveAudio.preload = 'none'; + } + + // Update live stream info + updateLiveStreamInfo(); + setInterval(updateLiveStreamInfo, 10000); // Every 10 seconds }); // Load tracks from API @@ -81,6 +105,7 @@ function renderPage() {
+
@@ -304,3 +329,267 @@ function openIncomingFolder() { // Update player status every 5 seconds setInterval(updatePlayerStatus, 5000); + +// ======================================== +// Stream Queue Management +// ======================================== + +let streamQueue = []; +let queueSearchTimeout = null; + +// Load current stream queue +async function loadStreamQueue() { + try { + const response = await fetch('/api/asteroid/stream/queue'); + const result = await response.json(); + const data = result.data || result; + + if (data.status === 'success') { + streamQueue = data.queue || []; + displayStreamQueue(); + } + } catch (error) { + console.error('Error loading stream queue:', error); + document.getElementById('stream-queue-container').innerHTML = + '
Error loading queue
'; + } +} + +// Display stream queue +function displayStreamQueue() { + const container = document.getElementById('stream-queue-container'); + + if (streamQueue.length === 0) { + container.innerHTML = '
Queue is empty. Add tracks below.
'; + return; + } + + let html = '
'; + streamQueue.forEach((item, index) => { + if (item) { + html += ` +
+ ${index + 1} +
+
${item.title || 'Unknown'}
+
${item.artist || 'Unknown Artist'}
+
+ +
+ `; + } + }); + html += '
'; + + container.innerHTML = html; +} + +// Clear stream queue +async function clearStreamQueue() { + if (!confirm('Clear the entire stream queue? This will stop playback until new tracks are added.')) { + return; + } + + try { + const response = await fetch('/api/asteroid/stream/queue/clear', { + method: 'POST' + }); + const result = await response.json(); + const data = result.data || result; + + if (data.status === 'success') { + alert('Queue cleared successfully'); + loadStreamQueue(); + } else { + alert('Error clearing queue: ' + (data.message || 'Unknown error')); + } + } catch (error) { + console.error('Error clearing queue:', error); + alert('Error clearing queue'); + } +} + +// Remove track from queue +async function removeFromQueue(trackId) { + try { + const response = await fetch('/api/asteroid/stream/queue/remove', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: `track-id=${trackId}` + }); + const result = await response.json(); + const data = result.data || result; + + if (data.status === 'success') { + loadStreamQueue(); + } else { + alert('Error removing track: ' + (data.message || 'Unknown error')); + } + } catch (error) { + console.error('Error removing track:', error); + alert('Error removing track'); + } +} + +// Add track to queue +async function addToQueue(trackId, position = 'end', showNotification = true) { + try { + const response = await fetch('/api/asteroid/stream/queue/add', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: `track-id=${trackId}&position=${position}` + }); + const result = await response.json(); + const data = result.data || result; + + if (data.status === 'success') { + // Only reload queue if we're in the queue management section + const queueContainer = document.getElementById('stream-queue-container'); + if (queueContainer && queueContainer.offsetParent !== null) { + loadStreamQueue(); + } + + // Show brief success notification + if (showNotification) { + showToast('✓ Added to queue'); + } + return true; + } else { + alert('Error adding track: ' + (data.message || 'Unknown error')); + return false; + } + } catch (error) { + console.error('Error adding track:', error); + alert('Error adding track'); + return false; + } +} + +// Simple toast notification +function showToast(message) { + const toast = document.createElement('div'); + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + background: #00ff00; + color: #000; + padding: 12px 20px; + border-radius: 4px; + font-weight: bold; + z-index: 10000; + animation: slideIn 0.3s ease-out; + `; + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transition = 'opacity 0.3s'; + setTimeout(() => toast.remove(), 300); + }, 2000); +} + +// Add random tracks to queue +async function addRandomTracks() { + if (tracks.length === 0) { + alert('No tracks available. Please scan the library first.'); + return; + } + + const count = 10; + const shuffled = [...tracks].sort(() => Math.random() - 0.5); + const selected = shuffled.slice(0, Math.min(count, tracks.length)); + + for (const track of selected) { + await addToQueue(track.id, 'end', false); // Don't show toast for each track + } + + showToast(`✓ Added ${selected.length} random tracks to queue`); +} + +// Search tracks for adding to queue +function searchTracksForQueue(event) { + clearTimeout(queueSearchTimeout); + const query = event.target.value.toLowerCase(); + + if (query.length < 2) { + document.getElementById('queue-track-results').innerHTML = ''; + return; + } + + queueSearchTimeout = setTimeout(() => { + const results = tracks.filter(track => + (track.title && track.title.toLowerCase().includes(query)) || + (track.artist && track.artist.toLowerCase().includes(query)) || + (track.album && track.album.toLowerCase().includes(query)) + ).slice(0, 20); // Limit to 20 results + + displayQueueSearchResults(results); + }, 300); +} + +// Display search results for queue +function displayQueueSearchResults(results) { + const container = document.getElementById('queue-track-results'); + + if (results.length === 0) { + container.innerHTML = '
No tracks found
'; + return; + } + + let html = '
'; + results.forEach(track => { + html += ` +
+
+
${track.title || 'Unknown'}
+
${track.artist || 'Unknown'} - ${track.album || 'Unknown Album'}
+
+
+ + +
+
+ `; + }); + html += '
'; + + container.innerHTML = html; +} + +// Live stream info update +async function updateLiveStreamInfo() { + try { + const response = await fetch('/api/asteroid/icecast-status'); + if (!response.ok) { + return; + } + + const result = await response.json(); + + // Handle Radiance API response format + const data = result.data || result; + + // Sources are nested in icestats + const sources = data.icestats?.source; + + if (sources) { + const mainStream = Array.isArray(sources) + ? sources.find(s => s.listenurl?.includes('/asteroid.aac') || s.listenurl?.includes('/asteroid.mp3')) + : sources; + + if (mainStream && mainStream.title) { + const nowPlayingEl = document.getElementById('live-now-playing'); + if (nowPlayingEl) { + const parts = mainStream.title.split(' - '); + const artist = parts[0] || 'Unknown'; + const track = parts.slice(1).join(' - ') || 'Unknown'; + nowPlayingEl.textContent = `${artist} - ${track}`; + } + } + } + } catch (error) { + console.error('Could not fetch stream info:', error); + } +} diff --git a/template/admin.chtml b/template/admin.chtml index f903d11..0397ba9 100644 --- a/template/admin.chtml +++ b/template/admin.chtml @@ -107,6 +107,41 @@ + +
+

📻 Live Stream Monitor

+
+ +

Now Playing: Loading...

+ +
+
+ + +
+

🎵 Stream Queue Management

+

Manage the live stream playback queue. Changes take effect within 5-10 seconds.

+ +
+ + + +
+ +
+
Loading queue...
+
+ +
+

Add Tracks to Queue

+ +
+
+
+

Player Control