Add channel/quality selector separation with dynamic playlist phase names

- Separate Channel selector (Curated/Shuffle) from Quality selector (bitrate)
- Add channel selector to frame player, front page, and popout player
- Dynamic curated channel name from playlist #PHASE: metadata
- Channel selection syncs across all player contexts via localStorage
- Quality selector disabled when Shuffle channel selected (fixed bitrate)
- Fix reconnectStream to use channel-aware config
- Consistent CSS styling for selector heights
This commit is contained in:
Glenn Thompson 2025-12-14 23:23:29 +03:00
parent 55d63770d2
commit 238e880b86
10 changed files with 538 additions and 152 deletions

View File

@ -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"))

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -13,14 +13,21 @@
LIVE:
</span>
<div class="quality-selector">
<div class="channel-selector">
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<label for="stream-channel">Channel:</label>
<select id="stream-channel" onchange="changeChannel()">
<option value="curated">🎧 <c:splice lquery="(text curated-channel-name)">Curated</c:splice></option>
<option value="shuffle">🎲 Shuffle</option>
</select>
</div>
<div class="quality-selector">
<label for="stream-quality">Quality:</label>
<select id="stream-quality" onchange="changeStreamQuality()">
<option value="aac">AAC 96k</option>
<option value="mp3">MP3 128k</option>
<option value="low">MP3 64k</option>
<option value="shuffle">🎲 Shuffle 96k</option>
</select>
</div>

View File

@ -66,6 +66,16 @@
<main>
<div class="live-stream">
<h2 style="color: #00ff00; margin: 0;"><span class="live-stream-indicator" style="font-size: 1rem;">🟢</span> LIVE STREAM</h2>
<!-- Channel Selector -->
<div class="live-stream-quality" style="margin-bottom: 15px;">
<label for="stream-channel" class="live-stream-label"><strong>Channel:</strong></label>
<select id="stream-channel" onchange="changeChannel()">
<option value="curated">🎧 <c:splice lquery="(text curated-channel-name)">Curated</c:splice></option>
<option value="shuffle">🎲 Shuffle</option>
</select>
</div>
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<p><strong class="live-stream-label">Stream URL:</strong> <code id="stream-url" lquery="(text default-stream-url)"></code></p>
<p><strong class="live-stream-label">Stream Quality:</strong> <span id="stream-format" lquery="(text default-stream-encoding-desc)"></span></p>

View File

@ -69,14 +69,22 @@
<h2 style="color: #00ff00; margin: 0;"><span class="live-stream-indicator" style="font-size: 1rem;">🟢</span> LIVE STREAM</h2>
</div>
<!-- Channel Selector -->
<div class="live-stream-quality" style="margin-bottom: 15px;">
<label for="stream-channel" class="live-stream-label"><strong>Channel:</strong></label>
<select id="stream-channel" onchange="changeChannel()">
<option value="curated">🎧 <c:splice lquery="(text curated-channel-name)">Curated</c:splice></option>
<option value="shuffle">🎲 Shuffle</option>
</select>
</div>
<!-- Stream Quality Selector -->
<div class="live-stream-quality">
<div class="live-stream-quality" style="margin-bottom: 15px;">
<label for="stream-quality" class="live-stream-label" ><strong>Quality:</strong></label>
<select id="stream-quality" onchange="changeStreamQuality()">
<option value="aac">AAC 96kbps (Recommended)</option>
<option value="mp3">MP3 128kbps (Compatible)</option>
<option value="low">MP3 64kbps (Low Bandwidth)</option>
<option value="shuffle">🎲 Shuffle 96kbps</option>
</select>
</div>

View File

@ -26,14 +26,21 @@
</div>
</div>
<div class="quality-selector">
<div class="channel-selector">
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<label for="popout-stream-channel"><strong>Channel:</strong></label>
<select id="popout-stream-channel" onchange="changeChannel()">
<option value="curated">🎧 <c:splice lquery="(text curated-channel-name)">Curated</c:splice></option>
<option value="shuffle">🎲 Shuffle</option>
</select>
</div>
<div class="quality-selector">
<label for="popout-stream-quality"><strong>Quality:</strong></label>
<select id="popout-stream-quality" onchange="changeStreamQuality()">
<option value="aac">AAC 96kbps</option>
<option value="mp3">MP3 128kbps</option>
<option value="low">MP3 64kbps</option>
<option value="shuffle">🎲 Shuffle 96kbps</option>
</select>
</div>