diff --git a/asteroid.lisp b/asteroid.lisp index 1f45c77..222d6cf 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -250,6 +250,23 @@ ("message" . ,(format nil "Error clearing queue: ~a" e))) :status 500)))) +(define-api asteroid/stream/queue/load-m3u () () + "Load queue from stream-queue.m3u file" + (require-role :admin) + (handler-case + (let ((count (or (load-queue-from-m3u) 0))) + (if (numberp count) + (api-output `(("status" . "success") + ("message" . "Queue loaded from M3U file") + ("count" . ,count))) + (api-output `(("status" . "error") + ("message" . "Failed to load queue from M3U file")) + :status 500))) + (error (e) + (api-output `(("status" . "error") + ("message" . ,(format nil "Error loading from M3U: ~a" e))) + :status 500)))) + (define-api asteroid/stream/queue/add-playlist (playlist-id) () "Add all tracks from a playlist to the stream queue" (require-role :admin) @@ -503,10 +520,19 @@ ("message" . "Listening history cleared successfully")))) |# -;; Front page +;; Front page - now serves frameset wrapper (define-page front-page #@"/" () - "Main front page" - (let ((template-path (merge-pathnames "template/front-page.chtml" + "Main front page with persistent audio player frame" + (let ((template-path (merge-pathnames "template/frameset-wrapper.chtml" + (asdf:system-source-directory :asteroid)))) + (clip:process-to-string + (plump:parse (alexandria:read-file-into-string template-path)) + :title "๐ŸŽต ASTEROID RADIO ๐ŸŽต"))) + +;; Content frame - the actual front page content +(define-page front-page-content #@"/content" () + "Front page content (displayed in content frame)" + (let ((template-path (merge-pathnames "template/front-page-content.chtml" (asdf:system-source-directory :asteroid)))) (clip:process-to-string (plump:parse (alexandria:read-file-into-string template-path)) @@ -524,6 +550,17 @@ :now-playing-album "Startup Sounds" :now-playing-duration "โˆž"))) +;; Persistent audio player frame +(define-page audio-player-frame #@"/audio-player-frame" () + "Persistent audio player frame (bottom of page)" + (let ((template-path (merge-pathnames "template/audio-player-frame.chtml" + (asdf:system-source-directory :asteroid)))) + (clip:process-to-string + (plump:parse (alexandria:read-file-into-string template-path)) + :stream-base-url *stream-base-url* + :default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac") + :default-stream-encoding "audio/aac"))) + ;; Configure static file serving for other files (define-page static #@"/static/(.*)" (:uri-groups (path)) (serve-file (merge-pathnames (concatenate 'string "static/" path) @@ -810,8 +847,15 @@ :error-message "" :success-message "")))) +;; Player page - redirects to content frame version (define-page player #@"/player" () - (let ((template-path (merge-pathnames "template/player.chtml" + "Redirect to player content in frameset" + (radiance:redirect "/asteroid/")) + +;; Player content frame +(define-page player-content #@"/player-content" () + "Player page content (displayed in content frame)" + (let ((template-path (merge-pathnames "template/player-content.chtml" (asdf:system-source-directory :asteroid)))) (clip:process-to-string (plump:parse (alexandria:read-file-into-string template-path)) @@ -890,6 +934,24 @@ (setf (radiance:environment) "default")) (radiance:startup) + + ;; Load the stream queue from M3U file after database is ready + (bt:make-thread + (lambda () + (format t "Queue loader thread started, waiting for database...~%") + (sleep 3) ; Wait for database to be ready + (handler-case + (progn + (format t "Attempting to load stream queue from M3U file...~%") + (if (db:connected-p) + (progn + (format t "Database is connected, loading queue...~%") + (load-queue-from-m3u)) + (format t "Database not connected yet, skipping queue load~%"))) + (error (e) + (format t "โœ— Warning: Could not load stream queue: ~a~%" e)))) + :name "queue-loader") + (format t "Server started! Visit http://localhost:~a/asteroid/~%" port)) (defun stop-server () diff --git a/static/js/admin.js b/static/js/admin.js index 0626b27..45ad265 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -28,11 +28,13 @@ document.addEventListener('DOMContentLoaded', function() { // Queue controls const refreshQueueBtn = document.getElementById('refresh-queue'); + const loadFromM3uBtn = document.getElementById('load-from-m3u'); const clearQueueBtn = document.getElementById('clear-queue-btn'); const addRandomBtn = document.getElementById('add-random-tracks'); const queueSearchInput = document.getElementById('queue-track-search'); if (refreshQueueBtn) refreshQueueBtn.addEventListener('click', loadStreamQueue); + if (loadFromM3uBtn) loadFromM3uBtn.addEventListener('click', loadQueueFromM3U); if (clearQueueBtn) clearQueueBtn.addEventListener('click', clearStreamQueue); if (addRandomBtn) addRandomBtn.addEventListener('click', addRandomTracks); if (queueSearchInput) queueSearchInput.addEventListener('input', searchTracksForQueue); @@ -103,8 +105,6 @@ function renderPage() {
${track.album || 'Unknown Album'}
- -
@@ -368,14 +368,20 @@ function displayStreamQueue() { let html = '
'; streamQueue.forEach((item, index) => { if (item) { + const isFirst = index === 0; + const isLast = index === streamQueue.length - 1; html += ` -
+
${index + 1}
${item.title || 'Unknown'}
${item.artist || 'Unknown Artist'}
- +
+ + + +
`; } @@ -584,3 +590,71 @@ async function updateLiveStreamInfo() { } } } + +// Load queue from M3U file +async function loadQueueFromM3U() { + if (!confirm('Load queue from stream-queue.m3u file? This will replace the current queue.')) { + return; + } + + try { + const response = await fetch('/api/asteroid/stream/queue/load-m3u', { + method: 'POST' + }); + const result = await response.json(); + const data = result.data || result; + + if (data.status === 'success') { + alert(`Successfully loaded ${data.count} tracks from M3U file!`); + loadStreamQueue(); + } else { + alert('Error loading from M3U: ' + (data.message || 'Unknown error')); + } + } catch (error) { + console.error('Error loading from M3U:', error); + alert('Error loading from M3U: ' + error.message); + } +} + +// Move track up in queue +async function moveTrackUp(index) { + if (index === 0) return; + + // Swap with previous track + const newQueue = [...streamQueue]; + [newQueue[index - 1], newQueue[index]] = [newQueue[index], newQueue[index - 1]]; + + await reorderQueue(newQueue); +} + +// Move track down in queue +async function moveTrackDown(index) { + if (index === streamQueue.length - 1) return; + + // Swap with next track + const newQueue = [...streamQueue]; + [newQueue[index], newQueue[index + 1]] = [newQueue[index + 1], newQueue[index]]; + + await reorderQueue(newQueue); +} + +// Reorder the queue +async function reorderQueue(newQueue) { + try { + const trackIds = newQueue.map(track => track.id).join(','); + const response = await fetch('/api/asteroid/stream/queue/reorder?track-ids=' + trackIds, { + method: 'POST' + }); + const result = await response.json(); + const data = result.data || result; + + if (data.status === 'success') { + loadStreamQueue(); + } else { + alert('Error reordering queue: ' + (data.message || 'Unknown error')); + } + } catch (error) { + console.error('Error reordering queue:', error); + alert('Error reordering queue: ' + error.message); + } +} diff --git a/stream-control.lisp b/stream-control.lisp index 3bad8ee..afba1a0 100644 --- a/stream-control.lisp +++ b/stream-control.lisp @@ -96,6 +96,54 @@ (format stream "~a~%" docker-path)))))) t) +(defun load-queue-from-m3u () + "Load the stream queue from the existing M3U file" + (let ((playlist-path (merge-pathnames "stream-queue.m3u" + (asdf:system-source-directory :asteroid)))) + (format t "Checking for M3U file at: ~a~%" playlist-path) + (if (probe-file playlist-path) + (handler-case + (progn + (format t "M3U file found, loading...~%") + (format t "Available collections: ~a~%" (db:collections)) + (let ((all-tracks (db:select "tracks" (db:query :all)))) + (format t "Found ~d tracks in database~%" (length all-tracks)) + (when (> (length all-tracks) 0) + (format t "Sample track: ~a~%" (first all-tracks))) + (when (= (length all-tracks) 0) + (format t "โš  Warning: No tracks in database. Please scan your music library first!~%") + (format t " Visit the Admin page and click 'Scan Library' to add tracks.~%") + (format t " After scanning, restart the server to load the queue.~%") + (return-from load-queue-from-m3u nil)) + (with-open-file (stream playlist-path :direction :input) + (let ((track-ids '()) + (line-count 0)) + (loop for line = (read-line stream nil) + while line + do (progn + (incf line-count) + (when (and (> (length line) 0) + (not (char= (char line 0) #\#))) + ;; This is a file path line, find the track ID + (format t "Processing line ~d: ~a~%" line-count line) + (dolist (track all-tracks) + (let* ((file-path (gethash "file-path" track)) + (file-path-str (if (listp file-path) (first file-path) file-path)) + (docker-path (convert-to-docker-path file-path-str))) + (when (string= docker-path line) + (let ((id (gethash "_id" track))) + (push (if (listp id) (first id) id) track-ids) + (format t " Matched track ID: ~a~%" id)))))))) + (setf *stream-queue* (nreverse track-ids)) + (format t "โœ“ Loaded ~d tracks from stream-queue.m3u~%" (length *stream-queue*)) + (length *stream-queue*))))) + (error (e) + (format t "โœ— Error loading queue from M3U: ~a~%" e) + nil)) + (progn + (format t "โœ— M3U file not found at: ~a~%" playlist-path) + nil)))) + (defun regenerate-stream-playlist () "Regenerate the main stream playlist from the current queue" (let ((playlist-path (merge-pathnames "stream-queue.m3u" diff --git a/stream-queue.m3u b/stream-queue.m3u index 4288342..c804f57 100644 --- a/stream-queue.m3u +++ b/stream-queue.m3u @@ -1,19 +1,37 @@ #EXTM3U #EXTINF:0, -/app/music/Model500/2015 - Digital Solutions/02-model_500-electric_night.flac +/app/music/Vector Lovers/2005 - Capsule For One/08 - Empty Buildings, Falling Rain.mp3 #EXTINF:0, -/app/music/Kraftwerk/1978 - The Man-Machine \[2009 Digital Remaster]/02 - Spacelab.flac +/app/music/The Orb/1991 - The Orb's Adventures Beyond the Ultraworld (Double Album)/01 Little Fluffy Clouds.mp3 #EXTINF:0, -/app/music/Kraftwerk/1981 - Computer World \[2009 Digital Remaster]/03 - Numbers.flac +/app/music/Underworld/1996 - Second Toughest In The Infants/01. Underworld - Juanita, Kiteless, To Dream Of Love.flac #EXTINF:0, -/app/music/Model500/2015 - Digital Solutions/08-model_500-digital_solutions.flac +/app/music/Vector Lovers/2005 - Capsule For One/02 - Arrival, Metropolis.mp3 #EXTINF:0, -/app/music/Model500/2015 - Digital Solutions/02-model_500-electric_night.flac +/app/music/Vector Lovers/2005 - Capsule For One/11 - Capsule For One.mp3 +#EXTINF:0, +/app/music/Underworld/1996 - Second Toughest In The Infants/05. Underworld - Pearls Girl.flac +#EXTINF:0, +/app/music/Kraftwerk/1978 - The Man-Machine/04 - The Model.flac +#EXTINF:0, +/app/music/Model500/2015 - Digital Solutions/01-model_500-hi_nrg.flac +#EXTINF:0, +/app/music/Model500/2015 - Digital Solutions/07-model_500-station.flac +#EXTINF:0, +/app/music/Model500/\[1985] - Night Drive/01. Night Drive (Thru Babylon).mp3 +#EXTINF:0, +/app/music/Model500/\[1988] - Interference/05. OK Corral.mp3 +#EXTINF:0, +/app/music/This Mortal Coil/1984 - It'll End In Tears/02 - Song to the Siren.flac #EXTINF:0, /app/music/This Mortal Coil/1984 - It'll End In Tears/09 - Barramundi.flac #EXTINF:0, -/app/music/Underworld/1996 - Second Toughest In The Infants/04. Underworld - Rowla.flac +/app/music/Boards of Canada/1998 - Music Has the Right to Children/07 Turquoise Hexagon Sun.flac #EXTINF:0, -/app/music/This Mortal Coil/1984 - It'll End In Tears/10 - Dreams Made Flesh.flac +/app/music/Aphex Twin/1992 - Selected Ambient Works 85-92/09 Schottkey 7Th Path.flac #EXTINF:0, -/app/music/Underworld/1996 - Second Toughest In The Infants/07. Underworld - Blueski.flac +/app/music/Aphex Twin/1992 - Selected Ambient Works 85-92/08 We Are the Music Makers.flac +#EXTINF:0, +/app/music/LaBradford/1995 - A Stable Reference/2 El Lago.flac +#EXTINF:0, +/app/music/Orbital/1993 - Orbital - Orbital 2 (Brown Album - TRUCD2, 828 386.2)/00. Planet Of The Shapes.mp3 diff --git a/template/admin.chtml b/template/admin.chtml index 0397ba9..8d843c6 100644 --- a/template/admin.chtml +++ b/template/admin.chtml @@ -12,11 +12,11 @@

๐ŸŽ›๏ธ ADMIN DASHBOARD

@@ -127,6 +127,7 @@
+
diff --git a/template/audio-player-frame.chtml b/template/audio-player-frame.chtml new file mode 100644 index 0000000..1dfe76f --- /dev/null +++ b/template/audio-player-frame.chtml @@ -0,0 +1,162 @@ + + + + + + + + + +
+ ๐ŸŸข LIVE: + +
+ + + +
+ + + + Loading... +
+ + + + diff --git a/template/frameset-wrapper.chtml b/template/frameset-wrapper.chtml new file mode 100644 index 0000000..0cf1f2e --- /dev/null +++ b/template/frameset-wrapper.chtml @@ -0,0 +1,23 @@ + + + + ๐ŸŽต ASTEROID RADIO ๐ŸŽต + + + + + + + + + <body> + <p>Your browser does not support frames. Please use a modern browser or visit <a href="/asteroid/content">the main site</a>.</p> + </body> + + + diff --git a/template/front-page-content.chtml b/template/front-page-content.chtml new file mode 100644 index 0000000..f4e8c97 --- /dev/null +++ b/template/front-page-content.chtml @@ -0,0 +1,47 @@ + + + + ๐ŸŽต ASTEROID RADIO ๐ŸŽต + + + + + + + +
+
+

๐ŸŽต ASTEROID RADIO ๐ŸŽต

+ +
+ +
+
+

Station Status

+

๐ŸŸข LIVE - Broadcasting asteroid music for hackers

+

Current listeners: 0

+

Stream quality: AAC 96kbps Stereo

+
+ +
+

๐ŸŸข LIVE STREAM

+

The live stream player is now in the persistent bar at the bottom of the page.

+

Stream URL:

+

Format:

+

Status: โ— BROADCASTING

+
+ +
+
+
+ + diff --git a/template/login.chtml b/template/login.chtml index 7c315c2..94eafe3 100644 --- a/template/login.chtml +++ b/template/login.chtml @@ -11,10 +11,10 @@

๐ŸŽต ASTEROID RADIO - LOGIN

@@ -37,11 +37,6 @@
-
- Default Admin Credentials:
- Username:
admin
- Password:
asteroid123 -
diff --git a/template/player-content.chtml b/template/player-content.chtml new file mode 100644 index 0000000..c347c46 --- /dev/null +++ b/template/player-content.chtml @@ -0,0 +1,120 @@ + + + + Asteroid Radio - Web Player + + + + + + + +
+

๐ŸŽต WEB PLAYER

+ + + +
+

๐ŸŸข Live Radio Stream

+

The live stream player is now in the persistent bar at the bottom of the page. It will continue playing as you navigate between pages!

+
+ +
+ + +
+

Personal Track Library

+
+ + +
+
Loading tracks...
+
+ + +
+
+ + +
+

Audio Player

+
+
+
๐ŸŽต
+
+
No track selected
+
Unknown Artist
+
Unknown Album
+
+
+ + + +
+ + + + + +
+ +
+
+ 0:00 / 0:00 +
+
+ + +
+
+
+
+ + +
+

Playlists

+
+ + +
+ +
+
+
No playlists created yet.
+
+
+
+ + +
+

Play Queue

+
+ + +
+
+
Queue is empty
+
+
+
+ + diff --git a/template/profile.chtml b/template/profile.chtml index 5398903..36b964f 100644 --- a/template/profile.chtml +++ b/template/profile.chtml @@ -12,10 +12,10 @@

๐Ÿ‘ค USER PROFILE

diff --git a/template/register.chtml b/template/register.chtml index 95ba65f..b7aa7ff 100644 --- a/template/register.chtml +++ b/template/register.chtml @@ -11,10 +11,10 @@

๐ŸŽต ASTEROID RADIO - REGISTER

diff --git a/template/users.chtml b/template/users.chtml index bd63584..eb357f0 100644 --- a/template/users.chtml +++ b/template/users.chtml @@ -11,9 +11,9 @@

๐Ÿ‘ฅ USER MANAGEMENT