From d8306f0585079da96888bf9464ddf29c7085e07a Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Wed, 1 Oct 2025 19:22:35 +0300 Subject: [PATCH] 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 --- .gitignore | 10 ++- asteroid.asd | 1 + asteroid.lisp | 18 ++++- docker/docker-compose.yml.remote-backup | 33 ++++++++++ docker/music/.gitkeep | 0 template/front-page.chtml | 41 +++++++++++- template/player.chtml | 87 ++++++++++++++++++++++--- 7 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 docker/docker-compose.yml.remote-backup create mode 100644 docker/music/.gitkeep diff --git a/.gitignore b/.gitignore index e14928f..fa83782 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/asteroid.asd b/asteroid.asd index 78fad7c..3fb20f8 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -24,6 +24,7 @@ :cl-fad :bordeaux-threads (:interface :auth) + :drakma (:interface :database) (:interface :user)) :pathname "./" diff --git a/asteroid.lisp b/asteroid.lisp index b0ece3f..55da662 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -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 diff --git a/docker/docker-compose.yml.remote-backup b/docker/docker-compose.yml.remote-backup new file mode 100644 index 0000000..68c69e6 --- /dev/null +++ b/docker/docker-compose.yml.remote-backup @@ -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 diff --git a/docker/music/.gitkeep b/docker/music/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/template/front-page.chtml b/template/front-page.chtml index e6871b8..dd83159 100644 --- a/template/front-page.chtml +++ b/template/front-page.chtml @@ -46,10 +46,47 @@

Now Playing

Artist: The Void

Track: Silence

-

Album: Startup Sounds

-

Duration:

+

Listeners: 0

+ + diff --git a/template/player.chtml b/template/player.chtml index 9b68e93..f6aca2e 100644 --- a/template/player.chtml +++ b/template/player.chtml @@ -14,9 +14,23 @@ Admin Dashboard + +
+

🔴 Live Radio Stream

+
+

Now Playing: Loading...

+

Listeners: 0

+ +

Listen to the live Asteroid Radio stream

+
+
+
-

Track Library

+

Personal Track Library

@@ -122,11 +136,13 @@ document.getElementById('volume-slider').addEventListener('input', updateVolume); // Audio player events - audioPlayer.addEventListener('loadedmetadata', updateTimeDisplay); - audioPlayer.addEventListener('timeupdate', updateTimeDisplay); - audioPlayer.addEventListener('ended', handleTrackEnd); - audioPlayer.addEventListener('play', () => updatePlayButton('⏸️ Pause')); - audioPlayer.addEventListener('pause', () => updatePlayButton('▶️ Play')); + 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,7 +288,9 @@ function updateVolume() { const volume = document.getElementById('volume-slider').value / 100; - audioPlayer.volume = volume; + if (audioPlayer) { + audioPlayer.volume = volume; + } } function updateTimeDisplay() { @@ -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);