Compare commits
3 Commits
136fa2fa74
...
4d0b54f7d6
| Author | SHA1 | Date |
|---|---|---|
|
|
4d0b54f7d6 | |
|
|
f3d012cbc6 | |
|
|
d0efc89e33 |
|
|
@ -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")))
|
||||||
|
|
|
||||||
|
|
@ -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))))
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
|
||||||
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';
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.error('Live stream update error:', error);
|
console.log('Could not fetch stream status:', 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);
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue