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:
parent
55d63770d2
commit
238e880b86
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@
|
|||
(defvar *is-reconnecting* false)
|
||||
(defvar *reconnect-timeout* nil)
|
||||
|
||||
;; Stream quality configuration
|
||||
(defun get-stream-config (stream-base-url encoding)
|
||||
(let ((config (ps:create
|
||||
;; 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"
|
||||
|
|
@ -30,56 +31,129 @@
|
|||
:url (+ stream-base-url "/asteroid-low.mp3")
|
||||
:format "MP3 64kbps Stereo"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid-low.mp3")
|
||||
:shuffle (ps:create
|
||||
: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"))))
|
||||
(ps:getprop config encoding)))
|
||||
: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
|
||||
(when source-element
|
||||
(setf (ps:@ source-element src) (ps:@ config url))
|
||||
(setf (ps:@ source-element type) (ps:@ config type))
|
||||
(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))))))))
|
||||
(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
|
||||
|
|
|
|||
|
|
@ -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")))
|
||||
(let ((channel (get-current-channel))
|
||||
(quality (get-current-quality)))
|
||||
(if (= channel "shuffle")
|
||||
"asteroid-shuffle.mp3"
|
||||
(cond
|
||||
((= quality "shuffle") "asteroid-shuffle.mp3")
|
||||
((= quality "low") "asteroid-low.mp3")
|
||||
((= quality "mp3") "asteroid.mp3")
|
||||
(t "asteroid.aac"))))
|
||||
(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)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@
|
|||
;; Stream Configuration
|
||||
;; ========================================
|
||||
|
||||
;; Get stream configuration for a given quality
|
||||
(defun get-stream-config (stream-base-url encoding)
|
||||
(let ((config (ps:create
|
||||
;; 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"
|
||||
|
|
@ -25,24 +26,140 @@
|
|||
: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")
|
||||
: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"))))
|
||||
(ps:getprop config encoding)))
|
||||
: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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue