feat: Complete Docker streaming integration with web interface
- Add live stream integration to both front page and player page - Add /api/icecast-status endpoint to fetch real-time stream data - Add drakma dependency for HTTP requests to Icecast - Fix JavaScript errors on player page with proper error handling - Add auto-updating 'Now Playing' info every 10 seconds - Update .gitignore to preserve docker/music/ directory structure - Add .gitkeep to maintain docker/music/ folder in repository - Improve user experience with separate public/registered user flows Integration now complete: - Front page: Public live stream access - Player page: Live stream + playlist management for registered users - Real-time metadata from Icecast JSON API - Graceful error handling for missing stream backend
This commit is contained in:
parent
e61a5a51df
commit
d8306f0585
|
|
@ -32,8 +32,14 @@ build-sbcl.sh
|
|||
*.aac
|
||||
*.wma
|
||||
|
||||
# Docker music directory
|
||||
docker/music/
|
||||
# Docker music directory - keep folder but ignore music files
|
||||
docker/music/*.mp3
|
||||
docker/music/*.flac
|
||||
docker/music/*.ogg
|
||||
docker/music/*.wav
|
||||
docker/music/*.m4a
|
||||
docker/music/*.aac
|
||||
docker/music/*.wma
|
||||
|
||||
# Docker build artifacts
|
||||
docker/.env
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
:cl-fad
|
||||
:bordeaux-threads
|
||||
(:interface :auth)
|
||||
:drakma
|
||||
(:interface :database)
|
||||
(:interface :user))
|
||||
:pathname "./"
|
||||
|
|
|
|||
|
|
@ -307,7 +307,23 @@
|
|||
("artist" . "The Void")
|
||||
("album" . "Startup Sounds")))
|
||||
("listeners" . 0)
|
||||
("stream-url" . "http://localhost:8000/asteroid"))))
|
||||
("stream-url" . "http://localhost:8000/asteroid.mp3")
|
||||
("stream-status" . "live"))))
|
||||
|
||||
;; Live stream status from Icecast
|
||||
(define-page icecast-status #@"/api/icecast-status" ()
|
||||
"Get live status from Icecast server"
|
||||
(setf (radiance:header "Content-Type") "application/json")
|
||||
(handler-case
|
||||
(let* ((icecast-url "http://localhost:8000/status-json.xsl")
|
||||
(response (drakma:http-request icecast-url :want-stream nil)))
|
||||
(if response
|
||||
(babel:octets-to-string response :encoding :utf-8) ; Convert response to string
|
||||
(cl-json:encode-json-to-string
|
||||
`(("error" . "Could not connect to Icecast server")))))
|
||||
(error (e)
|
||||
(cl-json:encode-json-to-string
|
||||
`(("error" . ,(format nil "Icecast connection failed: ~a" e)))))))
|
||||
|
||||
|
||||
;; RADIANCE server management functions
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
services:
|
||||
icecast:
|
||||
image: infiniteproject/icecast:latest
|
||||
container_name: asteroid-icecast
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./icecast.xml:/etc/icecast2/icecast.xml:ro
|
||||
environment:
|
||||
- ICECAST_SOURCE_PASSWORD=H1tn31EhsyLrfRmo
|
||||
- ICECAST_ADMIN_PASSWORD=asteroid_admin_2024
|
||||
- ICECAST_RELAY_PASSWORD=asteroid_relay_2024
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- asteroid-network
|
||||
|
||||
liquidsoap:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.liquidsoap
|
||||
container_name: asteroid-liquidsoap
|
||||
depends_on:
|
||||
- icecast
|
||||
volumes:
|
||||
- /mnt/remote-music/Music:/app/music:ro
|
||||
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- asteroid-network
|
||||
|
||||
networks:
|
||||
asteroid-network:
|
||||
driver: bridge
|
||||
|
|
@ -46,10 +46,47 @@
|
|||
<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>Album: <span data-text="now-playing-album">Startup Sounds</span></p>
|
||||
<p>Duration: <span data-text="now-playing-duration">∞</span></p>
|
||||
<p>Listeners: <span data-text="listeners">0</span></p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Update now playing info from Icecast
|
||||
function updateNowPlaying() {
|
||||
fetch('/asteroid/api/icecast-status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.icestats && data.icestats.source) {
|
||||
// Find the high quality stream (asteroid.mp3)
|
||||
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) {
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('Could not fetch stream status:', error));
|
||||
}
|
||||
|
||||
// Update every 10 seconds
|
||||
updateNowPlaying();
|
||||
setInterval(updateNowPlaying, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,23 @@
|
|||
<a href="/asteroid/admin">Admin Dashboard</a>
|
||||
</div>
|
||||
|
||||
<!-- Live Stream Section -->
|
||||
<div class="player-section">
|
||||
<h2>🔴 Live Radio Stream</h2>
|
||||
<div class="live-player">
|
||||
<p><strong>Now Playing:</strong> <span id="live-now-playing">Loading...</span></p>
|
||||
<p><strong>Listeners:</strong> <span id="live-listeners">0</span></p>
|
||||
<audio controls style="width: 100%; margin: 10px 0;">
|
||||
<source src="http://localhost:8000/asteroid.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p><em>Listen to the live Asteroid Radio stream</em></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Track Browser -->
|
||||
<div class="player-section">
|
||||
<h2>Track Library</h2>
|
||||
<h2>Personal Track Library</h2>
|
||||
<div class="track-browser">
|
||||
<input type="text" id="search-tracks" placeholder="Search tracks..." class="search-input">
|
||||
<div id="track-list" class="track-list">
|
||||
|
|
@ -122,11 +136,13 @@
|
|||
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);
|
||||
|
|
@ -136,7 +152,10 @@
|
|||
|
||||
async function loadTracks() {
|
||||
try {
|
||||
const response = await fetch('/admin/tracks');
|
||||
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') {
|
||||
|
|
@ -269,8 +288,10 @@
|
|||
|
||||
function updateVolume() {
|
||||
const volume = document.getElementById('volume-slider').value / 100;
|
||||
if (audioPlayer) {
|
||||
audioPlayer.volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimeDisplay() {
|
||||
const current = formatTime(audioPlayer.currentTime);
|
||||
|
|
@ -363,6 +384,56 @@
|
|||
|
||||
// Initialize volume
|
||||
updateVolume();
|
||||
|
||||
// Live stream functionality
|
||||
function updateLiveStream() {
|
||||
try {
|
||||
fetch('/asteroid/api/icecast-status')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
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 fetch error:', error);
|
||||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||||
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream unavailable';
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Live stream update error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update live stream info every 10 seconds
|
||||
setTimeout(updateLiveStream, 1000); // Initial update after 1 second
|
||||
setInterval(updateLiveStream, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue