diff --git a/asteroid.lisp b/asteroid.lisp index f123874..31c7bbf 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -78,6 +78,54 @@ :song (string-trim " " (subseq title (+ pos 3)))) (list :artist "Unknown" :song title)))) +(defun get-playlist-metadata () + "Parse metadata from the stream-queue.m3u playlist file. + Returns a plist with :playlist-name, :phase, :description, :curator, :duration" + (let ((playlist-path (merge-pathnames "playlists/stream-queue.m3u" + (asdf:system-source-directory :asteroid)))) + (if (probe-file playlist-path) + (handler-case + (with-open-file (stream playlist-path :direction :input) + (let ((metadata (list :playlist-name nil + :phase nil + :description nil + :curator nil + :duration nil))) + (loop for line = (read-line stream nil nil) + while line + do (cond + ((cl-ppcre:scan "^#PLAYLIST:" line) + (setf (getf metadata :playlist-name) + (string-trim " " (subseq line 10)))) + ((cl-ppcre:scan "^#PHASE:" line) + (setf (getf metadata :phase) + (string-trim " " (subseq line 7)))) + ((cl-ppcre:scan "^#DESCRIPTION:" line) + (setf (getf metadata :description) + (string-trim " " (subseq line 13)))) + ((cl-ppcre:scan "^#CURATOR:" line) + (setf (getf metadata :curator) + (string-trim " " (subseq line 9)))) + ((cl-ppcre:scan "^#DURATION:" line) + (setf (getf metadata :duration) + (string-trim " " (subseq line 10)))) + ;; Stop parsing after we hit actual track entries + ((and (> (length line) 0) + (not (char= (char line 0) #\#))) + (return)))) + metadata)) + (error (e) + (format t "Error reading playlist metadata: ~a~%" e) + (list :playlist-name nil :phase nil :description nil :curator nil :duration nil))) + (list :playlist-name nil :phase nil :description nil :curator nil :duration nil)))) + +(defun get-curated-channel-name () + "Get the display name for the curated channel from playlist metadata. + Falls back to 'Curated' if no phase is defined." + (let* ((metadata (get-playlist-metadata)) + (phase (getf metadata :phase))) + (or phase "Curated"))) + (defun generate-music-search-url (artist song) "Generate MusicBrainz search URL for artist and song" ;; Simple search without field prefixes works better with URL encoding @@ -767,6 +815,7 @@ :listeners "0" :stream-quality "128kbps MP3" :stream-base-url *stream-base-url* + :curated-channel-name (get-curated-channel-name) :default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*) :default-stream-encoding "audio/aac" :default-stream-encoding-desc "AAC 96kbps Stereo" @@ -793,6 +842,7 @@ :listeners "0" :stream-quality "128kbps MP3" :stream-base-url *stream-base-url* + :curated-channel-name (get-curated-channel-name) :now-playing-artist "The Void" :now-playing-track "Silence" :now-playing-album "Startup Sounds" @@ -806,6 +856,7 @@ (clip:process-to-string (load-template "audio-player-frame") :stream-base-url *stream-base-url* + :curated-channel-name (get-curated-channel-name) :default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*) :default-stream-encoding "audio/aac")) @@ -1209,6 +1260,7 @@ (clip:process-to-string (load-template "popout-player") :stream-base-url *stream-base-url* + :curated-channel-name (get-curated-channel-name) :default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*) :default-stream-encoding "audio/aac")) diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index b76057e..628324d 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -13,73 +13,147 @@ (defvar *is-reconnecting* false) (defvar *reconnect-timeout* nil) - ;; Stream quality configuration - (defun get-stream-config (stream-base-url encoding) - (let ((config (ps:create - :aac (ps:create - :url (+ stream-base-url "/asteroid.aac") - :format "AAC 96kbps Stereo" - :type "audio/aac" - :mount "asteroid.aac") - :mp3 (ps:create - :url (+ stream-base-url "/asteroid.mp3") - :format "MP3 128kbps Stereo" - :type "audio/mpeg" - :mount "asteroid.mp3") - :low (ps:create - :url (+ stream-base-url "/asteroid-low.mp3") - :format "MP3 64kbps Stereo" - :type "audio/mpeg" - :mount "asteroid-low.mp3") - :shuffle (ps:create - :url (+ stream-base-url "/asteroid-shuffle.mp3") - :format "Shuffle MP3 96kbps" - :type "audio/mpeg" - :mount "asteroid-shuffle.mp3")))) - (ps:getprop config encoding))) + ;; Stream configuration by channel and quality + ;; Curated channel has multiple quality options, shuffle has only one + (defun get-stream-config (stream-base-url channel quality) + (let ((curated-config (ps:create + :aac (ps:create + :url (+ stream-base-url "/asteroid.aac") + :format "AAC 96kbps Stereo" + :type "audio/aac" + :mount "asteroid.aac") + :mp3 (ps:create + :url (+ stream-base-url "/asteroid.mp3") + :format "MP3 128kbps Stereo" + :type "audio/mpeg" + :mount "asteroid.mp3") + :low (ps:create + :url (+ stream-base-url "/asteroid-low.mp3") + :format "MP3 64kbps Stereo" + :type "audio/mpeg" + :mount "asteroid-low.mp3"))) + (shuffle-config (ps:create + :url (+ stream-base-url "/asteroid-shuffle.mp3") + :format "Shuffle MP3 96kbps" + :type "audio/mpeg" + :mount "asteroid-shuffle.mp3"))) + (if (= channel "shuffle") + shuffle-config + (ps:getprop curated-config quality)))) - ;; Change stream quality - (defun change-stream-quality () - (let* ((selector (ps:chain document (get-element-by-id "stream-quality"))) + ;; Get current channel from selector or localStorage + (defun get-current-channel () + (let ((selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel"))))) + (if selector + (ps:@ selector value) + (or (ps:chain local-storage (get-item "stream-channel")) "curated")))) + + ;; Get current quality from selector or localStorage + (defun get-current-quality () + (let ((selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality"))))) + (if selector + (ps:@ selector value) + (or (ps:chain local-storage (get-item "stream-quality")) "aac")))) + + ;; Update quality selector state based on channel + (defun update-quality-selector-state () + (let* ((channel (get-current-channel)) + (quality-selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality"))))) + (when quality-selector + (if (= channel "shuffle") + (progn + (setf (ps:@ quality-selector disabled) t) + (setf (ps:@ quality-selector title) "Shuffle channel has fixed quality")) + (progn + (setf (ps:@ quality-selector disabled) nil) + (setf (ps:@ quality-selector title) "")))))) + + ;; Change channel (curated vs shuffle) + (defun change-channel () + (let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel")))) + (channel (ps:@ channel-selector value)) (stream-base-url (ps:chain document (get-element-by-id "stream-base-url"))) - (config (get-stream-config (ps:@ stream-base-url value) (ps:@ selector value))) - (audio-element (ps:chain document (get-element-by-id "live-audio"))) + (quality (get-current-quality)) + (config (get-stream-config (ps:@ stream-base-url value) channel quality)) + (audio-element (or (ps:chain document (get-element-by-id "live-audio")) + (ps:chain document (get-element-by-id "persistent-audio")))) (source-element (ps:chain document (get-element-by-id "audio-source"))) - (was-playing (not (ps:@ audio-element paused))) - (current-time (ps:@ audio-element current-time))) + (was-playing (and audio-element (not (ps:@ audio-element paused))))) ;; Save preference - (ps:chain local-storage (set-item "stream-quality" (ps:@ selector value))) + (ps:chain local-storage (set-item "stream-channel" channel)) + + ;; Update quality selector state + (update-quality-selector-state) + + ;; Update stream information display + (update-stream-information) + + ;; Update audio player + (when source-element + (setf (ps:@ source-element src) (ps:@ config url)) + (setf (ps:@ source-element type) (ps:@ config type))) + (when audio-element + (ps:chain audio-element (load)) + ;; Resume playback if it was playing + (when was-playing + (ps:chain (ps:chain audio-element (play)) + (catch (lambda (e) + (ps:chain console (log "Autoplay prevented:" e))))))) + + ;; If in frameset mode, notify the player frame to update + (when (not (= (ps:@ window parent) window)) + (ps:try + (let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame"))) + (when (and player-frame (ps:@ player-frame sync-channel-from-storage)) + (ps:chain player-frame (sync-channel-from-storage)))) + (:catch (e) nil))) + + ;; Immediately refresh now playing and recently played + (update-now-playing) + (when (ps:@ window update-recently-played) + (ps:chain window (update-recently-played))))) + + ;; Change stream quality (bitrate) + (defun change-stream-quality () + (let* ((selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality")))) + (stream-base-url (ps:chain document (get-element-by-id "stream-base-url"))) + (channel (get-current-channel)) + (quality (ps:@ selector value)) + (config (get-stream-config (ps:@ stream-base-url value) channel quality)) + (audio-element (or (ps:chain document (get-element-by-id "live-audio")) + (ps:chain document (get-element-by-id "persistent-audio")))) + (source-element (ps:chain document (get-element-by-id "audio-source"))) + (was-playing (and audio-element (not (ps:@ audio-element paused))))) + + ;; Save preference + (ps:chain local-storage (set-item "stream-quality" quality)) ;; Update stream information (update-stream-information) ;; Update audio player - (setf (ps:@ source-element src) (ps:@ config url)) - (setf (ps:@ source-element type) (ps:@ config type)) - (ps:chain audio-element (load)) - - ;; Resume playback if it was playing - (when was-playing - (ps:chain (ps:chain audio-element (play)) - (catch (lambda (e) - (ps:chain console (log "Autoplay prevented:" e)))))))) + (when source-element + (setf (ps:@ source-element src) (ps:@ config url)) + (setf (ps:@ source-element type) (ps:@ config type))) + (when audio-element + (ps:chain audio-element (load)) + ;; Resume playback if it was playing + (when was-playing + (ps:chain (ps:chain audio-element (play)) + (catch (lambda (e) + (ps:chain console (log "Autoplay prevented:" e))))))))) - ;; Get current mount from stream quality selection - ;; Checks local selector first, then sibling player-frame (for frameset mode) + ;; Get current mount from channel and quality selection + ;; Checks local selectors first, then sibling player-frame (for frameset mode) (defun get-current-mount () - (let* ((selector (ps:chain document (get-element-by-id "stream-quality"))) - ;; If no local selector, try to get from sibling player-frame (frameset mode) - (player-frame-selector - (when (and (not selector) - (not (= (ps:@ window parent) window))) - (ps:try - (let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame"))) - (when player-frame - (ps:chain player-frame document (get-element-by-id "stream-quality")))) - (:catch (e) nil)))) - (effective-selector (or selector player-frame-selector)) - (quality (if effective-selector (ps:@ effective-selector value) "aac")) + (let* ((channel (get-current-channel)) + (quality (get-current-quality)) (stream-base-url (or (ps:chain document (get-element-by-id "stream-base-url")) (when (not (= (ps:@ window parent) window)) (ps:try @@ -88,7 +162,7 @@ (ps:chain player-frame document (get-element-by-id "stream-base-url")))) (:catch (e) nil))))) (config (when stream-base-url - (get-stream-config (ps:@ stream-base-url value) quality)))) + (get-stream-config (ps:@ stream-base-url value) channel quality)))) (if config (ps:@ config mount) "asteroid.mp3"))) ;; Update now playing info from API @@ -109,22 +183,34 @@ ;; Update stream information (defun update-stream-information () - (let* ((selector (ps:chain document (get-element-by-id "stream-quality"))) + (let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel")))) + (quality-selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality")))) (stream-base-url (ps:chain document (get-element-by-id "stream-base-url"))) + (stream-channel (or (ps:chain local-storage (get-item "stream-channel")) "curated")) (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))) - ;; Update selector if needed - (when (and selector (not (= (ps:@ selector value) stream-quality))) - (setf (ps:@ selector value) stream-quality) - (ps:chain selector (dispatch-event (ps:new (-event "change"))))) + ;; Update channel selector if needed + (when (and channel-selector (not (= (ps:@ channel-selector value) stream-channel))) + (setf (ps:@ channel-selector value) stream-channel)) + + ;; Update quality selector if needed + (when (and quality-selector (not (= (ps:@ quality-selector value) stream-quality))) + (setf (ps:@ quality-selector value) stream-quality)) + + ;; Update quality selector state (disabled for shuffle) + (update-quality-selector-state) ;; Update stream info display (when stream-base-url - (let ((config (get-stream-config (ps:@ stream-base-url value) stream-quality))) - (setf (ps:@ (ps:chain document (get-element-by-id "stream-url")) text-content) - (ps:@ config url)) - (setf (ps:@ (ps:chain document (get-element-by-id "stream-format")) text-content) - (ps:@ config format)) + (let ((config (get-stream-config (ps:@ stream-base-url value) stream-channel stream-quality))) + (let ((url-el (ps:chain document (get-element-by-id "stream-url")))) + (when url-el + (setf (ps:@ url-el text-content) (ps:@ config url)))) + (let ((format-el (ps:chain document (get-element-by-id "stream-format")))) + (when format-el + (setf (ps:@ format-el text-content) (ps:@ config format)))) (let ((status-quality (ps:chain document (query-selector "[data-text=\"stream-quality\"]")))) (when status-quality (setf (ps:@ status-quality text-content) (ps:@ config format)))))))) @@ -219,8 +305,9 @@ (let* ((container (ps:chain document (get-element-by-id "audio-container"))) (old-audio (ps:chain document (get-element-by-id "live-audio"))) (stream-base-url (ps:chain document (get-element-by-id "stream-base-url"))) - (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac")) - (config (get-stream-config (ps:@ stream-base-url value) stream-quality))) + (stream-channel (get-current-channel)) + (stream-quality (get-current-quality)) + (config (get-stream-config (ps:@ stream-base-url value) stream-channel stream-quality))) (when (and container old-audio) ;; Reset spectrum analyzer before removing audio @@ -469,21 +556,25 @@ ;; Update now playing (update-now-playing) - ;; Refresh now playing immediately when user switches streams - (let ((selector (ps:chain document (get-element-by-id "stream-quality")))) - (when (not selector) - (setf selector - (ps:try - (let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame"))) - (when player-frame - (ps:chain player-frame document (get-element-by-id "stream-quality")))) - (:catch (e) nil)))) - (when selector - (ps:chain selector + ;; Refresh now playing immediately when user switches channel or quality + (let ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel")))) + (quality-selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality"))))) + (when channel-selector + (ps:chain channel-selector (add-event-listener "change" (lambda (_ev) ;; Small delay so localStorage / UI state settles + (ps:chain window (set-timeout update-now-playing 50)) + (when (ps:@ window update-recently-played) + (ps:chain window (set-timeout (ps:@ window update-recently-played) 50))))))) + (when quality-selector + (ps:chain quality-selector + (add-event-listener + "change" + (lambda (_ev) (ps:chain window (set-timeout update-now-playing 50))))))) ;; Attach event listeners to audio element diff --git a/parenscript/recently-played.lisp b/parenscript/recently-played.lisp index 16b84df..121a294 100644 --- a/parenscript/recently-played.lisp +++ b/parenscript/recently-played.lisp @@ -7,28 +7,32 @@ (ps:ps (progn - ;; Get current mount from stream quality selection - ;; Checks local selector first, then sibling player-frame (for frameset mode) + ;; Get current channel from selector or localStorage + (defun get-current-channel () + (let ((selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel"))))) + (if selector + (ps:@ selector value) + (or (ps:chain local-storage (get-item "stream-channel")) "curated")))) + + ;; Get current quality from selector or localStorage + (defun get-current-quality () + (let ((selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality"))))) + (if selector + (ps:@ selector value) + (or (ps:chain local-storage (get-item "stream-quality")) "aac")))) + + ;; Get current mount from channel and quality selection (defun get-current-mount-for-recently-played () - (let* ((selector (ps:chain document (get-element-by-id "stream-quality"))) - ;; If no local selector, try to get from sibling player-frame (frameset mode) - (player-frame-selector - (when (and (not selector) - (not (= (ps:@ window parent) window))) - (ps:try - (let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame"))) - (when player-frame - (ps:chain player-frame document (get-element-by-id "stream-quality")))) - (:catch (e) nil)))) - (effective-selector (or selector player-frame-selector)) - (quality (or (when effective-selector (ps:@ effective-selector value)) - (ps:chain local-storage (get-item "stream-quality")) - "aac"))) - (cond - ((= quality "shuffle") "asteroid-shuffle.mp3") - ((= quality "low") "asteroid-low.mp3") - ((= quality "mp3") "asteroid.mp3") - (t "asteroid.aac")))) + (let ((channel (get-current-channel)) + (quality (get-current-quality))) + (if (= channel "shuffle") + "asteroid-shuffle.mp3" + (cond + ((= quality "low") "asteroid-low.mp3") + ((= quality "mp3") "asteroid.mp3") + (t "asteroid.aac"))))) ;; Update recently played tracks display (defun update-recently-played () @@ -105,17 +109,11 @@ (if panel (progn (update-recently-played) - ;; Refresh immediately when user switches streams - (let ((selector (ps:chain document (get-element-by-id "stream-quality")))) - (when (not selector) - (setf selector - (ps:try - (let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame"))) - (when player-frame - (ps:chain player-frame document (get-element-by-id "stream-quality")))) - (:catch (e) nil)))) - (when selector - (ps:chain selector + ;; Refresh immediately when user switches channel + (let ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel"))))) + (when channel-selector + (ps:chain channel-selector (add-event-listener "change" (lambda (_ev) diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp index 7711288..b430417 100644 --- a/parenscript/stream-player.lisp +++ b/parenscript/stream-player.lisp @@ -11,38 +11,155 @@ ;; Stream Configuration ;; ======================================== - ;; Get stream configuration for a given quality - (defun get-stream-config (stream-base-url encoding) - (let ((config (ps:create - :aac (ps:create :url (+ stream-base-url "/asteroid.aac") - :type "audio/aac" - :format "AAC 96kbps Stereo" - :mount "asteroid.aac") - :mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3") - :type "audio/mpeg" - :format "MP3 128kbps Stereo" - :mount "asteroid.mp3") - :low (ps:create :url (+ stream-base-url "/asteroid-low.mp3") - :type "audio/mpeg" - :format "MP3 64kbps Stereo" - :mount "asteroid-low.mp3") - :shuffle (ps:create :url (+ stream-base-url "/asteroid-shuffle.mp3") - :type "audio/mpeg" - :format "Shuffle MP3 96kbps" - :mount "asteroid-shuffle.mp3")))) - (ps:getprop config encoding))) + ;; Get stream configuration for a given channel and quality + ;; Curated channel has multiple quality options, shuffle has only one + (defun get-stream-config (stream-base-url channel quality) + (let ((curated-config (ps:create + :aac (ps:create :url (+ stream-base-url "/asteroid.aac") + :type "audio/aac" + :format "AAC 96kbps Stereo" + :mount "asteroid.aac") + :mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3") + :type "audio/mpeg" + :format "MP3 128kbps Stereo" + :mount "asteroid.mp3") + :low (ps:create :url (+ stream-base-url "/asteroid-low.mp3") + :type "audio/mpeg" + :format "MP3 64kbps Stereo" + :mount "asteroid-low.mp3"))) + (shuffle-config (ps:create :url (+ stream-base-url "/asteroid-shuffle.mp3") + :type "audio/mpeg" + :format "Shuffle MP3 96kbps" + :mount "asteroid-shuffle.mp3"))) + (if (= channel "shuffle") + shuffle-config + (ps:getprop curated-config quality)))) + + ;; Get current channel from selector or localStorage + (defun get-current-channel () + (let ((selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel"))))) + (if selector + (ps:@ selector value) + (or (ps:chain local-storage (get-item "stream-channel")) "curated")))) + + ;; Get current quality from selector or localStorage + (defun get-current-quality () + (let ((selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality"))))) + (if selector + (ps:@ selector value) + (or (ps:chain local-storage (get-item "stream-quality")) "aac")))) + + ;; Update quality selector state based on channel + (defun update-quality-selector-state () + (let* ((channel (get-current-channel)) + (quality-selector (or (ps:chain document (get-element-by-id "stream-quality")) + (ps:chain document (get-element-by-id "popout-stream-quality"))))) + (when quality-selector + (if (= channel "shuffle") + (progn + (setf (ps:@ quality-selector disabled) t) + (setf (ps:@ quality-selector title) "Shuffle channel has fixed quality")) + (progn + (setf (ps:@ quality-selector disabled) nil) + (setf (ps:@ quality-selector title) "")))))) + + ;; Change channel (curated vs shuffle) + ;; Called from content frame or popout - updates localStorage and notifies frame player + (defun change-channel () + (let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel")) + (ps:chain document (get-element-by-id "popout-stream-channel")))) + (channel (ps:@ channel-selector value)) + (stream-base-url-el (ps:chain document (get-element-by-id "stream-base-url"))) + (stream-base-url (when stream-base-url-el (ps:@ stream-base-url-el value))) + (quality (get-current-quality)) + (audio-element (or (ps:chain document (get-element-by-id "persistent-audio")) + (ps:chain document (get-element-by-id "live-audio")))) + (source-element (ps:chain document (get-element-by-id "audio-source"))) + (was-playing (and audio-element (not (ps:@ audio-element paused))))) + + ;; Save preference + (ps:chain local-storage (set-item "stream-channel" channel)) + + ;; Update quality selector state + (update-quality-selector-state) + + ;; If we have audio element (popout player), update it + (when (and stream-base-url audio-element source-element) + (let ((config (get-stream-config stream-base-url channel quality))) + ;; Swap source and reload + (setf (ps:@ source-element src) (ps:@ config url)) + (setf (ps:@ source-element type) (ps:@ config type)) + (ps:chain audio-element (load)) + + ;; Resume playback if it was playing + (when was-playing + (ps:chain audio-element (play) + (catch (lambda (e) + (ps:chain console (log "Autoplay prevented:" e)))))))) + + ;; If in frameset mode, notify the player frame to update + (when (not (= (ps:@ window parent) window)) + (ps:try + (let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame"))) + (when (and player-frame (ps:@ player-frame sync-channel-from-storage)) + (ps:chain player-frame (sync-channel-from-storage)))) + (:catch (e) nil))) + + ;; Refresh now-playing immediately + (when (ps:chain document (get-element-by-id "mini-now-playing")) + (ps:chain window (set-timeout update-mini-now-playing 50))) + (when (or (ps:chain document (get-element-by-id "popout-track-title")) + (ps:chain document (get-element-by-id "popout-track-artist"))) + (ps:chain window (set-timeout update-popout-now-playing 50))))) + + ;; Sync channel from localStorage (called by content frame when channel changes) + (defun sync-channel-from-storage () + (let* ((channel (or (ps:chain local-storage (get-item "stream-channel")) "curated")) + (quality (get-current-quality)) + (stream-base-url-el (ps:chain document (get-element-by-id "stream-base-url"))) + (stream-base-url (when stream-base-url-el (ps:@ stream-base-url-el value))) + (channel-selector (ps:chain document (get-element-by-id "stream-channel"))) + (audio-element (ps:chain document (get-element-by-id "persistent-audio"))) + (source-element (ps:chain document (get-element-by-id "audio-source"))) + (was-playing (and audio-element (not (ps:@ audio-element paused))))) + + ;; Update channel selector dropdown to match localStorage + (when (and channel-selector (not (= (ps:@ channel-selector value) channel))) + (setf (ps:@ channel-selector value) channel)) + + (when (and stream-base-url audio-element source-element) + (let ((config (get-stream-config stream-base-url channel quality))) + ;; Update quality selector state + (update-quality-selector-state) + + ;; Swap source and reload + (setf (ps:@ source-element src) (ps:@ config url)) + (setf (ps:@ source-element type) (ps:@ config type)) + (ps:chain audio-element (load)) + + ;; Resume playback if it was playing + (when was-playing + (ps:chain audio-element (play) + (catch (lambda (e) + (ps:chain console (log "Autoplay prevented:" e)))))) + + ;; Refresh now-playing + (ps:chain window (set-timeout update-mini-now-playing 50)))))) ;; ======================================== ;; Stream Quality Selection ;; ======================================== - ;; Change stream quality + ;; Change stream quality (bitrate) (defun change-stream-quality () (let* ((selector (or (ps:chain document (get-element-by-id "stream-quality")) (ps:chain document (get-element-by-id "popout-stream-quality")))) (stream-base-url (ps:@ (ps:chain document (get-element-by-id "stream-base-url")) value)) + (channel (get-current-channel)) (selected-quality (ps:@ selector value)) - (config (get-stream-config stream-base-url selected-quality)) + (config (get-stream-config stream-base-url channel selected-quality)) (audio-element (or (ps:chain document (get-element-by-id "persistent-audio")) (ps:chain document (get-element-by-id "live-audio")))) (source-element (ps:chain document (get-element-by-id "audio-source"))) @@ -73,13 +190,12 @@ ;; Now Playing Updates ;; ======================================== - ;; Get current mount from stream quality selection + ;; Get current mount from channel and quality selection (defun get-current-mount () - (let* ((selector (or (ps:chain document (get-element-by-id "stream-quality")) - (ps:chain document (get-element-by-id "popout-stream-quality")))) - (quality (if selector (ps:@ selector value) "aac")) + (let* ((channel (get-current-channel)) + (quality (get-current-quality)) (stream-base-url (ps:@ (ps:chain document (get-element-by-id "stream-base-url")) value)) - (config (get-stream-config stream-base-url quality))) + (config (get-stream-config stream-base-url channel quality))) (if config (ps:@ config mount) "asteroid.mp3"))) ;; Update mini now playing display (for persistent player frame) @@ -173,8 +289,9 @@ (let* ((container (ps:chain document (query-selector ".persistent-player"))) (old-audio (ps:chain document (get-element-by-id "persistent-audio"))) (stream-base-url (ps:@ (ps:chain document (get-element-by-id "stream-base-url")) value)) - (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac")) - (config (get-stream-config stream-base-url stream-quality))) + (stream-channel (get-current-channel)) + (stream-quality (get-current-quality)) + (config (get-stream-config stream-base-url stream-channel stream-quality))) (unless (and container old-audio) (show-status "❌ Could not reconnect - reload page" true) @@ -422,12 +539,22 @@ ;; Attach event listeners (attach-audio-listeners audio-element) + ;; Restore user channel preference + (let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))) + (stream-channel (or (ps:chain local-storage (get-item "stream-channel")) "curated"))) + (when (and channel-selector (not (= (ps:@ channel-selector value) stream-channel))) + (setf (ps:@ channel-selector value) stream-channel) + ;; Sync the stream to the saved channel + (change-channel))) + ;; Restore user quality preference - (let ((selector (ps:chain document (get-element-by-id "stream-quality"))) + (let ((quality-selector (ps:chain document (get-element-by-id "stream-quality"))) (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))) - (when (and selector (not (= (ps:@ selector value) stream-quality))) - (setf (ps:@ selector value) stream-quality) - (ps:chain selector (dispatch-event (ps:new (-event "change")))))) + (when (and quality-selector (not (= (ps:@ quality-selector value) stream-quality))) + (setf (ps:@ quality-selector value) stream-quality))) + + ;; Update quality selector state based on channel + (update-quality-selector-state) ;; Start now playing updates (set-timeout update-mini-now-playing 1000) @@ -440,6 +567,21 @@ ;; Attach event listeners (attach-popout-listeners audio-element) + ;; Restore user channel preference + (let ((channel-selector (ps:chain document (get-element-by-id "popout-stream-channel"))) + (stream-channel (or (ps:chain local-storage (get-item "stream-channel")) "curated"))) + (when (and channel-selector (not (= (ps:@ channel-selector value) stream-channel))) + (setf (ps:@ channel-selector value) stream-channel))) + + ;; Restore user quality preference + (let ((quality-selector (ps:chain document (get-element-by-id "popout-stream-quality"))) + (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))) + (when (and quality-selector (not (= (ps:@ quality-selector value) stream-quality))) + (setf (ps:@ quality-selector value) stream-quality))) + + ;; Update quality selector state based on channel + (update-quality-selector-state) + ;; Start now playing updates (update-popout-now-playing) (set-interval update-popout-now-playing 5000) @@ -452,6 +594,8 @@ ;; Make functions globally accessible (setf (ps:@ window get-stream-config) get-stream-config) + (setf (ps:@ window change-channel) change-channel) + (setf (ps:@ window sync-channel-from-storage) sync-channel-from-storage) (setf (ps:@ window change-stream-quality) change-stream-quality) (setf (ps:@ window reconnect-stream) reconnect-stream) (setf (ps:@ window disable-frameset-mode) disable-frameset-mode) diff --git a/static/asteroid.css b/static/asteroid.css index 9555580..93c3445 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -1295,6 +1295,37 @@ body.persistent-player-container .quality-selector{ gap: 5px; } +body.persistent-player-container .channel-selector{ + display: flex; + align-items: center; + gap: 5px; +} + + + +body.persistent-player-container .channel-selector label{ + color: #00ff00; + font-size: 0.9em; +} + + + +body.persistent-player-container .channel-selector select{ + background: transparent; + color: #00ff00; + letter-spacing: 0.08rem; + border: 1px solid #00ff00; + padding: 3px 8px; + min-width: 140px; + font-size: 0.9em; + height: 26px; + line-height: 18px; +} + +body.persistent-player-container .channel-selector select:hover{ + background: #2a3441; +} + body.persistent-player-container .quality-selector label{ @@ -1311,12 +1342,20 @@ body.persistent-player-container .quality-selector select{ border: 1px solid #00ff00; padding: 3px 8px; min-width: 140px; + font-size: 0.9em; + height: 26px; + line-height: 18px; } body.persistent-player-container .quality-selector select:hover{ background: #2a3441; } +body.persistent-player-container .quality-selector select:disabled{ + opacity: 0.5; + cursor: not-allowed; +} + body.persistent-player-container audio{ flex: 1; min-width: 200px; diff --git a/static/asteroid.lass b/static/asteroid.lass index 8b5a6ff..6f7348b 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -1044,6 +1044,30 @@ :align-items "center" :gap "5px") + (.channel-selector + :display "flex" + :align-items "center" + :gap "5px") + + (.channel-selector + (label + :color "#00ff00" + :font-size "0.9em")) + + (.channel-selector + (select + :background "transparent" + :color "#00ff00" + :letter-spacing "0.08rem" + :border "1px solid #00ff00" + :padding "3px 8px" + :min-width "140px" + :font-size "0.9em" + :height "26px" + :line-height "18px") + ((:and select :hover) + :background "#2a3441")) + (.quality-selector (label :color "#00ff00" @@ -1056,9 +1080,15 @@ :letter-spacing "0.08rem" :border "1px solid #00ff00" :padding "3px 8px" - :min-width "140px") + :min-width "140px" + :font-size "0.9em" + :height "26px" + :line-height "18px") ((:and select :hover) - :background "#2a3441")) + :background "#2a3441") + ((:and select :disabled) + :opacity "0.5" + :cursor "not-allowed")) (audio :flex 1 diff --git a/template/audio-player-frame.ctml b/template/audio-player-frame.ctml index 9759454..494a4d6 100644 --- a/template/audio-player-frame.ctml +++ b/template/audio-player-frame.ctml @@ -13,14 +13,21 @@ LIVE: -
+
+ + +
+ +
diff --git a/template/front-page-content.ctml b/template/front-page-content.ctml index 51e8189..2cc4bb4 100644 --- a/template/front-page-content.ctml +++ b/template/front-page-content.ctml @@ -66,6 +66,16 @@

🟢 LIVE STREAM

+ + +
+ + +
+

Stream URL:

Stream Quality:

diff --git a/template/front-page.ctml b/template/front-page.ctml index d3190ef..adcf481 100644 --- a/template/front-page.ctml +++ b/template/front-page.ctml @@ -69,14 +69,22 @@

🟢 LIVE STREAM

+ +
+ + +
+ -
+
diff --git a/template/popout-player.ctml b/template/popout-player.ctml index cf7675f..016f6aa 100644 --- a/template/popout-player.ctml +++ b/template/popout-player.ctml @@ -26,14 +26,21 @@
-
+
+ + +
+ +