Compare commits

..

3 Commits

Author SHA1 Message Date
Luis Pereira 4d0b54f7d6 feat: move player to use now-playing partial 2025-10-16 19:02:00 -04:00
Luis Pereira f3d012cbc6 feat: move front-page to use now-playing partial 2025-10-16 19:02:00 -04:00
Luis Pereira d0efc89e33 feat: add HTML partial hidration for now-playing 2025-10-16 19:02:00 -04:00
7 changed files with 101 additions and 64 deletions

View File

@ -40,4 +40,5 @@
(:file "playlist-management") (:file "playlist-management")
(:file "stream-control") (:file "stream-control")
(:file "auth-routes") (:file "auth-routes")
(:file "frontend-partials")
(:file "asteroid"))) (:file "asteroid")))

55
frontend-partials.lisp Normal file
View File

@ -0,0 +1,55 @@
(in-package :asteroid)
(defun icecast-now-playing (icecast-base-url)
(let* ((icecast-url (concatenate 'string icecast-base-url "/admin/stats.xml"))
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
(when response
(let ((xml-string (if (stringp response)
response
(babel:octets-to-string response :encoding :utf-8))))
;; Simple XML parsing to extract source information
;; Look for <source mount="/asteroid.mp3"> sections and extract title, listeners, etc.
(multiple-value-bind (match-start match-end)
(cl-ppcre:scan "<source mount=\"/asteroid\\.mp3\">" xml-string)
(if match-start
(let* ((source-section (subseq xml-string match-start
(or (cl-ppcre:scan "</source>" xml-string :start match-start)
(length xml-string))))
(titlep (cl-ppcre:all-matches "<title>" source-section))
(listenersp (cl-ppcre:all-matches "<listeners>" source-section))
(title (if titlep (cl-ppcre:regex-replace-all ".*<title>(.*?)</title>.*" source-section "\\1") "Unknown"))
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
(:title . ,title)
(:listeners . ,(parse-integer listeners :junk-allowed t))))
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
(:title . "Unknown")
(:listeners . "Unknown"))))))))
(define-api asteroid/partial/now-playing () ()
"Get Partial HTML with live status from Icecast server"
(handler-case
(let ((now-playing-stats (icecast-now-playing *stream-base-url*))
(template-path (merge-pathnames "template/partial/now-playing.chtml"
(asdf:system-source-directory :asteroid))))
(if now-playing-stats
(progn
;; TODO: it should be able to define a custom api-output for this
;; (api-output <clip-parser> :format "html"))
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
:stats now-playing-stats))
(progn
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
:connection-error t
:stats nil))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error loading profile: ~a" e)))
:status 500))))

View File

@ -60,33 +60,15 @@ function changeStreamQuality() {
// Update now playing info from Icecast // Update now playing info from Icecast
async function updateNowPlaying() { async function updateNowPlaying() {
try { try {
const response = await fetch('/api/asteroid/icecast-status') const response = await fetch('/api/asteroid/partial/now-playing')
const data = await response.json() const contentType = response.headers.get("content-type")
// Handle RADIANCE API wrapper format if (!contentType.includes('text/html')) {
const icecastData = data.data || data; throw new Error('Error connecting to stream')
if (icecastData.icestats && icecastData.icestats.source) {
// Find the high quality stream (asteroid.mp3)
const sources = Array.isArray(icecastData.icestats.source) ? icecastData.icestats.source : [icecastData.icestats.source];
const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3'));
if (mainStream && mainStream.title) {
// Parse "Artist - Track" format
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;
document.querySelector('[data-text="now-playing-artist"]').textContent = artist;
document.querySelector('[data-text="now-playing-track"]').textContent = track;
document.querySelector('[data-text="listeners"]').textContent = mainStream.listeners || '0';
// Update stream status
const statusElement = document.querySelector('.live-stream p:nth-child(3) span');
if (statusElement) {
statusElement.textContent = '● LIVE - ' + track;
statusElement.style.color = '#00ff00';
}
}
} }
const data = await response.text()
document.getElementById('now-playing').innerHTML = data
} catch(error) { } catch(error) {
console.log('Could not fetch stream status:', error); console.log('Could not fetch stream status:', error);
} }

View File

@ -566,41 +566,24 @@ function changeLiveStreamQuality() {
} }
} }
// Live stream functionality // Live stream informatio update
async function updateLiveStream() { async function updateNowPlaying() {
try { try {
const response = await fetch('/api/asteroid/icecast-status') const response = await fetch('/api/asteroid/partial/now-playing')
if (!response.ok) { const contentType = response.headers.get("content-type")
throw new Error(`HTTP ${response.status}`); if (!contentType.includes('text/html')) {
throw new Error('Error connecting to stream')
} }
const result = await response.json();
// Handle RADIANCE API wrapper format const data = await response.text()
const data = result.data || result; document.getElementById('now-playing').innerHTML = data
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) { } catch(error) {
const titleParts = mainStream.title.split(' - '); console.log('Could not fetch stream status:', error);
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';
} else {
}
}
} catch (error) {
console.error('Live stream update error:', error);
const nowPlayingEl = document.getElementById('live-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream unavailable';
} }
} }
// Initial update after 1 second
setTimeout(updateNowPlaying, 1000);
// Update live stream info every 10 seconds // Update live stream info every 10 seconds
setTimeout(updateLiveStream, 1000); // Initial update after 1 second setInterval(updateNowPlaying, 10000);
setInterval(updateLiveStream, 10000);

View File

@ -56,12 +56,7 @@
</audio> </audio>
</div> </div>
<div class="now-playing"> <div id="now-playing" class="now-playing"></div>
<h2>Now Playing</h2>
<p>Artist: <span data-text="now-playing-artist">The Void</span></p>
<p>Track: <span data-text="now-playing-track">Silence</span></p>
<p>Listeners: <span data-text="listeners">0</span></p>
</div>
</main> </main>
</div> </div>
</body> </body>

View File

@ -0,0 +1,21 @@
<h2>Now Playing</h2>
<c:if test="stats">
<c:then>
<c:using value="stats">
<!--<p>Artist: <span>The Void</span></p>-->
<p>Track: <span lquery="(text title)">The Void - Silence</span></p>
<p>Listeners: <span lquery="(text listeners)">1</span></p>
</c:using>
</c:then>
<c:else>
<c:if test="connection-error">
<c:then>
<div class="message error">
<span>There was an error trying to get information from stream.</span>
</div>
</c:then>
</c:if>
<p>Track: <span>NA</span></p>
<p>Listeners: <span>NA</span></p>
</c:else>
</c:if>

View File

@ -25,8 +25,6 @@
<h2 style="color: #00ff00;">🟢 Live Radio Stream</h2> <h2 style="color: #00ff00;">🟢 Live Radio Stream</h2>
<div class="live-stream"> <div class="live-stream">
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)"> <input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<p><strong>Now Playing:</strong> <span id="live-now-playing">Loading...</span></p>
<p><strong>Listeners:</strong> <span id="live-listeners">0</span></p>
<!-- Stream Quality Selector --> <!-- Stream Quality Selector -->
<div class="live-stream-quality"> <div class="live-stream-quality">
<label for="live-stream-quality"><strong>Quality:</strong></label> <label for="live-stream-quality"><strong>Quality:</strong></label>
@ -45,6 +43,8 @@
</div> </div>
</div> </div>
<div id="now-playing" class="now-playing"></div>
<!-- Track Browser --> <!-- Track Browser -->
<div class="player-section"> <div class="player-section">
<h2>Personal Track Library</h2> <h2>Personal Track Library</h2>