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))))
|
:song (string-trim " " (subseq title (+ pos 3))))
|
||||||
(list :artist "Unknown" :song title))))
|
(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)
|
(defun generate-music-search-url (artist song)
|
||||||
"Generate MusicBrainz search URL for artist and song"
|
"Generate MusicBrainz search URL for artist and song"
|
||||||
;; Simple search without field prefixes works better with URL encoding
|
;; Simple search without field prefixes works better with URL encoding
|
||||||
|
|
@ -767,6 +815,7 @@
|
||||||
:listeners "0"
|
:listeners "0"
|
||||||
:stream-quality "128kbps MP3"
|
:stream-quality "128kbps MP3"
|
||||||
:stream-base-url *stream-base-url*
|
: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-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"
|
:default-stream-encoding "audio/aac"
|
||||||
:default-stream-encoding-desc "AAC 96kbps Stereo"
|
:default-stream-encoding-desc "AAC 96kbps Stereo"
|
||||||
|
|
@ -793,6 +842,7 @@
|
||||||
:listeners "0"
|
:listeners "0"
|
||||||
:stream-quality "128kbps MP3"
|
:stream-quality "128kbps MP3"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
|
:curated-channel-name (get-curated-channel-name)
|
||||||
:now-playing-artist "The Void"
|
:now-playing-artist "The Void"
|
||||||
:now-playing-track "Silence"
|
:now-playing-track "Silence"
|
||||||
:now-playing-album "Startup Sounds"
|
:now-playing-album "Startup Sounds"
|
||||||
|
|
@ -806,6 +856,7 @@
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "audio-player-frame")
|
(load-template "audio-player-frame")
|
||||||
:stream-base-url *stream-base-url*
|
: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-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
|
|
@ -1209,6 +1260,7 @@
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "popout-player")
|
(load-template "popout-player")
|
||||||
:stream-base-url *stream-base-url*
|
: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-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,73 +13,147 @@
|
||||||
(defvar *is-reconnecting* false)
|
(defvar *is-reconnecting* false)
|
||||||
(defvar *reconnect-timeout* nil)
|
(defvar *reconnect-timeout* nil)
|
||||||
|
|
||||||
;; Stream quality configuration
|
;; Stream configuration by channel and quality
|
||||||
(defun get-stream-config (stream-base-url encoding)
|
;; Curated channel has multiple quality options, shuffle has only one
|
||||||
(let ((config (ps:create
|
(defun get-stream-config (stream-base-url channel quality)
|
||||||
:aac (ps:create
|
(let ((curated-config (ps:create
|
||||||
:url (+ stream-base-url "/asteroid.aac")
|
:aac (ps:create
|
||||||
:format "AAC 96kbps Stereo"
|
:url (+ stream-base-url "/asteroid.aac")
|
||||||
:type "audio/aac"
|
:format "AAC 96kbps Stereo"
|
||||||
:mount "asteroid.aac")
|
:type "audio/aac"
|
||||||
:mp3 (ps:create
|
:mount "asteroid.aac")
|
||||||
:url (+ stream-base-url "/asteroid.mp3")
|
:mp3 (ps:create
|
||||||
:format "MP3 128kbps Stereo"
|
:url (+ stream-base-url "/asteroid.mp3")
|
||||||
:type "audio/mpeg"
|
:format "MP3 128kbps Stereo"
|
||||||
:mount "asteroid.mp3")
|
:type "audio/mpeg"
|
||||||
:low (ps:create
|
:mount "asteroid.mp3")
|
||||||
:url (+ stream-base-url "/asteroid-low.mp3")
|
:low (ps:create
|
||||||
:format "MP3 64kbps Stereo"
|
:url (+ stream-base-url "/asteroid-low.mp3")
|
||||||
:type "audio/mpeg"
|
:format "MP3 64kbps Stereo"
|
||||||
:mount "asteroid-low.mp3")
|
:type "audio/mpeg"
|
||||||
:shuffle (ps:create
|
:mount "asteroid-low.mp3")))
|
||||||
:url (+ stream-base-url "/asteroid-shuffle.mp3")
|
(shuffle-config (ps:create
|
||||||
:format "Shuffle MP3 96kbps"
|
:url (+ stream-base-url "/asteroid-shuffle.mp3")
|
||||||
:type "audio/mpeg"
|
:format "Shuffle MP3 96kbps"
|
||||||
:mount "asteroid-shuffle.mp3"))))
|
:type "audio/mpeg"
|
||||||
(ps:getprop config encoding)))
|
:mount "asteroid-shuffle.mp3")))
|
||||||
|
(if (= channel "shuffle")
|
||||||
|
shuffle-config
|
||||||
|
(ps:getprop curated-config quality))))
|
||||||
|
|
||||||
;; Change stream quality
|
;; Get current channel from selector or localStorage
|
||||||
(defun change-stream-quality ()
|
(defun get-current-channel ()
|
||||||
(let* ((selector (ps:chain document (get-element-by-id "stream-quality")))
|
(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")))
|
(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)))
|
(quality (get-current-quality))
|
||||||
(audio-element (ps:chain document (get-element-by-id "live-audio")))
|
(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")))
|
(source-element (ps:chain document (get-element-by-id "audio-source")))
|
||||||
(was-playing (not (ps:@ audio-element paused)))
|
(was-playing (and audio-element (not (ps:@ audio-element paused)))))
|
||||||
(current-time (ps:@ audio-element current-time)))
|
|
||||||
|
|
||||||
;; Save preference
|
;; 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-stream-information)
|
(update-stream-information)
|
||||||
|
|
||||||
;; Update audio player
|
;; Update audio player
|
||||||
(setf (ps:@ source-element src) (ps:@ config url))
|
(when source-element
|
||||||
(setf (ps:@ source-element type) (ps:@ config type))
|
(setf (ps:@ source-element src) (ps:@ config url))
|
||||||
(ps:chain audio-element (load))
|
(setf (ps:@ source-element type) (ps:@ config type)))
|
||||||
|
(when audio-element
|
||||||
;; Resume playback if it was playing
|
(ps:chain audio-element (load))
|
||||||
(when was-playing
|
;; Resume playback if it was playing
|
||||||
(ps:chain (ps:chain audio-element (play))
|
(when was-playing
|
||||||
(catch (lambda (e)
|
(ps:chain (ps:chain audio-element (play))
|
||||||
(ps:chain console (log "Autoplay prevented:" e))))))))
|
(catch (lambda (e)
|
||||||
|
(ps:chain console (log "Autoplay prevented:" e)))))))))
|
||||||
|
|
||||||
;; Get current mount from stream quality selection
|
;; Get current mount from channel and quality selection
|
||||||
;; Checks local selector first, then sibling player-frame (for frameset mode)
|
;; Checks local selectors first, then sibling player-frame (for frameset mode)
|
||||||
(defun get-current-mount ()
|
(defun get-current-mount ()
|
||||||
(let* ((selector (ps:chain document (get-element-by-id "stream-quality")))
|
(let* ((channel (get-current-channel))
|
||||||
;; If no local selector, try to get from sibling player-frame (frameset mode)
|
(quality (get-current-quality))
|
||||||
(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"))
|
|
||||||
(stream-base-url (or (ps:chain document (get-element-by-id "stream-base-url"))
|
(stream-base-url (or (ps:chain document (get-element-by-id "stream-base-url"))
|
||||||
(when (not (= (ps:@ window parent) window))
|
(when (not (= (ps:@ window parent) window))
|
||||||
(ps:try
|
(ps:try
|
||||||
|
|
@ -88,7 +162,7 @@
|
||||||
(ps:chain player-frame document (get-element-by-id "stream-base-url"))))
|
(ps:chain player-frame document (get-element-by-id "stream-base-url"))))
|
||||||
(:catch (e) nil)))))
|
(:catch (e) nil)))))
|
||||||
(config (when stream-base-url
|
(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")))
|
(if config (ps:@ config mount) "asteroid.mp3")))
|
||||||
|
|
||||||
;; Update now playing info from API
|
;; Update now playing info from API
|
||||||
|
|
@ -109,22 +183,34 @@
|
||||||
|
|
||||||
;; Update stream information
|
;; Update stream information
|
||||||
(defun 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-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")))
|
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac")))
|
||||||
|
|
||||||
;; Update selector if needed
|
;; Update channel selector if needed
|
||||||
(when (and selector (not (= (ps:@ selector value) stream-quality)))
|
(when (and channel-selector (not (= (ps:@ channel-selector value) stream-channel)))
|
||||||
(setf (ps:@ selector value) stream-quality)
|
(setf (ps:@ channel-selector value) stream-channel))
|
||||||
(ps:chain selector (dispatch-event (ps:new (-event "change")))))
|
|
||||||
|
;; 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
|
;; Update stream info display
|
||||||
(when stream-base-url
|
(when stream-base-url
|
||||||
(let ((config (get-stream-config (ps:@ stream-base-url value) stream-quality)))
|
(let ((config (get-stream-config (ps:@ stream-base-url value) stream-channel stream-quality)))
|
||||||
(setf (ps:@ (ps:chain document (get-element-by-id "stream-url")) text-content)
|
(let ((url-el (ps:chain document (get-element-by-id "stream-url"))))
|
||||||
(ps:@ config url))
|
(when url-el
|
||||||
(setf (ps:@ (ps:chain document (get-element-by-id "stream-format")) text-content)
|
(setf (ps:@ url-el text-content) (ps:@ config url))))
|
||||||
(ps:@ config format))
|
(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\"]"))))
|
(let ((status-quality (ps:chain document (query-selector "[data-text=\"stream-quality\"]"))))
|
||||||
(when status-quality
|
(when status-quality
|
||||||
(setf (ps:@ status-quality text-content) (ps:@ config format))))))))
|
(setf (ps:@ status-quality text-content) (ps:@ config format))))))))
|
||||||
|
|
@ -219,8 +305,9 @@
|
||||||
(let* ((container (ps:chain document (get-element-by-id "audio-container")))
|
(let* ((container (ps:chain document (get-element-by-id "audio-container")))
|
||||||
(old-audio (ps:chain document (get-element-by-id "live-audio")))
|
(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-base-url (ps:chain document (get-element-by-id "stream-base-url")))
|
||||||
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))
|
(stream-channel (get-current-channel))
|
||||||
(config (get-stream-config (ps:@ stream-base-url value) stream-quality)))
|
(stream-quality (get-current-quality))
|
||||||
|
(config (get-stream-config (ps:@ stream-base-url value) stream-channel stream-quality)))
|
||||||
|
|
||||||
(when (and container old-audio)
|
(when (and container old-audio)
|
||||||
;; Reset spectrum analyzer before removing audio
|
;; Reset spectrum analyzer before removing audio
|
||||||
|
|
@ -469,21 +556,25 @@
|
||||||
;; Update now playing
|
;; Update now playing
|
||||||
(update-now-playing)
|
(update-now-playing)
|
||||||
|
|
||||||
;; Refresh now playing immediately when user switches streams
|
;; Refresh now playing immediately when user switches channel or quality
|
||||||
(let ((selector (ps:chain document (get-element-by-id "stream-quality"))))
|
(let ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel"))
|
||||||
(when (not selector)
|
(ps:chain document (get-element-by-id "popout-stream-channel"))))
|
||||||
(setf selector
|
(quality-selector (or (ps:chain document (get-element-by-id "stream-quality"))
|
||||||
(ps:try
|
(ps:chain document (get-element-by-id "popout-stream-quality")))))
|
||||||
(let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame")))
|
(when channel-selector
|
||||||
(when player-frame
|
(ps:chain channel-selector
|
||||||
(ps:chain player-frame document (get-element-by-id "stream-quality"))))
|
|
||||||
(:catch (e) nil))))
|
|
||||||
(when selector
|
|
||||||
(ps:chain selector
|
|
||||||
(add-event-listener
|
(add-event-listener
|
||||||
"change"
|
"change"
|
||||||
(lambda (_ev)
|
(lambda (_ev)
|
||||||
;; Small delay so localStorage / UI state settles
|
;; 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)))))))
|
(ps:chain window (set-timeout update-now-playing 50)))))))
|
||||||
|
|
||||||
;; Attach event listeners to audio element
|
;; Attach event listeners to audio element
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,32 @@
|
||||||
(ps:ps
|
(ps:ps
|
||||||
(progn
|
(progn
|
||||||
|
|
||||||
;; Get current mount from stream quality selection
|
;; Get current channel from selector or localStorage
|
||||||
;; Checks local selector first, then sibling player-frame (for frameset mode)
|
(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 ()
|
(defun get-current-mount-for-recently-played ()
|
||||||
(let* ((selector (ps:chain document (get-element-by-id "stream-quality")))
|
(let ((channel (get-current-channel))
|
||||||
;; If no local selector, try to get from sibling player-frame (frameset mode)
|
(quality (get-current-quality)))
|
||||||
(player-frame-selector
|
(if (= channel "shuffle")
|
||||||
(when (and (not selector)
|
"asteroid-shuffle.mp3"
|
||||||
(not (= (ps:@ window parent) window)))
|
(cond
|
||||||
(ps:try
|
((= quality "low") "asteroid-low.mp3")
|
||||||
(let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame")))
|
((= quality "mp3") "asteroid.mp3")
|
||||||
(when player-frame
|
(t "asteroid.aac")))))
|
||||||
(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"))))
|
|
||||||
|
|
||||||
;; Update recently played tracks display
|
;; Update recently played tracks display
|
||||||
(defun update-recently-played ()
|
(defun update-recently-played ()
|
||||||
|
|
@ -105,17 +109,11 @@
|
||||||
(if panel
|
(if panel
|
||||||
(progn
|
(progn
|
||||||
(update-recently-played)
|
(update-recently-played)
|
||||||
;; Refresh immediately when user switches streams
|
;; Refresh immediately when user switches channel
|
||||||
(let ((selector (ps:chain document (get-element-by-id "stream-quality"))))
|
(let ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel"))
|
||||||
(when (not selector)
|
(ps:chain document (get-element-by-id "popout-stream-channel")))))
|
||||||
(setf selector
|
(when channel-selector
|
||||||
(ps:try
|
(ps:chain channel-selector
|
||||||
(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
|
|
||||||
(add-event-listener
|
(add-event-listener
|
||||||
"change"
|
"change"
|
||||||
(lambda (_ev)
|
(lambda (_ev)
|
||||||
|
|
|
||||||
|
|
@ -11,38 +11,155 @@
|
||||||
;; Stream Configuration
|
;; Stream Configuration
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
||||||
;; Get stream configuration for a given quality
|
;; Get stream configuration for a given channel and quality
|
||||||
(defun get-stream-config (stream-base-url encoding)
|
;; Curated channel has multiple quality options, shuffle has only one
|
||||||
(let ((config (ps:create
|
(defun get-stream-config (stream-base-url channel quality)
|
||||||
:aac (ps:create :url (+ stream-base-url "/asteroid.aac")
|
(let ((curated-config (ps:create
|
||||||
:type "audio/aac"
|
:aac (ps:create :url (+ stream-base-url "/asteroid.aac")
|
||||||
:format "AAC 96kbps Stereo"
|
:type "audio/aac"
|
||||||
:mount "asteroid.aac")
|
:format "AAC 96kbps Stereo"
|
||||||
:mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3")
|
:mount "asteroid.aac")
|
||||||
:type "audio/mpeg"
|
:mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3")
|
||||||
:format "MP3 128kbps Stereo"
|
:type "audio/mpeg"
|
||||||
:mount "asteroid.mp3")
|
:format "MP3 128kbps Stereo"
|
||||||
:low (ps:create :url (+ stream-base-url "/asteroid-low.mp3")
|
:mount "asteroid.mp3")
|
||||||
:type "audio/mpeg"
|
:low (ps:create :url (+ stream-base-url "/asteroid-low.mp3")
|
||||||
:format "MP3 64kbps Stereo"
|
:type "audio/mpeg"
|
||||||
:mount "asteroid-low.mp3")
|
:format "MP3 64kbps Stereo"
|
||||||
:shuffle (ps:create :url (+ stream-base-url "/asteroid-shuffle.mp3")
|
:mount "asteroid-low.mp3")))
|
||||||
:type "audio/mpeg"
|
(shuffle-config (ps:create :url (+ stream-base-url "/asteroid-shuffle.mp3")
|
||||||
:format "Shuffle MP3 96kbps"
|
:type "audio/mpeg"
|
||||||
:mount "asteroid-shuffle.mp3"))))
|
:format "Shuffle MP3 96kbps"
|
||||||
(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
|
;; Stream Quality Selection
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
||||||
;; Change stream quality
|
;; Change stream quality (bitrate)
|
||||||
(defun change-stream-quality ()
|
(defun change-stream-quality ()
|
||||||
(let* ((selector (or (ps:chain document (get-element-by-id "stream-quality"))
|
(let* ((selector (or (ps:chain document (get-element-by-id "stream-quality"))
|
||||||
(ps:chain document (get-element-by-id "popout-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))
|
(stream-base-url (ps:@ (ps:chain document (get-element-by-id "stream-base-url")) value))
|
||||||
|
(channel (get-current-channel))
|
||||||
(selected-quality (ps:@ selector value))
|
(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"))
|
(audio-element (or (ps:chain document (get-element-by-id "persistent-audio"))
|
||||||
(ps:chain document (get-element-by-id "live-audio"))))
|
(ps:chain document (get-element-by-id "live-audio"))))
|
||||||
(source-element (ps:chain document (get-element-by-id "audio-source")))
|
(source-element (ps:chain document (get-element-by-id "audio-source")))
|
||||||
|
|
@ -73,13 +190,12 @@
|
||||||
;; Now Playing Updates
|
;; Now Playing Updates
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
||||||
;; Get current mount from stream quality selection
|
;; Get current mount from channel and quality selection
|
||||||
(defun get-current-mount ()
|
(defun get-current-mount ()
|
||||||
(let* ((selector (or (ps:chain document (get-element-by-id "stream-quality"))
|
(let* ((channel (get-current-channel))
|
||||||
(ps:chain document (get-element-by-id "popout-stream-quality"))))
|
(quality (get-current-quality))
|
||||||
(quality (if selector (ps:@ selector value) "aac"))
|
|
||||||
(stream-base-url (ps:@ (ps:chain document (get-element-by-id "stream-base-url")) value))
|
(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")))
|
(if config (ps:@ config mount) "asteroid.mp3")))
|
||||||
|
|
||||||
;; Update mini now playing display (for persistent player frame)
|
;; Update mini now playing display (for persistent player frame)
|
||||||
|
|
@ -173,8 +289,9 @@
|
||||||
(let* ((container (ps:chain document (query-selector ".persistent-player")))
|
(let* ((container (ps:chain document (query-selector ".persistent-player")))
|
||||||
(old-audio (ps:chain document (get-element-by-id "persistent-audio")))
|
(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-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"))
|
(stream-channel (get-current-channel))
|
||||||
(config (get-stream-config stream-base-url stream-quality)))
|
(stream-quality (get-current-quality))
|
||||||
|
(config (get-stream-config stream-base-url stream-channel stream-quality)))
|
||||||
|
|
||||||
(unless (and container old-audio)
|
(unless (and container old-audio)
|
||||||
(show-status "❌ Could not reconnect - reload page" true)
|
(show-status "❌ Could not reconnect - reload page" true)
|
||||||
|
|
@ -422,12 +539,22 @@
|
||||||
;; Attach event listeners
|
;; Attach event listeners
|
||||||
(attach-audio-listeners audio-element)
|
(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
|
;; 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")))
|
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac")))
|
||||||
(when (and selector (not (= (ps:@ selector value) stream-quality)))
|
(when (and quality-selector (not (= (ps:@ quality-selector value) stream-quality)))
|
||||||
(setf (ps:@ selector value) stream-quality)
|
(setf (ps:@ quality-selector value) stream-quality)))
|
||||||
(ps:chain selector (dispatch-event (ps:new (-event "change"))))))
|
|
||||||
|
;; Update quality selector state based on channel
|
||||||
|
(update-quality-selector-state)
|
||||||
|
|
||||||
;; Start now playing updates
|
;; Start now playing updates
|
||||||
(set-timeout update-mini-now-playing 1000)
|
(set-timeout update-mini-now-playing 1000)
|
||||||
|
|
@ -440,6 +567,21 @@
|
||||||
;; Attach event listeners
|
;; Attach event listeners
|
||||||
(attach-popout-listeners audio-element)
|
(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
|
;; Start now playing updates
|
||||||
(update-popout-now-playing)
|
(update-popout-now-playing)
|
||||||
(set-interval update-popout-now-playing 5000)
|
(set-interval update-popout-now-playing 5000)
|
||||||
|
|
@ -452,6 +594,8 @@
|
||||||
|
|
||||||
;; Make functions globally accessible
|
;; Make functions globally accessible
|
||||||
(setf (ps:@ window get-stream-config) get-stream-config)
|
(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 change-stream-quality) change-stream-quality)
|
||||||
(setf (ps:@ window reconnect-stream) reconnect-stream)
|
(setf (ps:@ window reconnect-stream) reconnect-stream)
|
||||||
(setf (ps:@ window disable-frameset-mode) disable-frameset-mode)
|
(setf (ps:@ window disable-frameset-mode) disable-frameset-mode)
|
||||||
|
|
|
||||||
|
|
@ -1295,6 +1295,37 @@ body.persistent-player-container .quality-selector{
|
||||||
gap: 5px;
|
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{
|
body.persistent-player-container .quality-selector label{
|
||||||
|
|
@ -1311,12 +1342,20 @@ body.persistent-player-container .quality-selector select{
|
||||||
border: 1px solid #00ff00;
|
border: 1px solid #00ff00;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.persistent-player-container .quality-selector select:hover{
|
body.persistent-player-container .quality-selector select:hover{
|
||||||
background: #2a3441;
|
background: #2a3441;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.persistent-player-container .quality-selector select:disabled{
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
body.persistent-player-container audio{
|
body.persistent-player-container audio{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
|
|
||||||
|
|
@ -1044,6 +1044,30 @@
|
||||||
:align-items "center"
|
:align-items "center"
|
||||||
:gap "5px")
|
: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
|
(.quality-selector
|
||||||
(label
|
(label
|
||||||
:color "#00ff00"
|
:color "#00ff00"
|
||||||
|
|
@ -1056,9 +1080,15 @@
|
||||||
:letter-spacing "0.08rem"
|
:letter-spacing "0.08rem"
|
||||||
:border "1px solid #00ff00"
|
:border "1px solid #00ff00"
|
||||||
:padding "3px 8px"
|
:padding "3px 8px"
|
||||||
:min-width "140px")
|
:min-width "140px"
|
||||||
|
:font-size "0.9em"
|
||||||
|
:height "26px"
|
||||||
|
:line-height "18px")
|
||||||
((:and select :hover)
|
((:and select :hover)
|
||||||
:background "#2a3441"))
|
:background "#2a3441")
|
||||||
|
((:and select :disabled)
|
||||||
|
:opacity "0.5"
|
||||||
|
:cursor "not-allowed"))
|
||||||
|
|
||||||
(audio
|
(audio
|
||||||
:flex 1
|
:flex 1
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,21 @@
|
||||||
LIVE:
|
LIVE:
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="quality-selector">
|
<div class="channel-selector">
|
||||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
<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>
|
<label for="stream-quality">Quality:</label>
|
||||||
<select id="stream-quality" onchange="changeStreamQuality()">
|
<select id="stream-quality" onchange="changeStreamQuality()">
|
||||||
<option value="aac">AAC 96k</option>
|
<option value="aac">AAC 96k</option>
|
||||||
<option value="mp3">MP3 128k</option>
|
<option value="mp3">MP3 128k</option>
|
||||||
<option value="low">MP3 64k</option>
|
<option value="low">MP3 64k</option>
|
||||||
<option value="shuffle">🎲 Shuffle 96k</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,16 @@
|
||||||
<main>
|
<main>
|
||||||
<div class="live-stream">
|
<div class="live-stream">
|
||||||
<h2 style="color: #00ff00; margin: 0;"><span class="live-stream-indicator" style="font-size: 1rem;">🟢</span> LIVE STREAM</h2>
|
<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)">
|
<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 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>
|
<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>
|
<h2 style="color: #00ff00; margin: 0;"><span class="live-stream-indicator" style="font-size: 1rem;">🟢</span> LIVE STREAM</h2>
|
||||||
</div>
|
</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 -->
|
<!-- 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>
|
<label for="stream-quality" class="live-stream-label" ><strong>Quality:</strong></label>
|
||||||
<select id="stream-quality" onchange="changeStreamQuality()">
|
<select id="stream-quality" onchange="changeStreamQuality()">
|
||||||
<option value="aac">AAC 96kbps (Recommended)</option>
|
<option value="aac">AAC 96kbps (Recommended)</option>
|
||||||
<option value="mp3">MP3 128kbps (Compatible)</option>
|
<option value="mp3">MP3 128kbps (Compatible)</option>
|
||||||
<option value="low">MP3 64kbps (Low Bandwidth)</option>
|
<option value="low">MP3 64kbps (Low Bandwidth)</option>
|
||||||
<option value="shuffle">🎲 Shuffle 96kbps</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quality-selector">
|
<div class="channel-selector">
|
||||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
<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>
|
<label for="popout-stream-quality"><strong>Quality:</strong></label>
|
||||||
<select id="popout-stream-quality" onchange="changeStreamQuality()">
|
<select id="popout-stream-quality" onchange="changeStreamQuality()">
|
||||||
<option value="aac">AAC 96kbps</option>
|
<option value="aac">AAC 96kbps</option>
|
||||||
<option value="mp3">MP3 128kbps</option>
|
<option value="mp3">MP3 128kbps</option>
|
||||||
<option value="low">MP3 64kbps</option>
|
<option value="low">MP3 64kbps</option>
|
||||||
<option value="shuffle">🎲 Shuffle 96kbps</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue