Compare commits
No commits in common. "4d0b54f7d6a208673c7979ef51304763544e1781" and "136fa2fa7424ca06e5486e6d00fb0d0118f41067" have entirely different histories.
4d0b54f7d6
...
136fa2fa74
|
|
@ -40,5 +40,4 @@
|
||||||
(: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")))
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
(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,15 +60,33 @@ 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/partial/now-playing')
|
const response = await fetch('/api/asteroid/icecast-status')
|
||||||
const contentType = response.headers.get("content-type")
|
const data = await response.json()
|
||||||
if (!contentType.includes('text/html')) {
|
// Handle RADIANCE API wrapper format
|
||||||
throw new Error('Error connecting to stream')
|
const icecastData = data.data || data;
|
||||||
|
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,24 +566,41 @@ function changeLiveStreamQuality() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live stream informatio update
|
// Live stream functionality
|
||||||
async function updateNowPlaying() {
|
async function updateLiveStream() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
const response = await fetch('/api/asteroid/icecast-status')
|
||||||
const contentType = response.headers.get("content-type")
|
if (!response.ok) {
|
||||||
if (!contentType.includes('text/html')) {
|
throw new Error(`HTTP ${response.status}`);
|
||||||
throw new Error('Error connecting to stream')
|
|
||||||
}
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
const data = await response.text()
|
// Handle RADIANCE API wrapper format
|
||||||
document.getElementById('now-playing').innerHTML = data
|
const data = result.data || result;
|
||||||
|
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'));
|
||||||
|
|
||||||
} catch(error) {
|
if (mainStream && mainStream.title) {
|
||||||
console.log('Could not fetch stream status:', error);
|
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) {
|
||||||
|
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
|
||||||
setInterval(updateNowPlaying, 10000);
|
setTimeout(updateLiveStream, 1000); // Initial update after 1 second
|
||||||
|
setInterval(updateLiveStream, 10000);
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,12 @@
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="now-playing" class="now-playing"></div>
|
<div class="now-playing">
|
||||||
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<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,6 +25,8 @@
|
||||||
<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>
|
||||||
|
|
@ -43,8 +45,6 @@
|
||||||
</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