Compare commits
No commits in common. "cb76c02e63d08b5f87003d9ce3f09bf0dc84249b" and "55d63770d2d26e0a61a507cc538846c8a0ac7d4e" have entirely different histories.
cb76c02e63
...
55d63770d2
|
|
@ -78,54 +78,6 @@
|
|||
: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
|
||||
|
|
@ -429,17 +381,10 @@
|
|||
;; Copy playlist to stream-queue.m3u
|
||||
(copy-playlist-to-stream-queue playlist-path)
|
||||
;; Load into in-memory queue
|
||||
(let ((count (load-queue-from-m3u-file))
|
||||
(channel-name (get-curated-channel-name)))
|
||||
;; Skip current track to trigger crossfade to new playlist
|
||||
(handler-case
|
||||
(liquidsoap-command "stream-queue_m3u.skip")
|
||||
(error (e)
|
||||
(format *error-output* "Warning: Could not skip track: ~a~%" e)))
|
||||
(let ((count (load-queue-from-m3u-file)))
|
||||
(api-output `(("status" . "success")
|
||||
("message" . ,(format nil "Loaded playlist: ~a" name))
|
||||
("count" . ,count)
|
||||
("channel-name" . ,channel-name)
|
||||
("paths" . ,(read-m3u-file-paths stream-queue-path))))))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Playlist file not found"))
|
||||
|
|
@ -822,7 +767,6 @@
|
|||
: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"
|
||||
|
|
@ -849,7 +793,6 @@
|
|||
: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"
|
||||
|
|
@ -863,7 +806,6 @@
|
|||
(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"))
|
||||
|
||||
|
|
@ -1267,7 +1209,6 @@
|
|||
(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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -653,12 +653,7 @@
|
|||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(show-toast (+ "✓ Loaded " (ps:@ data count) " tracks from " name))
|
||||
(load-current-queue)
|
||||
;; Update channel name in all channel selectors
|
||||
;; Use bracket notation because API returns "channel-name" with hyphen
|
||||
(let ((channel-name (aref data "channel-name")))
|
||||
(when channel-name
|
||||
(update-channel-selector-name channel-name))))
|
||||
(load-current-queue))
|
||||
(alert (+ "Error loading playlist: " (or (ps:@ data message) "Unknown error")))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading playlist:" error))
|
||||
|
|
@ -710,27 +705,6 @@
|
|||
(setf html (+ html "</div>"))
|
||||
(setf (ps:@ container inner-h-t-m-l) html))))))
|
||||
|
||||
;; Update channel selector name in UI after loading a new playlist
|
||||
(defun update-channel-selector-name (channel-name)
|
||||
"Update the curated channel option text in all channel selectors"
|
||||
;; Store in localStorage so popout player can pick it up
|
||||
(ps:chain local-storage (set-item "curated-channel-name" channel-name))
|
||||
|
||||
;; Update in current document
|
||||
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))))
|
||||
(when channel-selector
|
||||
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
|
||||
(when curated-option
|
||||
(setf (ps:@ curated-option text-content) (+ "🎧 " channel-name))))))
|
||||
|
||||
;; Use postMessage to notify all frames about the channel name change
|
||||
(when (and (ps:@ window top)
|
||||
(not (= (ps:@ window top) window)))
|
||||
;; Post to top window which will relay to all frames
|
||||
(ps:chain window top (post-message
|
||||
(ps:create :type "channel-name-update" :channel-name channel-name)
|
||||
"*"))))
|
||||
|
||||
;; Save current queue to stream-queue.m3u
|
||||
(defun save-stream-queue ()
|
||||
(ps:chain
|
||||
|
|
@ -1009,9 +983,9 @@
|
|||
|
||||
;; Toggle city display for a country
|
||||
(defun toggle-country-cities (country)
|
||||
(let* ((city-row (ps:chain document (get-element-by-id (+ "cities-" country))))
|
||||
(country-row (ps:chain document (query-selector (+ "tr[data-country=\"" country "\"]"))))
|
||||
(arrow (when country-row (ps:chain country-row (query-selector ".expand-arrow")))))
|
||||
(let ((city-row (ps:chain document (get-element-by-id (+ "cities-" country))))
|
||||
(country-row (ps:chain document (query-selector (+ "tr[data-country=\"" country "\"]"))))
|
||||
(arrow (when country-row (ps:chain country-row (query-selector ".expand-arrow")))))
|
||||
|
||||
(if (ps:chain *expanded-countries* (has country))
|
||||
(progn
|
||||
|
|
|
|||
|
|
@ -13,147 +13,73 @@
|
|||
(defvar *is-reconnecting* false)
|
||||
(defvar *reconnect-timeout* nil)
|
||||
|
||||
;; Stream configuration by channel and quality
|
||||
;; Curated channel has multiple quality options, shuffle has only one
|
||||
(defun get-stream-config (stream-base-url channel quality)
|
||||
(let ((curated-config (ps:create
|
||||
:aac (ps:create
|
||||
:url (+ stream-base-url "/asteroid.aac")
|
||||
:format "AAC 96kbps Stereo"
|
||||
:type "audio/aac"
|
||||
:mount "asteroid.aac")
|
||||
:mp3 (ps:create
|
||||
:url (+ stream-base-url "/asteroid.mp3")
|
||||
:format "MP3 128kbps Stereo"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid.mp3")
|
||||
:low (ps:create
|
||||
:url (+ stream-base-url "/asteroid-low.mp3")
|
||||
:format "MP3 64kbps Stereo"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid-low.mp3")))
|
||||
(shuffle-config (ps:create
|
||||
:url (+ stream-base-url "/asteroid-shuffle.mp3")
|
||||
:format "Shuffle MP3 96kbps"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid-shuffle.mp3")))
|
||||
(if (= channel "shuffle")
|
||||
shuffle-config
|
||||
(ps:getprop curated-config quality))))
|
||||
;; Stream quality configuration
|
||||
(defun get-stream-config (stream-base-url encoding)
|
||||
(let ((config (ps:create
|
||||
:aac (ps:create
|
||||
:url (+ stream-base-url "/asteroid.aac")
|
||||
:format "AAC 96kbps Stereo"
|
||||
:type "audio/aac"
|
||||
:mount "asteroid.aac")
|
||||
:mp3 (ps:create
|
||||
:url (+ stream-base-url "/asteroid.mp3")
|
||||
:format "MP3 128kbps Stereo"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid.mp3")
|
||||
:low (ps:create
|
||||
:url (+ stream-base-url "/asteroid-low.mp3")
|
||||
:format "MP3 64kbps Stereo"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid-low.mp3")
|
||||
:shuffle (ps:create
|
||||
:url (+ stream-base-url "/asteroid-shuffle.mp3")
|
||||
:format "Shuffle MP3 96kbps"
|
||||
:type "audio/mpeg"
|
||||
:mount "asteroid-shuffle.mp3"))))
|
||||
(ps:getprop config encoding)))
|
||||
|
||||
;; 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")))
|
||||
(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 (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)
|
||||
|
||||
;; 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)
|
||||
;; Change stream quality
|
||||
(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"))))
|
||||
(let* ((selector (ps:chain document (get-element-by-id "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"))))
|
||||
(config (get-stream-config (ps:@ stream-base-url value) (ps:@ selector value)))
|
||||
(audio-element (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)))))
|
||||
(was-playing (not (ps:@ audio-element paused)))
|
||||
(current-time (ps:@ audio-element current-time)))
|
||||
|
||||
;; Save preference
|
||||
(ps:chain local-storage (set-item "stream-quality" quality))
|
||||
(ps:chain local-storage (set-item "stream-quality" (ps:@ selector value)))
|
||||
|
||||
;; 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)))
|
||||
(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)))))))))
|
||||
(setf (ps:@ source-element src) (ps:@ config url))
|
||||
(setf (ps:@ source-element type) (ps:@ config type))
|
||||
(ps:chain audio-element (load))
|
||||
|
||||
;; Resume playback if it was playing
|
||||
(when was-playing
|
||||
(ps:chain (ps:chain audio-element (play))
|
||||
(catch (lambda (e)
|
||||
(ps:chain console (log "Autoplay prevented:" e))))))))
|
||||
|
||||
;; Get current mount from channel and quality selection
|
||||
;; Checks local selectors first, then sibling player-frame (for frameset mode)
|
||||
;; Get current mount from stream quality selection
|
||||
;; Checks local selector first, then sibling player-frame (for frameset mode)
|
||||
(defun get-current-mount ()
|
||||
(let* ((channel (get-current-channel))
|
||||
(quality (get-current-quality))
|
||||
(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"))
|
||||
(stream-base-url (or (ps:chain document (get-element-by-id "stream-base-url"))
|
||||
(when (not (= (ps:@ window parent) window))
|
||||
(ps:try
|
||||
|
|
@ -162,7 +88,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) channel quality))))
|
||||
(get-stream-config (ps:@ stream-base-url value) quality))))
|
||||
(if config (ps:@ config mount) "asteroid.mp3")))
|
||||
|
||||
;; Update now playing info from API
|
||||
|
|
@ -183,34 +109,22 @@
|
|||
|
||||
;; Update stream information
|
||||
(defun update-stream-information ()
|
||||
(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"))))
|
||||
(let* ((selector (ps:chain document (get-element-by-id "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 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 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 stream info display
|
||||
(when stream-base-url
|
||||
(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 ((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 ((status-quality (ps:chain document (query-selector "[data-text=\"stream-quality\"]"))))
|
||||
(when status-quality
|
||||
(setf (ps:@ status-quality text-content) (ps:@ config format))))))))
|
||||
|
|
@ -305,9 +219,8 @@
|
|||
(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-channel (get-current-channel))
|
||||
(stream-quality (get-current-quality))
|
||||
(config (get-stream-config (ps:@ stream-base-url value) stream-channel stream-quality)))
|
||||
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))
|
||||
(config (get-stream-config (ps:@ stream-base-url value) stream-quality)))
|
||||
|
||||
(when (and container old-audio)
|
||||
;; Reset spectrum analyzer before removing audio
|
||||
|
|
@ -556,25 +469,21 @@
|
|||
;; Update now playing
|
||||
(update-now-playing)
|
||||
|
||||
;; 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
|
||||
;; 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
|
||||
(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,32 +7,28 @@
|
|||
(ps:ps
|
||||
(progn
|
||||
|
||||
;; 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
|
||||
;; Get current mount from stream quality selection
|
||||
;; Checks local selector first, then sibling player-frame (for frameset mode)
|
||||
(defun get-current-mount-for-recently-played ()
|
||||
(let ((channel (get-current-channel))
|
||||
(quality (get-current-quality)))
|
||||
(if (= channel "shuffle")
|
||||
"asteroid-shuffle.mp3"
|
||||
(cond
|
||||
((= quality "low") "asteroid-low.mp3")
|
||||
((= quality "mp3") "asteroid.mp3")
|
||||
(t "asteroid.aac")))))
|
||||
(let* ((selector (ps:chain document (get-element-by-id "stream-quality")))
|
||||
;; If no local selector, try to get from sibling player-frame (frameset mode)
|
||||
(player-frame-selector
|
||||
(when (and (not selector)
|
||||
(not (= (ps:@ window parent) window)))
|
||||
(ps:try
|
||||
(let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame")))
|
||||
(when player-frame
|
||||
(ps:chain player-frame document (get-element-by-id "stream-quality"))))
|
||||
(:catch (e) nil))))
|
||||
(effective-selector (or selector player-frame-selector))
|
||||
(quality (or (when effective-selector (ps:@ effective-selector value))
|
||||
(ps:chain local-storage (get-item "stream-quality"))
|
||||
"aac")))
|
||||
(cond
|
||||
((= quality "shuffle") "asteroid-shuffle.mp3")
|
||||
((= quality "low") "asteroid-low.mp3")
|
||||
((= quality "mp3") "asteroid.mp3")
|
||||
(t "asteroid.aac"))))
|
||||
|
||||
;; Update recently played tracks display
|
||||
(defun update-recently-played ()
|
||||
|
|
@ -109,11 +105,17 @@
|
|||
(if panel
|
||||
(progn
|
||||
(update-recently-played)
|
||||
;; 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
|
||||
;; 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
|
||||
(add-event-listener
|
||||
"change"
|
||||
(lambda (_ev)
|
||||
|
|
|
|||
|
|
@ -11,155 +11,38 @@
|
|||
;; Stream Configuration
|
||||
;; ========================================
|
||||
|
||||
;; Get stream configuration for a given channel and quality
|
||||
;; Curated channel has multiple quality options, shuffle has only one
|
||||
(defun get-stream-config (stream-base-url channel quality)
|
||||
(let ((curated-config (ps:create
|
||||
:aac (ps:create :url (+ stream-base-url "/asteroid.aac")
|
||||
:type "audio/aac"
|
||||
:format "AAC 96kbps Stereo"
|
||||
:mount "asteroid.aac")
|
||||
:mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3")
|
||||
:type "audio/mpeg"
|
||||
:format "MP3 128kbps Stereo"
|
||||
:mount "asteroid.mp3")
|
||||
:low (ps:create :url (+ stream-base-url "/asteroid-low.mp3")
|
||||
:type "audio/mpeg"
|
||||
:format "MP3 64kbps Stereo"
|
||||
:mount "asteroid-low.mp3")))
|
||||
(shuffle-config (ps:create :url (+ stream-base-url "/asteroid-shuffle.mp3")
|
||||
:type "audio/mpeg"
|
||||
:format "Shuffle MP3 96kbps"
|
||||
:mount "asteroid-shuffle.mp3")))
|
||||
(if (= channel "shuffle")
|
||||
shuffle-config
|
||||
(ps:getprop curated-config quality))))
|
||||
|
||||
;; Get current channel from selector or localStorage
|
||||
(defun get-current-channel ()
|
||||
(let ((selector (or (ps:chain document (get-element-by-id "stream-channel"))
|
||||
(ps:chain document (get-element-by-id "popout-stream-channel")))))
|
||||
(if selector
|
||||
(ps:@ selector value)
|
||||
(or (ps:chain local-storage (get-item "stream-channel")) "curated"))))
|
||||
|
||||
;; Get current quality from selector or localStorage
|
||||
(defun get-current-quality ()
|
||||
(let ((selector (or (ps:chain document (get-element-by-id "stream-quality"))
|
||||
(ps:chain document (get-element-by-id "popout-stream-quality")))))
|
||||
(if selector
|
||||
(ps:@ selector value)
|
||||
(or (ps:chain local-storage (get-item "stream-quality")) "aac"))))
|
||||
|
||||
;; Update quality selector state based on channel
|
||||
(defun update-quality-selector-state ()
|
||||
(let* ((channel (get-current-channel))
|
||||
(quality-selector (or (ps:chain document (get-element-by-id "stream-quality"))
|
||||
(ps:chain document (get-element-by-id "popout-stream-quality")))))
|
||||
(when quality-selector
|
||||
(if (= channel "shuffle")
|
||||
(progn
|
||||
(setf (ps:@ quality-selector disabled) t)
|
||||
(setf (ps:@ quality-selector title) "Shuffle channel has fixed quality"))
|
||||
(progn
|
||||
(setf (ps:@ quality-selector disabled) nil)
|
||||
(setf (ps:@ quality-selector title) ""))))))
|
||||
|
||||
;; Change channel (curated vs shuffle)
|
||||
;; Called from content frame or popout - updates localStorage and notifies frame player
|
||||
(defun change-channel ()
|
||||
(let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel"))
|
||||
(ps:chain document (get-element-by-id "popout-stream-channel"))))
|
||||
(channel (ps:@ channel-selector value))
|
||||
(stream-base-url-el (ps:chain document (get-element-by-id "stream-base-url")))
|
||||
(stream-base-url (when stream-base-url-el (ps:@ stream-base-url-el value)))
|
||||
(quality (get-current-quality))
|
||||
(audio-element (or (ps:chain document (get-element-by-id "persistent-audio"))
|
||||
(ps:chain document (get-element-by-id "live-audio"))))
|
||||
(source-element (ps:chain document (get-element-by-id "audio-source")))
|
||||
(was-playing (and audio-element (not (ps:@ audio-element paused)))))
|
||||
|
||||
;; Save preference
|
||||
(ps:chain local-storage (set-item "stream-channel" channel))
|
||||
|
||||
;; Update quality selector state
|
||||
(update-quality-selector-state)
|
||||
|
||||
;; If we have audio element (popout player), update it
|
||||
(when (and stream-base-url audio-element source-element)
|
||||
(let ((config (get-stream-config stream-base-url channel quality)))
|
||||
;; Swap source and reload
|
||||
(setf (ps:@ source-element src) (ps:@ config url))
|
||||
(setf (ps:@ source-element type) (ps:@ config type))
|
||||
(ps:chain audio-element (load))
|
||||
|
||||
;; Resume playback if it was playing
|
||||
(when was-playing
|
||||
(ps:chain audio-element (play)
|
||||
(catch (lambda (e)
|
||||
(ps:chain console (log "Autoplay prevented:" e))))))))
|
||||
|
||||
;; If in frameset mode, notify the player frame to update
|
||||
(when (not (= (ps:@ window parent) window))
|
||||
(ps:try
|
||||
(let ((player-frame (ps:@ (ps:@ window parent) frames "player-frame")))
|
||||
(when (and player-frame (ps:@ player-frame sync-channel-from-storage))
|
||||
(ps:chain player-frame (sync-channel-from-storage))))
|
||||
(:catch (e) nil)))
|
||||
|
||||
;; Refresh now-playing immediately
|
||||
(when (ps:chain document (get-element-by-id "mini-now-playing"))
|
||||
(ps:chain window (set-timeout update-mini-now-playing 50)))
|
||||
(when (or (ps:chain document (get-element-by-id "popout-track-title"))
|
||||
(ps:chain document (get-element-by-id "popout-track-artist")))
|
||||
(ps:chain window (set-timeout update-popout-now-playing 50)))))
|
||||
|
||||
;; Sync channel from localStorage (called by content frame when channel changes)
|
||||
(defun sync-channel-from-storage ()
|
||||
(let* ((channel (or (ps:chain local-storage (get-item "stream-channel")) "curated"))
|
||||
(quality (get-current-quality))
|
||||
(stream-base-url-el (ps:chain document (get-element-by-id "stream-base-url")))
|
||||
(stream-base-url (when stream-base-url-el (ps:@ stream-base-url-el value)))
|
||||
(channel-selector (ps:chain document (get-element-by-id "stream-channel")))
|
||||
(audio-element (ps:chain document (get-element-by-id "persistent-audio")))
|
||||
(source-element (ps:chain document (get-element-by-id "audio-source")))
|
||||
(was-playing (and audio-element (not (ps:@ audio-element paused)))))
|
||||
|
||||
;; Update channel selector dropdown to match localStorage
|
||||
(when (and channel-selector (not (= (ps:@ channel-selector value) channel)))
|
||||
(setf (ps:@ channel-selector value) channel))
|
||||
|
||||
(when (and stream-base-url audio-element source-element)
|
||||
(let ((config (get-stream-config stream-base-url channel quality)))
|
||||
;; Update quality selector state
|
||||
(update-quality-selector-state)
|
||||
|
||||
;; Swap source and reload
|
||||
(setf (ps:@ source-element src) (ps:@ config url))
|
||||
(setf (ps:@ source-element type) (ps:@ config type))
|
||||
(ps:chain audio-element (load))
|
||||
|
||||
;; Resume playback if it was playing
|
||||
(when was-playing
|
||||
(ps:chain audio-element (play)
|
||||
(catch (lambda (e)
|
||||
(ps:chain console (log "Autoplay prevented:" e))))))
|
||||
|
||||
;; Refresh now-playing
|
||||
(ps:chain window (set-timeout update-mini-now-playing 50))))))
|
||||
;; Get stream configuration for a given quality
|
||||
(defun get-stream-config (stream-base-url encoding)
|
||||
(let ((config (ps:create
|
||||
:aac (ps:create :url (+ stream-base-url "/asteroid.aac")
|
||||
:type "audio/aac"
|
||||
:format "AAC 96kbps Stereo"
|
||||
:mount "asteroid.aac")
|
||||
:mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3")
|
||||
:type "audio/mpeg"
|
||||
:format "MP3 128kbps Stereo"
|
||||
:mount "asteroid.mp3")
|
||||
:low (ps:create :url (+ stream-base-url "/asteroid-low.mp3")
|
||||
:type "audio/mpeg"
|
||||
:format "MP3 64kbps Stereo"
|
||||
:mount "asteroid-low.mp3")
|
||||
:shuffle (ps:create :url (+ stream-base-url "/asteroid-shuffle.mp3")
|
||||
:type "audio/mpeg"
|
||||
:format "Shuffle MP3 96kbps"
|
||||
:mount "asteroid-shuffle.mp3"))))
|
||||
(ps:getprop config encoding)))
|
||||
|
||||
;; ========================================
|
||||
;; Stream Quality Selection
|
||||
;; ========================================
|
||||
|
||||
;; Change stream quality (bitrate)
|
||||
;; Change stream quality
|
||||
(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 channel selected-quality))
|
||||
(config (get-stream-config stream-base-url 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")))
|
||||
|
|
@ -190,12 +73,13 @@
|
|||
;; Now Playing Updates
|
||||
;; ========================================
|
||||
|
||||
;; Get current mount from channel and quality selection
|
||||
;; Get current mount from stream quality selection
|
||||
(defun get-current-mount ()
|
||||
(let* ((channel (get-current-channel))
|
||||
(quality (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"))))
|
||||
(quality (if selector (ps:@ selector value) "aac"))
|
||||
(stream-base-url (ps:@ (ps:chain document (get-element-by-id "stream-base-url")) value))
|
||||
(config (get-stream-config stream-base-url channel quality)))
|
||||
(config (get-stream-config stream-base-url quality)))
|
||||
(if config (ps:@ config mount) "asteroid.mp3")))
|
||||
|
||||
;; Update mini now playing display (for persistent player frame)
|
||||
|
|
@ -289,9 +173,8 @@
|
|||
(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-channel (get-current-channel))
|
||||
(stream-quality (get-current-quality))
|
||||
(config (get-stream-config stream-base-url stream-channel stream-quality)))
|
||||
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))
|
||||
(config (get-stream-config stream-base-url stream-quality)))
|
||||
|
||||
(unless (and container old-audio)
|
||||
(show-status "❌ Could not reconnect - reload page" true)
|
||||
|
|
@ -539,37 +422,12 @@
|
|||
;; 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 ((quality-selector (ps:chain document (get-element-by-id "stream-quality")))
|
||||
(let ((selector (ps:chain document (get-element-by-id "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)
|
||||
|
||||
;; Check for channel name changes from localStorage periodically
|
||||
(let ((last-channel-name (ps:chain local-storage (get-item "curated-channel-name"))))
|
||||
(set-interval
|
||||
(lambda ()
|
||||
(let ((current-channel-name (ps:chain local-storage (get-item "curated-channel-name"))))
|
||||
(when (and current-channel-name
|
||||
(not (= current-channel-name last-channel-name)))
|
||||
(setf last-channel-name current-channel-name)
|
||||
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))))
|
||||
(when channel-selector
|
||||
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
|
||||
(when curated-option
|
||||
(setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name)))))))))
|
||||
2000))
|
||||
(when (and selector (not (= (ps:@ selector value) stream-quality)))
|
||||
(setf (ps:@ selector value) stream-quality)
|
||||
(ps:chain selector (dispatch-event (ps:new (-event "change"))))))
|
||||
|
||||
;; Start now playing updates
|
||||
(set-timeout update-mini-now-playing 1000)
|
||||
|
|
@ -582,31 +440,6 @@
|
|||
;; 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)
|
||||
|
||||
;; Listen for channel name changes from localStorage
|
||||
(ps:chain window (add-event-listener "storage"
|
||||
(lambda (e)
|
||||
(when (= (ps:@ e key) "curated-channel-name")
|
||||
(let ((channel-selector (ps:chain document (get-element-by-id "popout-stream-channel"))))
|
||||
(when channel-selector
|
||||
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
|
||||
(when curated-option
|
||||
(setf (ps:@ curated-option text-content) (+ "🎧 " (ps:@ e new-value)))))))))))
|
||||
|
||||
;; Start now playing updates
|
||||
(update-popout-now-playing)
|
||||
(set-interval update-popout-now-playing 5000)
|
||||
|
|
@ -619,8 +452,6 @@
|
|||
|
||||
;; 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)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
#EXTM3U
|
||||
#PLAYLIST:Low Orbit
|
||||
#PHASE:Low Orbit
|
||||
#DURATION:12 hours (approx)
|
||||
#PLAYLIST:Asteroid Low Orbit - Ambient Electronic Journey
|
||||
#CURATOR:Asteroid Radio
|
||||
#DESCRIPTION:A 12-hour voyage through ambient, IDM, and space music
|
||||
|
||||
#EXTINF:-1,Brian Eno - Emerald And Lime
|
||||
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A1 Emerald And Lime.flac
|
||||
|
|
|
|||
|
|
@ -1295,37 +1295,6 @@ 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{
|
||||
|
|
@ -1342,20 +1311,12 @@ 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,52 +1044,22 @@
|
|||
:align-items "center"
|
||||
:gap "5px")
|
||||
|
||||
(.channel-selector
|
||||
:display "flex"
|
||||
:align-items "center"
|
||||
:gap "5px")
|
||||
|
||||
(.channel-selector
|
||||
(.quality-selector
|
||||
(label
|
||||
:color "#00ff00"
|
||||
:font-size "0.9em"))
|
||||
|
||||
(.channel-selector
|
||||
(.quality-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")
|
||||
:min-width "140px")
|
||||
((:and select :hover)
|
||||
:background "#2a3441"))
|
||||
|
||||
(.quality-selector
|
||||
(label
|
||||
:color "#00ff00"
|
||||
:font-size "0.9em"))
|
||||
|
||||
(.quality-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")
|
||||
((:and select :disabled)
|
||||
:opacity "0.5"
|
||||
:cursor "not-allowed"))
|
||||
|
||||
(audio
|
||||
:flex 1
|
||||
:min-width "200px"
|
||||
|
|
|
|||
|
|
@ -13,21 +13,14 @@
|
|||
LIVE:
|
||||
</span>
|
||||
|
||||
<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">
|
||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
||||
<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,16 +66,6 @@
|
|||
<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,22 +69,14 @@
|
|||
<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" style="margin-bottom: 15px;">
|
||||
<div class="live-stream-quality">
|
||||
<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,21 +26,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
||||
<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