From 8700724f8120e16ac9cac11ed34b1590892e621a Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Fri, 20 Feb 2026 17:26:14 +0000 Subject: [PATCH] feat: add media session API on now-playing update --- asteroid.asd | 3 +- parenscript/front-page.lisp | 37 +- parenscript/player.lisp | 1578 ++++++++++++++++---------------- parenscript/stream-player.lisp | 8 +- static/asteroid-squared.png | Bin 0 -> 44369 bytes 5 files changed, 832 insertions(+), 794 deletions(-) create mode 100644 static/asteroid-squared.png diff --git a/asteroid.asd b/asteroid.asd index 117038b..c01fb35 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -49,7 +49,8 @@ (:file "template-utils") (:file "parenscript-utils") (:module :parenscript - :components ((:file "recently-played") + :components ((:file "parenscript-utils") + (:file "recently-played") (:file "auth-ui") (:file "front-page") (:file "profile") diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index fe7c873..1a72015 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -222,9 +222,9 @@ (catch (lambda (error) ;; Silently fail nil))))) - + ;; Update now playing info from API - (defun update-now-playing () + (defun update-now-playing() (let ((mount (get-current-mount))) (ps:chain (fetch (+ "/api/asteroid/partial/now-playing?mount=" mount)) @@ -250,19 +250,25 @@ ;; Check if this track is in user's favorites (check-favorite-status) ;; Update favorite count display - (let ((count-el (ps:chain document (get-element-by-id "favorite-count-display"))) - (count-val-el (ps:chain document (get-element-by-id "favorite-count-value")))) - (when (and count-el count-val-el) - (let ((fav-count (parse-int (or (ps:@ count-val-el value) "0") 10))) - (if (> fav-count 0) - (setf (ps:@ count-el text-content) - (if (= fav-count 1) - "1 person loves this track ❤️" - (+ fav-count " people love this track ❤️"))) - (setf (ps:@ count-el text-content) ""))))))))))))) + (update-favorite-information) + (update-media-session new-title))))))))) (catch (lambda (error) (ps:chain console (log "Could not fetch stream status:" error))))))) - + + ;; Update favorite count display + (defun update-favorite-information () + (let ((count-el (ps:chain document (get-element-by-id "favorite-count-display"))) + (count-val-el (ps:chain document (get-element-by-id "favorite-count-value")))) + (when (and count-el count-val-el) + (let ((fav-count (parse-int (or (ps:@ count-val-el value) "0") 10))) + (if (> fav-count 0) + (setf (ps:@ count-el text-content) + (if (= fav-count 1) + "1 person loves this track ❤️" + (+ fav-count " people love this track ❤️"))) + (setf (ps:@ count-el text-content) "")))))) + + ;; Update stream information (defun update-stream-information () (let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel")) @@ -635,6 +641,7 @@ ;; Load user's favorites for highlight feature (load-favorites-cache) + (update-favorite-information) ;; Update now playing (update-now-playing) @@ -864,4 +871,6 @@ (defun generate-front-page-js () "Return the pre-compiled JavaScript for front page" - *front-page-js*) + (ps-join + *common-player-js* + *front-page-js*)) diff --git a/parenscript/player.lisp b/parenscript/player.lisp index 11cd87e..325423b 100644 --- a/parenscript/player.lisp +++ b/parenscript/player.lisp @@ -3,811 +3,835 @@ (in-package #:asteroid) -(defparameter *player-js* +(defparameter *common-player-js* (ps:ps* '(progn - - ;; Global variables - (defvar *tracks* (array)) - (defvar *current-track* nil) - (defvar *current-track-index* -1) - (defvar *play-queue* (array)) - (defvar *is-shuffled* nil) - (defvar *is-repeating* nil) - (defvar *audio-player* nil) - - ;; Pagination variables for track library - (defvar *library-current-page* 1) - (defvar *library-tracks-per-page* 20) - (defvar *filtered-library-tracks* (array)) - - ;; Initialize player on page load - (ps:chain document - (add-event-listener - "DOMContentLoaded" - (lambda () - (setf *audio-player* (ps:chain document (get-element-by-id "audio-player"))) - (redirect-when-frame) - (load-tracks) - (load-playlists) - (setup-event-listeners) - (update-player-display) - (update-volume) - - ;; Setup live stream with reduced buffering and reconnect logic - (let ((live-audio (ps:chain document (get-element-by-id "live-stream-audio")))) - (when live-audio - ;; Reduce buffer to minimize delay - (setf (ps:@ live-audio preload) "none") - - ;; Add reconnect logic for long pauses - (let ((pause-timestamp nil) - (is-reconnecting false) - (needs-reconnect false) - (pause-reconnect-threshold 10000)) - - (ps:chain live-audio - (add-event-listener "pause" - (lambda () - (setf pause-timestamp (ps:chain |Date| (now))) - (ps:chain console (log "Live stream paused at:" pause-timestamp))))) - - (ps:chain live-audio - (add-event-listener "play" - (lambda () - (when (and (not is-reconnecting) - pause-timestamp - (> (- (ps:chain |Date| (now)) pause-timestamp) pause-reconnect-threshold)) - (setf needs-reconnect true) - (ps:chain console (log "Long pause detected, will reconnect when playing starts..."))) - (setf pause-timestamp nil)))) - - (ps:chain live-audio - (add-event-listener "playing" - (lambda () - (when (and needs-reconnect (not is-reconnecting)) - (setf is-reconnecting true) - (setf needs-reconnect false) - (ps:chain console (log "Reconnecting live stream after long pause to clear stale buffers...")) - - (ps:chain live-audio (pause)) - - (when (ps:@ window |resetSpectrumAnalyzer|) - (ps:chain window (reset-spectrum-analyzer))) - - (ps:chain live-audio (load)) - - (set-timeout - (lambda () - (ps:chain live-audio (play) - (catch (lambda (err) - (ps:chain console (log "Reconnect play failed:" err))))) - - (when (ps:@ window |initSpectrumAnalyzer|) - (ps:chain window (init-spectrum-analyzer)) - (ps:chain console (log "Spectrum analyzer reinitialized after reconnect"))) - - (setf is-reconnecting false)) - 200))))) - ))) - - ;; Restore user quality preference - (let ((selector (ps:chain document (get-element-by-id "live-stream-quality"))) - (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))) - (when (and selector (not (= (ps:@ selector value) stream-quality))) - (setf (ps:@ selector value) stream-quality) - (ps:chain selector (dispatch-event (ps:new (-Event "change"))))))))) - - ;; Frame redirection logic - (defun redirect-when-frame () - (let* ((path (ps:@ window location pathname)) - (is-frameset-page (not (= (ps:@ window parent) (ps:@ window self)))) - (is-content-frame (ps:chain path (includes "player-content")))) - - (when (and is-frameset-page (not is-content-frame)) - (setf (ps:@ window location href) "/asteroid/player-content")) - - (when (and (not is-frameset-page) is-content-frame) - (setf (ps:@ window location href) "/asteroid/player")))) - - ;; Setup all event listeners - (defun setup-event-listeners () - ;; Search - (ps:chain (ps:chain document (get-element-by-id "search-tracks")) - (add-event-listener "input" filter-tracks)) - - ;; Player controls - (ps:chain (ps:chain document (get-element-by-id "play-pause-btn")) - (add-event-listener "click" toggle-play-pause)) - (ps:chain (ps:chain document (get-element-by-id "prev-btn")) - (add-event-listener "click" play-previous)) - (ps:chain (ps:chain document (get-element-by-id "next-btn")) - (add-event-listener "click" play-next)) - (ps:chain (ps:chain document (get-element-by-id "shuffle-btn")) - (add-event-listener "click" toggle-shuffle)) - (ps:chain (ps:chain document (get-element-by-id "repeat-btn")) - (add-event-listener "click" toggle-repeat)) - - ;; Volume control - (ps:chain (ps:chain document (get-element-by-id "volume-slider")) - (add-event-listener "input" update-volume)) - - ;; Audio player events - (when (and *audio-player* (ps:chain *audio-player* add-event-listener)) - (ps:chain *audio-player* (add-event-listener "loadedmetadata" update-time-display)) - (ps:chain *audio-player* (add-event-listener "timeupdate" update-time-display)) - (ps:chain *audio-player* (add-event-listener "ended" handle-track-end)) - (ps:chain *audio-player* (add-event-listener "play" (lambda () (update-play-button "⏸️ Pause")))) - (ps:chain *audio-player* (add-event-listener "pause" (lambda () (update-play-button "▶️ Play"))))) - - ;; Playlist controls - (ps:chain (ps:chain document (get-element-by-id "create-playlist")) - (add-event-listener "click" create-playlist)) - (ps:chain (ps:chain document (get-element-by-id "clear-queue")) - (add-event-listener "click" clear-queue)) - (ps:chain (ps:chain document (get-element-by-id "save-queue")) - (add-event-listener "click" save-queue-as-playlist))) - - ;; Load tracks from API - (defun load-tracks () - (ps:chain - (ps:chain (fetch "/api/asteroid/tracks")) - (then (lambda (response) - (if (ps:@ response ok) - (ps:chain response (json)) - (progn - (ps:chain console (error (+ "HTTP " (ps:@ response status)))) - (ps:create :status "error" :tracks (array)))))) - (then (lambda (result) - ;; Handle RADIANCE API wrapper format - (let ((data (or (ps:@ result data) result))) - (if (= (ps:@ data status) "success") + (defun update-media-session (title) + (let ((media-session (ps:@ navigator media-session))) + (when media-session + (let ((track-title "Unknown") + (now-playing-title-el (ps:chain document (query-selector "#current-track-title")))) + (when title + (setf track-title title)) + (when (and now-playing-title-el (not title)) + (let ((now-playing-title (ps:@ now-playing-title-el text-content))) + (when now-playing-title + (setf track-title now-playing-title)))) + (let* ((media-info (ps:create :title track-title + :artwork (list (ps:create :src "/asteroid/static/asteroid-squared.png" + :type "image/png" + :sizes "256x256")))) + (metadata (ps:new (-media-metadata media-info)))) + (setf (ps:@ media-session metadata) metadata))))))))) + +(defparameter *player-js* + (ps:ps* + `(progn + + ;; Global variables + (defvar *tracks* (array)) + (defvar *current-track* nil) + (defvar *current-track-index* -1) + (defvar *play-queue* (array)) + (defvar *is-shuffled* nil) + (defvar *is-repeating* nil) + (defvar *audio-player* nil) + + ;; Pagination variables for track library + (defvar *library-current-page* 1) + (defvar *library-tracks-per-page* 20) + (defvar *filtered-library-tracks* (array)) + + ;; Initialize player on page load + (ps:chain document + (add-event-listener + "DOMContentLoaded" + (lambda () + (setf *audio-player* (ps:chain document (get-element-by-id "audio-player"))) + (redirect-when-frame) + (load-tracks) + (load-playlists) + (setup-event-listeners) + (update-player-display) + (update-volume) + + ;; Setup live stream with reduced buffering and reconnect logic + (let ((live-audio (ps:chain document (get-element-by-id "live-stream-audio")))) + (when live-audio + ;; Reduce buffer to minimize delay + (setf (ps:@ live-audio preload) "none") + + ;; Add reconnect logic for long pauses + (let ((pause-timestamp nil) + (is-reconnecting false) + (needs-reconnect false) + (pause-reconnect-threshold 10000)) + + (ps:chain live-audio + (add-event-listener "pause" + (lambda () + (setf pause-timestamp (ps:chain |Date| (now))) + (ps:chain console (log "Live stream paused at:" pause-timestamp))))) + + (ps:chain live-audio + (add-event-listener "play" + (lambda () + (when (and (not is-reconnecting) + pause-timestamp + (> (- (ps:chain |Date| (now)) pause-timestamp) pause-reconnect-threshold)) + (setf needs-reconnect true) + (ps:chain console (log "Long pause detected, will reconnect when playing starts..."))) + (setf pause-timestamp nil)))) + + (ps:chain live-audio + (add-event-listener "playing" + (lambda () + (when (and needs-reconnect (not is-reconnecting)) + (setf is-reconnecting true) + (setf needs-reconnect false) + (ps:chain console (log "Reconnecting live stream after long pause to clear stale buffers...")) + + (ps:chain live-audio (pause)) + + (when (ps:@ window |resetSpectrumAnalyzer|) + (ps:chain window (reset-spectrum-analyzer))) + + (ps:chain live-audio (load)) + + (set-timeout + (lambda () + (ps:chain live-audio (play) + (catch (lambda (err) + (ps:chain console (log "Reconnect play failed:" err))))) + + (when (ps:@ window |initSpectrumAnalyzer|) + (ps:chain window (init-spectrum-analyzer)) + (ps:chain console (log "Spectrum analyzer reinitialized after reconnect"))) + + (setf is-reconnecting false)) + 200))))) + ))) + + ;; Restore user quality preference + (let ((selector (ps:chain document (get-element-by-id "live-stream-quality"))) + (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))) + (when (and selector (not (= (ps:@ selector value) stream-quality))) + (setf (ps:@ selector value) stream-quality) + (ps:chain selector (dispatch-event (ps:new (-Event "change"))))))))) + + ;; Frame redirection logic + (defun redirect-when-frame () + (let* ((path (ps:@ window location pathname)) + (is-frameset-page (not (= (ps:@ window parent) (ps:@ window self)))) + (is-content-frame (ps:chain path (includes "player-content")))) + + (when (and is-frameset-page (not is-content-frame)) + (setf (ps:@ window location href) "/asteroid/player-content")) + + (when (and (not is-frameset-page) is-content-frame) + (setf (ps:@ window location href) "/asteroid/player")))) + + ;; Setup all event listeners + (defun setup-event-listeners () + ;; Search + (ps:chain (ps:chain document (get-element-by-id "search-tracks")) + (add-event-listener "input" filter-tracks)) + + ;; Player controls + (ps:chain (ps:chain document (get-element-by-id "play-pause-btn")) + (add-event-listener "click" toggle-play-pause)) + (ps:chain (ps:chain document (get-element-by-id "prev-btn")) + (add-event-listener "click" play-previous)) + (ps:chain (ps:chain document (get-element-by-id "next-btn")) + (add-event-listener "click" play-next)) + (ps:chain (ps:chain document (get-element-by-id "shuffle-btn")) + (add-event-listener "click" toggle-shuffle)) + (ps:chain (ps:chain document (get-element-by-id "repeat-btn")) + (add-event-listener "click" toggle-repeat)) + + ;; Volume control + (ps:chain (ps:chain document (get-element-by-id "volume-slider")) + (add-event-listener "input" update-volume)) + + ;; Audio player events + (when (and *audio-player* (ps:chain *audio-player* add-event-listener)) + (ps:chain *audio-player* (add-event-listener "loadedmetadata" update-time-display)) + (ps:chain *audio-player* (add-event-listener "timeupdate" update-time-display)) + (ps:chain *audio-player* (add-event-listener "ended" handle-track-end)) + (ps:chain *audio-player* (add-event-listener "play" (lambda () (update-play-button "⏸️ Pause")))) + (ps:chain *audio-player* (add-event-listener "pause" (lambda () (update-play-button "▶️ Play"))))) + + ;; Playlist controls + (ps:chain (ps:chain document (get-element-by-id "create-playlist")) + (add-event-listener "click" create-playlist)) + (ps:chain (ps:chain document (get-element-by-id "clear-queue")) + (add-event-listener "click" clear-queue)) + (ps:chain (ps:chain document (get-element-by-id "save-queue")) + (add-event-listener "click" save-queue-as-playlist))) + + ;; Load tracks from API + (defun load-tracks () + (ps:chain + (ps:chain (fetch "/api/asteroid/tracks")) + (then (lambda (response) + (if (ps:@ response ok) + (ps:chain response (json)) (progn - (setf *tracks* (or (ps:@ data tracks) (array))) - (display-tracks *tracks*)) - (progn - (ps:chain console (error "Error loading tracks:" (ps:@ data error))) - (setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l) - "
Error loading tracks
")))))) - (catch (lambda (error) - (ps:chain console (error "Error loading tracks:" error)) - (setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l) - "
Error loading tracks
"))))) - - ;; Display tracks in library - (defun display-tracks (track-list) - (setf *filtered-library-tracks* track-list) - (setf *library-current-page* 1) - (render-library-page)) - - ;; Render current library page - (defun render-library-page () - (let ((container (ps:chain document (get-element-by-id "track-list"))) - (pagination-controls (ps:chain document (get-element-by-id "library-pagination-controls")))) - - (if (= (ps:@ *filtered-library-tracks* length) 0) - (progn - (setf (ps:@ container inner-h-t-m-l) "
No tracks found
") - (setf (ps:@ pagination-controls style display) "none") - (return))) - - ;; Calculate pagination - (let* ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*))) - (start-index (* (- *library-current-page* 1) *library-tracks-per-page*)) - (end-index (+ start-index *library-tracks-per-page*)) - (tracks-to-show (ps:chain *filtered-library-tracks* (slice start-index end-index)))) - - ;; Render tracks for current page - (let ((tracks-html (ps:chain tracks-to-show - (map (lambda (track page-index) - ;; Find the actual index in the full tracks array - (let ((actual-index (ps:chain *tracks* - (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ track id))))))) - (+ "
" - "
" - "
" (or (ps:@ track title) "Unknown Title") "
" - "
" (or (ps:@ track artist) "Unknown Artist") " • " (or (ps:@ track album) "Unknown Album") "
" - "
" - "
" - "" - "" - "" - "
" - "
")))) - (join "")))) - - (setf (ps:@ container inner-h-t-m-l) tracks-html) - - ;; Update pagination controls - (setf (ps:chain (ps:chain document (get-element-by-id "library-page-info")) text-content) - (+ "Page " *library-current-page* " of " total-pages " (" (ps:@ *filtered-library-tracks* length) " tracks)")) - (setf (ps:@ pagination-controls style display) - (if (> total-pages 1) "block" "none")))))) - - ;; Library pagination functions - (defun library-go-to-page (page) - (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))) - (when (and (>= page 1) (<= page total-pages)) - (setf *library-current-page* page) - (render-library-page)))) - - (defun library-previous-page () - (when (> *library-current-page* 1) - (setf *library-current-page* (- *library-current-page* 1)) - (render-library-page))) - - (defun library-next-page () - (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))) - (when (< *library-current-page* total-pages) - (setf *library-current-page* (+ *library-current-page* 1)) - (render-library-page)))) - - (defun library-go-to-last-page () - (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))) - (setf *library-current-page* total-pages) - (render-library-page))) - - (defun change-library-tracks-per-page () - (setf *library-tracks-per-page* - (parse-int (ps:chain (ps:chain document (get-element-by-id "library-tracks-per-page")) value))) - (setf *library-current-page* 1) - (render-library-page)) - - ;; Filter tracks based on search query - (defun filter-tracks () - (let ((query (ps:chain (ps:chain document (get-element-by-id "search-tracks")) value (to-lower-case)))) - (let ((filtered (ps:chain *tracks* - (filter (lambda (track) - (or (ps:chain (or (ps:@ track title) "") (to-lower-case) (includes query)) - (ps:chain (or (ps:@ track artist) "") (to-lower-case) (includes query)) - (ps:chain (or (ps:@ track album) "") (to-lower-case) (includes query)))))))) - (display-tracks filtered)))) - - ;; Play a specific track by index - (defun play-track (index) - (when (and (>= index 0) (< index (ps:@ *tracks* length))) - (setf *current-track* (aref *tracks* index)) - (setf *current-track-index* index) - - ;; Load track into audio player - (setf (ps:@ *audio-player* src) (+ "/asteroid/tracks/" (ps:@ *current-track* id) "/stream")) - (ps:chain *audio-player* (load)) - (ps:chain *audio-player* - (play) - (catch (lambda (error) - (ps:chain console (error "Playback error:" error)) - (alert "Error playing track. The track may not be available.")))) - - (update-player-display) - - ;; Update server-side player state - (ps:chain (fetch (+ "/api/asteroid/player/play?track-id=" (ps:@ *current-track* id)) - (ps:create :method "POST")) - (catch (lambda (error) - (ps:chain console (error "API update error:" error))))))) - - ;; Toggle play/pause - (defun toggle-play-pause () - (if *current-track* - (if (ps:@ *audio-player* paused) - (ps:chain *audio-player* (play)) - (ps:chain *audio-player* (pause))) - (alert "Please select a track to play"))) - - ;; Play previous track - (defun play-previous () - (if (> (ps:@ *play-queue* length) 0) - ;; Play from queue - (let ((prev-index (max 0 (- *current-track-index* 1)))) - (play-track prev-index)) - ;; Play previous track in library - (let ((prev-index (if (> *current-track-index* 0) - (- *current-track-index* 1) - (- (ps:@ *tracks* length) 1)))) - (play-track prev-index)))) - - ;; Play next track - (defun play-next () - (if (> (ps:@ *play-queue* length) 0) - ;; Play from queue - (let ((next-track (ps:chain *play-queue* (shift)))) - (play-track (ps:chain *tracks* - (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ next-track id)))))) - (update-queue-display)) - ;; Play next track in library - (let ((next-index (if *is-shuffled* - (floor (* (random) (ps:@ *tracks* length))) - (mod (+ *current-track-index* 1) (ps:@ *tracks* length))))) - (play-track next-index)))) - - ;; Handle track end - (defun handle-track-end () - (if *is-repeating* - (progn - (setf (ps:@ *audio-player* current-time) 0) - (ps:chain *audio-player* (play))) - (play-next))) - - ;; Toggle shuffle mode - (defun toggle-shuffle () - (setf *is-shuffled* (not *is-shuffled*)) - (let ((btn (ps:chain document (get-element-by-id "shuffle-btn")))) - (setf (ps:@ btn text-content) (if *is-shuffled* "🔀 Shuffle ON" "🔀 Shuffle")) - (ps:chain btn (class-list toggle "active" *is-shuffled*)))) - - ;; Toggle repeat mode - (defun toggle-repeat () - (setf *is-repeating* (not *is-repeating*)) - (let ((btn (ps:chain document (get-element-by-id "repeat-btn")))) - (setf (ps:@ btn text-content) (if *is-repeating* "🔁 Repeat ON" "🔁 Repeat")) - (ps:chain btn (class-list toggle "active" *is-repeating*)))) - - ;; Update volume - (defun update-volume () - (let ((volume (/ (parse-int (ps:chain (ps:chain document (get-element-by-id "volume-slider")) value)) 100))) - (when *audio-player* - (setf (ps:@ *audio-player* volume) volume)))) - - ;; Update time display - (defun update-time-display () - (let ((current (format-time (ps:@ *audio-player* current-time))) - (total (format-time (ps:@ *audio-player* duration)))) - (setf (ps:chain (ps:chain document (get-element-by-id "current-time")) text-content) current) - (setf (ps:chain (ps:chain document (get-element-by-id "total-time")) text-content) total))) - - ;; Format time helper - (defun format-time (seconds) - (if (isNaN seconds) - "0:00" - (let ((mins (floor (/ seconds 60))) - (secs (floor (mod seconds 60)))) - (+ mins ":" (ps:chain secs (to-string) (pad-start 2 "0")))))) - - ;; Update play button text - (defun update-play-button (text) - (setf (ps:chain (ps:chain document (get-element-by-id "play-pause-btn")) text-content) text)) - - ;; Update player display with current track info - (defun update-player-display () - (when *current-track* - (setf (ps:chain (ps:chain document (get-element-by-id "current-title")) text-content) - (or (ps:@ *current-track* title) "Unknown Title")) - (setf (ps:chain (ps:chain document (get-element-by-id "current-artist")) text-content) - (or (ps:@ *current-track* artist) "Unknown Artist")) - (setf (ps:chain (ps:chain document (get-element-by-id "current-album")) text-content) - (or (ps:@ *current-track* album) "Unknown Album")))) - - ;; Add track to queue - (defun add-to-queue (index) - (when (and (>= index 0) (< index (ps:@ *tracks* length))) - (setf (aref *play-queue* (ps:@ *play-queue* length)) (aref *tracks* index)) - (update-queue-display))) - - ;; Update queue display - (defun update-queue-display () - (let ((container (ps:chain document (get-element-by-id "play-queue")))) - (if (= (ps:@ *play-queue* length) 0) - (setf (ps:@ container inner-h-t-m-l) "
Queue is empty
") - (let ((queue-html (ps:chain *play-queue* - (map (lambda (track index) - (+ "
" - "
" - "
" (or (ps:@ track title) "Unknown Title") "
" - "
" (or (ps:@ track artist) "Unknown Artist") "
" - "
" - "" - "
"))) + (ps:chain console (error (+ "HTTP " (ps:@ response status)))) + (ps:create :status "error" :tracks (array)))))) + (then (lambda (result) + ;; Handle RADIANCE API wrapper format + (let ((data (or (ps:@ result data) result))) + (if (= (ps:@ data status) "success") + (progn + (setf *tracks* (or (ps:@ data tracks) (array))) + (display-tracks *tracks*)) + (progn + (ps:chain console (error "Error loading tracks:" (ps:@ data error))) + (setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l) + "
Error loading tracks
")))))) + (catch (lambda (error) + (ps:chain console (error "Error loading tracks:" error)) + (setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l) + "
Error loading tracks
"))))) + + ;; Display tracks in library + (defun display-tracks (track-list) + (setf *filtered-library-tracks* track-list) + (setf *library-current-page* 1) + (render-library-page)) + + ;; Render current library page + (defun render-library-page () + (let ((container (ps:chain document (get-element-by-id "track-list"))) + (pagination-controls (ps:chain document (get-element-by-id "library-pagination-controls")))) + + (if (= (ps:@ *filtered-library-tracks* length) 0) + (progn + (setf (ps:@ container inner-h-t-m-l) "
No tracks found
") + (setf (ps:@ pagination-controls style display) "none") + (return))) + + ;; Calculate pagination + (let* ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*))) + (start-index (* (- *library-current-page* 1) *library-tracks-per-page*)) + (end-index (+ start-index *library-tracks-per-page*)) + (tracks-to-show (ps:chain *filtered-library-tracks* (slice start-index end-index)))) + + ;; Render tracks for current page + (let ((tracks-html (ps:chain tracks-to-show + (map (lambda (track page-index) + ;; Find the actual index in the full tracks array + (let ((actual-index (ps:chain *tracks* + (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ track id))))))) + (+ "
" + "
" + "
" (or (ps:@ track title) "Unknown Title") "
" + "
" (or (ps:@ track artist) "Unknown Artist") " • " (or (ps:@ track album) "Unknown Album") "
" + "
" + "
" + "" + "" + "" + "
" + "
")))) (join "")))) - (setf (ps:@ container inner-h-t-m-l) queue-html))))) - - ;; Remove track from queue - (defun remove-from-queue (index) - (ps:chain *play-queue* (splice index 1)) - (update-queue-display)) - - ;; Clear queue - (defun clear-queue () - (setf *play-queue* (array)) - (update-queue-display)) - - ;; Store playlists for the add-to-playlist menu - (defvar *user-playlists* (array)) - - ;; Show add to playlist dropdown menu - (defun show-add-to-playlist-menu (track-id event) - (ps:chain event (stop-propagation)) - ;; Remove any existing menu - (let ((existing-menu (ps:chain document (get-element-by-id "playlist-dropdown-menu")))) - (when existing-menu - (ps:chain existing-menu (remove)))) - - ;; Fetch playlists and show menu - (ps:chain (fetch "/api/asteroid/playlists") - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (result) - (let* ((data (or (ps:@ result data) result)) - (playlists (or (ps:@ data playlists) (array))) - (menu (ps:chain document (create-element "div")))) - (setf *user-playlists* playlists) - (setf (ps:@ menu id) "playlist-dropdown-menu") - (setf (ps:@ menu class-name) "playlist-dropdown-menu") - (setf (ps:@ menu style position) "fixed") - (setf (ps:@ menu style left) (+ (ps:@ event client-x) "px")) - (setf (ps:@ menu style top) (+ (ps:@ event client-y) "px")) - (setf (ps:@ menu style z-index) "1000") - (setf (ps:@ menu style background) "#1a1a2e") - (setf (ps:@ menu style border) "1px solid #00ff00") - (setf (ps:@ menu style border-radius) "4px") - (setf (ps:@ menu style padding) "5px 0") - (setf (ps:@ menu style min-width) "150px") - - (if (= (ps:@ playlists length) 0) - (setf (ps:@ menu inner-h-t-m-l) - "
No playlists yet
") - (setf (ps:@ menu inner-h-t-m-l) - (ps:chain playlists - (map (lambda (playlist) - (+ "
" - (ps:@ playlist name) " (" (ps:@ playlist "track-count") ")" - "
"))) - (join "")))) - - (ps:chain document body (append-child menu)) - - ;; Close menu when clicking elsewhere - (let ((close-handler (lambda (e) - (when (not (ps:chain menu (contains (ps:@ e target)))) - (ps:chain menu (remove)) - (ps:chain document (remove-event-listener "click" close-handler)))))) - (set-timeout (lambda () - (ps:chain document (add-event-listener "click" close-handler))) - 100))))) - (catch (lambda (error) - (ps:chain console (error "Error loading playlists for menu:" error)))))) - - ;; Add track to a specific playlist - (defun add-track-to-playlist (playlist-id track-id) - ;; Close the menu - (let ((menu (ps:chain document (get-element-by-id "playlist-dropdown-menu")))) - (when menu (ps:chain menu (remove)))) - - (let ((form-data (ps:new (-Form-data)))) - (ps:chain form-data (append "playlist-id" playlist-id)) - (ps:chain form-data (append "track-id" track-id)) - (ps:chain (fetch "/api/asteroid/playlists/add-track" - (ps:create :method "POST" :body form-data)) + + (setf (ps:@ container inner-h-t-m-l) tracks-html) + + ;; Update pagination controls + (setf (ps:chain (ps:chain document (get-element-by-id "library-page-info")) text-content) + (+ "Page " *library-current-page* " of " total-pages " (" (ps:@ *filtered-library-tracks* length) " tracks)")) + (setf (ps:@ pagination-controls style display) + (if (> total-pages 1) "block" "none")))))) + + ;; Library pagination functions + (defun library-go-to-page (page) + (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))) + (when (and (>= page 1) (<= page total-pages)) + (setf *library-current-page* page) + (render-library-page)))) + + (defun library-previous-page () + (when (> *library-current-page* 1) + (setf *library-current-page* (- *library-current-page* 1)) + (render-library-page))) + + (defun library-next-page () + (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))) + (when (< *library-current-page* total-pages) + (setf *library-current-page* (+ *library-current-page* 1)) + (render-library-page)))) + + (defun library-go-to-last-page () + (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))) + (setf *library-current-page* total-pages) + (render-library-page))) + + (defun change-library-tracks-per-page () + (setf *library-tracks-per-page* + (parse-int (ps:chain (ps:chain document (get-element-by-id "library-tracks-per-page")) value))) + (setf *library-current-page* 1) + (render-library-page)) + + ;; Filter tracks based on search query + (defun filter-tracks () + (let ((query (ps:chain (ps:chain document (get-element-by-id "search-tracks")) value (to-lower-case)))) + (let ((filtered (ps:chain *tracks* + (filter (lambda (track) + (or (ps:chain (or (ps:@ track title) "") (to-lower-case) (includes query)) + (ps:chain (or (ps:@ track artist) "") (to-lower-case) (includes query)) + (ps:chain (or (ps:@ track album) "") (to-lower-case) (includes query)))))))) + (display-tracks filtered)))) + + ;; Play a specific track by index + (defun play-track (index) + (when (and (>= index 0) (< index (ps:@ *tracks* length))) + (setf *current-track* (aref *tracks* index)) + (setf *current-track-index* index) + + ;; Load track into audio player + (setf (ps:@ *audio-player* src) (+ "/asteroid/tracks/" (ps:@ *current-track* id) "/stream")) + (ps:chain *audio-player* (load)) + (ps:chain *audio-player* + (play) + (catch (lambda (error) + (ps:chain console (error "Playback error:" error)) + (alert "Error playing track. The track may not be available.")))) + + (update-player-display) + + ;; Update server-side player state + (ps:chain (fetch (+ "/api/asteroid/player/play?track-id=" (ps:@ *current-track* id)) + (ps:create :method "POST")) + (catch (lambda (error) + (ps:chain console (error "API update error:" error))))))) + + ;; Toggle play/pause + (defun toggle-play-pause () + (if *current-track* + (if (ps:@ *audio-player* paused) + (ps:chain *audio-player* (play)) + (ps:chain *audio-player* (pause))) + (alert "Please select a track to play"))) + + ;; Play previous track + (defun play-previous () + (if (> (ps:@ *play-queue* length) 0) + ;; Play from queue + (let ((prev-index (max 0 (- *current-track-index* 1)))) + (play-track prev-index)) + ;; Play previous track in library + (let ((prev-index (if (> *current-track-index* 0) + (- *current-track-index* 1) + (- (ps:@ *tracks* length) 1)))) + (play-track prev-index)))) + + ;; Play next track + (defun play-next () + (if (> (ps:@ *play-queue* length) 0) + ;; Play from queue + (let ((next-track (ps:chain *play-queue* (shift)))) + (play-track (ps:chain *tracks* + (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ next-track id)))))) + (update-queue-display)) + ;; Play next track in library + (let ((next-index (if *is-shuffled* + (floor (* (random) (ps:@ *tracks* length))) + (mod (+ *current-track-index* 1) (ps:@ *tracks* length))))) + (play-track next-index)))) + + ;; Handle track end + (defun handle-track-end () + (if *is-repeating* + (progn + (setf (ps:@ *audio-player* current-time) 0) + (ps:chain *audio-player* (play))) + (play-next))) + + ;; Toggle shuffle mode + (defun toggle-shuffle () + (setf *is-shuffled* (not *is-shuffled*)) + (let ((btn (ps:chain document (get-element-by-id "shuffle-btn")))) + (setf (ps:@ btn text-content) (if *is-shuffled* "🔀 Shuffle ON" "🔀 Shuffle")) + (ps:chain btn (class-list toggle "active" *is-shuffled*)))) + + ;; Toggle repeat mode + (defun toggle-repeat () + (setf *is-repeating* (not *is-repeating*)) + (let ((btn (ps:chain document (get-element-by-id "repeat-btn")))) + (setf (ps:@ btn text-content) (if *is-repeating* "🔁 Repeat ON" "🔁 Repeat")) + (ps:chain btn (class-list toggle "active" *is-repeating*)))) + + ;; Update volume + (defun update-volume () + (let ((volume (/ (parse-int (ps:chain (ps:chain document (get-element-by-id "volume-slider")) value)) 100))) + (when *audio-player* + (setf (ps:@ *audio-player* volume) volume)))) + + ;; Update time display + (defun update-time-display () + (let ((current (format-time (ps:@ *audio-player* current-time))) + (total (format-time (ps:@ *audio-player* duration)))) + (setf (ps:chain (ps:chain document (get-element-by-id "current-time")) text-content) current) + (setf (ps:chain (ps:chain document (get-element-by-id "total-time")) text-content) total))) + + ;; Format time helper + (defun format-time (seconds) + (if (isNaN seconds) + "0:00" + (let ((mins (floor (/ seconds 60))) + (secs (floor (mod seconds 60)))) + (+ mins ":" (ps:chain secs (to-string) (pad-start 2 "0")))))) + + ;; Update play button text + (defun update-play-button (text) + (setf (ps:chain (ps:chain document (get-element-by-id "play-pause-btn")) text-content) text)) + + ;; Update player display with current track info + (defun update-player-display () + (when *current-track* + (setf (ps:chain (ps:chain document (get-element-by-id "current-title")) text-content) + (or (ps:@ *current-track* title) "Unknown Title")) + (setf (ps:chain (ps:chain document (get-element-by-id "current-artist")) text-content) + (or (ps:@ *current-track* artist) "Unknown Artist")) + (setf (ps:chain (ps:chain document (get-element-by-id "current-album")) text-content) + (or (ps:@ *current-track* album) "Unknown Album")))) + + ;; Add track to queue + (defun add-to-queue (index) + (when (and (>= index 0) (< index (ps:@ *tracks* length))) + (setf (aref *play-queue* (ps:@ *play-queue* length)) (aref *tracks* index)) + (update-queue-display))) + + ;; Update queue display + (defun update-queue-display () + (let ((container (ps:chain document (get-element-by-id "play-queue")))) + (if (= (ps:@ *play-queue* length) 0) + (setf (ps:@ container inner-h-t-m-l) "
Queue is empty
") + (let ((queue-html (ps:chain *play-queue* + (map (lambda (track index) + (+ "
" + "
" + "
" (or (ps:@ track title) "Unknown Title") "
" + "
" (or (ps:@ track artist) "Unknown Artist") "
" + "
" + "" + "
"))) + (join "")))) + (setf (ps:@ container inner-h-t-m-l) queue-html))))) + + ;; Remove track from queue + (defun remove-from-queue (index) + (ps:chain *play-queue* (splice index 1)) + (update-queue-display)) + + ;; Clear queue + (defun clear-queue () + (setf *play-queue* (array)) + (update-queue-display)) + + ;; Store playlists for the add-to-playlist menu + (defvar *user-playlists* (array)) + + ;; Show add to playlist dropdown menu + (defun show-add-to-playlist-menu (track-id event) + (ps:chain event (stop-propagation)) + ;; Remove any existing menu + (let ((existing-menu (ps:chain document (get-element-by-id "playlist-dropdown-menu")))) + (when existing-menu + (ps:chain existing-menu (remove)))) + + ;; Fetch playlists and show menu + (ps:chain (fetch "/api/asteroid/playlists") (then (lambda (response) (ps:chain response (json)))) (then (lambda (result) - (let ((data (or (ps:@ result data) result))) - (if (= (ps:@ data status) "success") - (progn - ;; Find playlist name for feedback - (let ((playlist (ps:chain *user-playlists* - (find (lambda (p) (= (ps:@ p id) playlist-id)))))) - (alert (+ "Track added to \"" (if playlist (ps:@ playlist name) "playlist") "\""))) - (load-playlists)) - (alert (+ "Error: " (ps:@ data message))))))) + (let* ((data (or (ps:@ result data) result)) + (playlists (or (ps:@ data playlists) (array))) + (menu (ps:chain document (create-element "div")))) + (setf *user-playlists* playlists) + (setf (ps:@ menu id) "playlist-dropdown-menu") + (setf (ps:@ menu class-name) "playlist-dropdown-menu") + (setf (ps:@ menu style position) "fixed") + (setf (ps:@ menu style left) (+ (ps:@ event client-x) "px")) + (setf (ps:@ menu style top) (+ (ps:@ event client-y) "px")) + (setf (ps:@ menu style z-index) "1000") + (setf (ps:@ menu style background) "#1a1a2e") + (setf (ps:@ menu style border) "1px solid #00ff00") + (setf (ps:@ menu style border-radius) "4px") + (setf (ps:@ menu style padding) "5px 0") + (setf (ps:@ menu style min-width) "150px") + + (if (= (ps:@ playlists length) 0) + (setf (ps:@ menu inner-h-t-m-l) + "
No playlists yet
") + (setf (ps:@ menu inner-h-t-m-l) + (ps:chain playlists + (map (lambda (playlist) + (+ "
" + (ps:@ playlist name) " (" (ps:@ playlist "track-count") ")" + "
"))) + (join "")))) + + (ps:chain document body (append-child menu)) + + ;; Close menu when clicking elsewhere + (let ((close-handler (lambda (e) + (when (not (ps:chain menu (contains (ps:@ e target)))) + (ps:chain menu (remove)) + (ps:chain document (remove-event-listener "click" close-handler)))))) + (set-timeout (lambda () + (ps:chain document (add-event-listener "click" close-handler))) + 100))))) (catch (lambda (error) - (ps:chain console (error "Error adding track to playlist:" error)) - (alert "Error adding track to playlist")))))) - - ;; Create playlist - (defun create-playlist () - (let ((name (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value (trim)))) - (when (not (= name "")) - (let ((form-data (ps:new (-Form-data)))) - (ps:chain form-data (append "name" name)) - (ps:chain form-data (append "description" "")) - - (ps:chain (fetch "/api/asteroid/playlists/create" - (ps:create :method "POST" :body form-data)) - (then (lambda (response) - (ps:chain response (json)))) - (then (lambda (result) - ;; Handle RADIANCE API wrapper format - (let ((data (or (ps:@ result data) result))) - (if (= (ps:@ data status) "success") - (progn - (alert (+ "Playlist \"" name "\" created successfully!")) - (setf (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value) "") - - ;; Wait a moment then reload playlists - (set-timeout load-playlists 500)) - (alert (+ "Error creating playlist: " (ps:@ data message))))))) - (catch (lambda (error) - (ps:chain console (error "Error creating playlist:" error)) - (alert (+ "Error creating playlist: " (ps:@ error message)))))))))) - - ;; Save queue as playlist - (defun save-queue-as-playlist () - (if (> (ps:@ *play-queue* length) 0) - (let ((name (prompt "Enter playlist name:"))) - (when name - ;; Create the playlist - (let ((form-data (ps:new (-Form-data)))) - (ps:chain form-data (append "name" name)) - (ps:chain form-data (append "description" (+ "Created from queue with " (ps:@ *play-queue* length) " tracks"))) - - (ps:chain (fetch "/api/asteroid/playlists/create" - (ps:create :method "POST" :body form-data)) - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (create-result) - ;; Handle RADIANCE API wrapper format - (let ((create-data (or (ps:@ create-result data) create-result))) - (if (= (ps:@ create-data status) "success") - (progn - ;; Wait a moment for database to update, then fetch playlists - (set-timeout - (lambda () - ;; Get the new playlist ID by fetching playlists - (ps:chain (fetch "/api/asteroid/playlists") - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (playlists-result) - ;; Handle RADIANCE API wrapper format - (let ((playlist-result-data (or (ps:@ playlists-result data) playlists-result))) - (if (and (= (ps:@ playlist-result-data status) "success") - (> (ps:@ playlist-result-data playlists length) 0)) - (progn - ;; Find the playlist with matching name (most recent) - (let ((new-playlist (or (ps:chain (ps:@ playlist-result-data playlists) - (find (lambda (p) (= (ps:@ p name) name)))) - (aref (ps:@ playlist-result-data playlists) - (- (ps:@ playlist-result-data playlists length) 1))))) - - ;; Add all tracks from queue to playlist - (let ((added-count 0)) - (ps:chain *play-queue* - (for-each (lambda (track) - (let ((track-id (ps:@ track id))) - (when track-id - (let ((add-form-data (ps:new (-Form-data)))) - (ps:chain add-form-data (append "playlist-id" (ps:@ new-playlist id))) - (ps:chain add-form-data (append "track-id" track-id)) - - (ps:chain (fetch "/api/asteroid/playlists/add-track" - (ps:create :method "POST" :body add-form-data)) - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (add-result) - (when (= (ps:@ add-result data status) "success") - (setf added-count (+ added-count 1))))) - (catch (lambda (err) - (ps:chain console (log "Error adding track:" err))))))))))) - - (alert (+ "Playlist \"" name "\" created with " added-count " tracks!")) - (load-playlists)))) - (progn - (alert (+ "Playlist created but could not add tracks. Error: " - (or (ps:@ playlist-result-data message) "Unknown"))) - (load-playlists)))))) - (catch (lambda (error) - (ps:chain console (error "Error fetching playlists:" error)) - (alert "Playlist created but could not add tracks"))))) - 500)) - (alert (+ "Error creating playlist: " (ps:@ create-data message))))))) - (catch (lambda (error) - (ps:chain console (error "Error saving queue as playlist:" error)) - (alert (+ "Error saving queue as playlist: " (ps:@ error message))))))))) - (alert "Queue is empty"))) - - ;; Load playlists from API - (defun load-playlists () - (ps:chain - (fetch "/api/asteroid/playlists") - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (result) - (ps:chain console (log "Playlists API result:" result)) - (let ((playlists (cond - ((and (ps:@ result data) (= (ps:@ result data status) "success")) - (ps:chain console (log "Found playlists in result.data.playlists")) - (or (ps:@ result data playlists) (array))) - ((= (ps:@ result status) "success") - (ps:chain console (log "Found playlists in result.playlists")) - (or (ps:@ result playlists) (array))) - (t - (ps:chain console (log "No playlists found in response")) - (array))))) - (ps:chain console (log "Playlists to display:" playlists)) - (display-playlists playlists)))) - (catch (lambda (error) - (ps:chain console (error "Error loading playlists:" error)) - (display-playlists (array)))))) - - ;; Display playlists - (defun display-playlists (playlists) - (let ((container (ps:chain document (get-element-by-id "playlists-container")))) - - (if (or (not playlists) (= (ps:@ playlists length) 0)) - (setf (ps:@ container inner-h-t-m-l) "
No playlists created yet.
") - (let ((playlists-html (ps:chain playlists - (map (lambda (playlist) - (+ "
" - "
" - "
" (ps:@ playlist name) "
" - "
" (ps:@ playlist "track-count") " tracks
" - "
" - "
" - "" - "" - "" - "
" - "
"))) - (join "")))) - - (setf (ps:@ container inner-h-t-m-l) playlists-html))))) - - ;; Delete playlist - (defun delete-playlist (playlist-id playlist-name) - (when (confirm (+ "Are you sure you want to delete playlist \"" playlist-name "\"?")) + (ps:chain console (error "Error loading playlists for menu:" error)))))) + + ;; Add track to a specific playlist + (defun add-track-to-playlist (playlist-id track-id) + ;; Close the menu + (let ((menu (ps:chain document (get-element-by-id "playlist-dropdown-menu")))) + (when menu (ps:chain menu (remove)))) + (let ((form-data (ps:new (-Form-data)))) (ps:chain form-data (append "playlist-id" playlist-id)) - (ps:chain (fetch "/api/asteroid/playlists/delete" + (ps:chain form-data (append "track-id" track-id)) + (ps:chain (fetch "/api/asteroid/playlists/add-track" (ps:create :method "POST" :body form-data)) (then (lambda (response) (ps:chain response (json)))) (then (lambda (result) (let ((data (or (ps:@ result data) result))) (if (= (ps:@ data status) "success") (progn - (alert (+ "Playlist \"" playlist-name "\" deleted")) + ;; Find playlist name for feedback + (let ((playlist (ps:chain *user-playlists* + (find (lambda (p) (= (ps:@ p id) playlist-id)))))) + (alert (+ "Track added to \"" (if playlist (ps:@ playlist name) "playlist") "\""))) (load-playlists)) - (alert (+ "Error deleting playlist: " (ps:@ data message))))))) + (alert (+ "Error: " (ps:@ data message))))))) (catch (lambda (error) - (ps:chain console (error "Error deleting playlist:" error)) - (alert "Error deleting playlist"))))))) - - ;; View playlist contents - (defun view-playlist (playlist-id) - (ps:chain - (fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id)) - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (result) - (let ((data (or (ps:@ result data) result))) - (if (and (= (ps:@ data status) "success") (ps:@ data playlist)) - (let* ((playlist (ps:@ data playlist)) - (tracks (or (ps:@ playlist tracks) (array))) - (track-list (if (> (ps:@ tracks length) 0) - (ps:chain tracks - (map (lambda (track index) - (+ (+ index 1) ". " - (or (ps:@ track artist) "Unknown") " - " - (or (ps:@ track title) "Unknown")))) - (join "\\n")) - "No tracks in playlist"))) - (alert (+ "Playlist: " (ps:@ playlist name) "\\n" - "Tracks: " (ps:@ playlist "track-count") "\\n\\n" - track-list))) - (alert "Could not load playlist"))))) - (catch (lambda (error) - (ps:chain console (error "Error viewing playlist:" error)) - (alert "Error viewing playlist"))))) - - ;; Load playlist into queue - (defun load-playlist (playlist-id) - (ps:chain - (ps:chain (fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id))) - (then (lambda (response) (ps:chain response (json)))) - (then (lambda (result) - ;; Handle RADIANCE API wrapper format - (let ((data (or (ps:@ result data) result))) - (if (and (= (ps:@ data status) "success") (ps:@ data playlist)) - (let ((playlist (ps:@ data playlist))) - - ;; Clear current queue - (setf *play-queue* (array)) - - ;; Add all playlist tracks to queue - (if (and (ps:@ playlist tracks) (> (ps:@ playlist tracks length) 0)) - (progn - (ps:chain (ps:@ playlist tracks) - (for-each (lambda (track) - ;; Find the full track object from our tracks array - (let ((full-track (ps:chain *tracks* - (find (lambda (trk) (= (ps:@ trk id) (ps:@ track id))))))) - (when full-track - (setf (aref *play-queue* (ps:@ *play-queue* length)) full-track)))))) - - (update-queue-display) - (let ((loaded-count (ps:@ *play-queue* length))) - (alert (+ "Loaded " loaded-count " tracks from \"" (ps:@ playlist name) "\" into queue!")) - - ;; Optionally start playing the first track - (when (> loaded-count 0) - (let* ((first-track (aref *play-queue* 0)) - (track-index (ps:chain *tracks* - (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ first-track id))))))) - ;; Remove first track from queue since we're playing it - (ps:chain *play-queue* (shift)) - (update-queue-display) - (when (>= track-index 0) - (play-track track-index)))))) - (alert (+ "Playlist \"" (ps:@ playlist name) "\" is empty")))) - (alert (+ "Error loading playlist: " (or (ps:@ data message) "Unknown error"))))))) - (catch (lambda (error) - (ps:chain console (error "Error loading playlist:" error)) - (alert (+ "Error loading playlist: " (ps:@ error message))))))) - - ;; Stream quality configuration - (defun get-live-stream-config (stream-base-url quality) - (let ((config (ps:create - :aac (ps:create + (ps:chain console (error "Error adding track to playlist:" error)) + (alert "Error adding track to playlist")))))) + + ;; Create playlist + (defun create-playlist () + (let ((name (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value (trim)))) + (when (not (= name "")) + (let ((form-data (ps:new (-Form-data)))) + (ps:chain form-data (append "name" name)) + (ps:chain form-data (append "description" "")) + + (ps:chain (fetch "/api/asteroid/playlists/create" + (ps:create :method "POST" :body form-data)) + (then (lambda (response) + (ps:chain response (json)))) + (then (lambda (result) + ;; Handle RADIANCE API wrapper format + (let ((data (or (ps:@ result data) result))) + (if (= (ps:@ data status) "success") + (progn + (alert (+ "Playlist \"" name "\" created successfully!")) + (setf (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value) "") + + ;; Wait a moment then reload playlists + (set-timeout load-playlists 500)) + (alert (+ "Error creating playlist: " (ps:@ data message))))))) + (catch (lambda (error) + (ps:chain console (error "Error creating playlist:" error)) + (alert (+ "Error creating playlist: " (ps:@ error message)))))))))) + + ;; Save queue as playlist + (defun save-queue-as-playlist () + (if (> (ps:@ *play-queue* length) 0) + (let ((name (prompt "Enter playlist name:"))) + (when name + ;; Create the playlist + (let ((form-data (ps:new (-Form-data)))) + (ps:chain form-data (append "name" name)) + (ps:chain form-data (append "description" (+ "Created from queue with " (ps:@ *play-queue* length) " tracks"))) + + (ps:chain (fetch "/api/asteroid/playlists/create" + (ps:create :method "POST" :body form-data)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (create-result) + ;; Handle RADIANCE API wrapper format + (let ((create-data (or (ps:@ create-result data) create-result))) + (if (= (ps:@ create-data status) "success") + (progn + ;; Wait a moment for database to update, then fetch playlists + (set-timeout + (lambda () + ;; Get the new playlist ID by fetching playlists + (ps:chain (fetch "/api/asteroid/playlists") + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (playlists-result) + ;; Handle RADIANCE API wrapper format + (let ((playlist-result-data (or (ps:@ playlists-result data) playlists-result))) + (if (and (= (ps:@ playlist-result-data status) "success") + (> (ps:@ playlist-result-data playlists length) 0)) + (progn + ;; Find the playlist with matching name (most recent) + (let ((new-playlist (or (ps:chain (ps:@ playlist-result-data playlists) + (find (lambda (p) (= (ps:@ p name) name)))) + (aref (ps:@ playlist-result-data playlists) + (- (ps:@ playlist-result-data playlists length) 1))))) + + ;; Add all tracks from queue to playlist + (let ((added-count 0)) + (ps:chain *play-queue* + (for-each (lambda (track) + (let ((track-id (ps:@ track id))) + (when track-id + (let ((add-form-data (ps:new (-Form-data)))) + (ps:chain add-form-data (append "playlist-id" (ps:@ new-playlist id))) + (ps:chain add-form-data (append "track-id" track-id)) + + (ps:chain (fetch "/api/asteroid/playlists/add-track" + (ps:create :method "POST" :body add-form-data)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (add-result) + (when (= (ps:@ add-result data status) "success") + (setf added-count (+ added-count 1))))) + (catch (lambda (err) + (ps:chain console (log "Error adding track:" err))))))))))) + + (alert (+ "Playlist \"" name "\" created with " added-count " tracks!")) + (load-playlists)))) + (progn + (alert (+ "Playlist created but could not add tracks. Error: " + (or (ps:@ playlist-result-data message) "Unknown"))) + (load-playlists)))))) + (catch (lambda (error) + (ps:chain console (error "Error fetching playlists:" error)) + (alert "Playlist created but could not add tracks"))))) + 500)) + (alert (+ "Error creating playlist: " (ps:@ create-data message))))))) + (catch (lambda (error) + (ps:chain console (error "Error saving queue as playlist:" error)) + (alert (+ "Error saving queue as playlist: " (ps:@ error message))))))))) + (alert "Queue is empty"))) + + ;; Load playlists from API + (defun load-playlists () + (ps:chain + (fetch "/api/asteroid/playlists") + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (ps:chain console (log "Playlists API result:" result)) + (let ((playlists (cond + ((and (ps:@ result data) (= (ps:@ result data status) "success")) + (ps:chain console (log "Found playlists in result.data.playlists")) + (or (ps:@ result data playlists) (array))) + ((= (ps:@ result status) "success") + (ps:chain console (log "Found playlists in result.playlists")) + (or (ps:@ result playlists) (array))) + (t + (ps:chain console (log "No playlists found in response")) + (array))))) + (ps:chain console (log "Playlists to display:" playlists)) + (display-playlists playlists)))) + (catch (lambda (error) + (ps:chain console (error "Error loading playlists:" error)) + (display-playlists (array)))))) + + ;; Display playlists + (defun display-playlists (playlists) + (let ((container (ps:chain document (get-element-by-id "playlists-container")))) + + (if (or (not playlists) (= (ps:@ playlists length) 0)) + (setf (ps:@ container inner-h-t-m-l) "
No playlists created yet.
") + (let ((playlists-html (ps:chain playlists + (map (lambda (playlist) + (+ "
" + "
" + "
" (ps:@ playlist name) "
" + "
" (ps:@ playlist "track-count") " tracks
" + "
" + "
" + "" + "" + "" + "
" + "
"))) + (join "")))) + + (setf (ps:@ container inner-h-t-m-l) playlists-html))))) + + ;; Delete playlist + (defun delete-playlist (playlist-id playlist-name) + (when (confirm (+ "Are you sure you want to delete playlist \"" playlist-name "\"?")) + (let ((form-data (ps:new (-Form-data)))) + (ps:chain form-data (append "playlist-id" playlist-id)) + (ps:chain (fetch "/api/asteroid/playlists/delete" + (ps:create :method "POST" :body form-data)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (if (= (ps:@ data status) "success") + (progn + (alert (+ "Playlist \"" playlist-name "\" deleted")) + (load-playlists)) + (alert (+ "Error deleting playlist: " (ps:@ data message))))))) + (catch (lambda (error) + (ps:chain console (error "Error deleting playlist:" error)) + (alert "Error deleting playlist"))))))) + + ;; View playlist contents + (defun view-playlist (playlist-id) + (ps:chain + (fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (if (and (= (ps:@ data status) "success") (ps:@ data playlist)) + (let* ((playlist (ps:@ data playlist)) + (tracks (or (ps:@ playlist tracks) (array))) + (track-list (if (> (ps:@ tracks length) 0) + (ps:chain tracks + (map (lambda (track index) + (+ (+ index 1) ". " + (or (ps:@ track artist) "Unknown") " - " + (or (ps:@ track title) "Unknown")))) + (join "\\n")) + "No tracks in playlist"))) + (alert (+ "Playlist: " (ps:@ playlist name) "\\n" + "Tracks: " (ps:@ playlist "track-count") "\\n\\n" + track-list))) + (alert "Could not load playlist"))))) + (catch (lambda (error) + (ps:chain console (error "Error viewing playlist:" error)) + (alert "Error viewing playlist"))))) + + ;; Load playlist into queue + (defun load-playlist (playlist-id) + (ps:chain + (ps:chain (fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id))) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + ;; Handle RADIANCE API wrapper format + (let ((data (or (ps:@ result data) result))) + (if (and (= (ps:@ data status) "success") (ps:@ data playlist)) + (let ((playlist (ps:@ data playlist))) + + ;; Clear current queue + (setf *play-queue* (array)) + + ;; Add all playlist tracks to queue + (if (and (ps:@ playlist tracks) (> (ps:@ playlist tracks length) 0)) + (progn + (ps:chain (ps:@ playlist tracks) + (for-each (lambda (track) + ;; Find the full track object from our tracks array + (let ((full-track (ps:chain *tracks* + (find (lambda (trk) (= (ps:@ trk id) (ps:@ track id))))))) + (when full-track + (setf (aref *play-queue* (ps:@ *play-queue* length)) full-track)))))) + + (update-queue-display) + (let ((loaded-count (ps:@ *play-queue* length))) + (alert (+ "Loaded " loaded-count " tracks from \"" (ps:@ playlist name) "\" into queue!")) + + ;; Optionally start playing the first track + (when (> loaded-count 0) + (let* ((first-track (aref *play-queue* 0)) + (track-index (ps:chain *tracks* + (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ first-track id))))))) + ;; Remove first track from queue since we're playing it + (ps:chain *play-queue* (shift)) + (update-queue-display) + (when (>= track-index 0) + (play-track track-index)))))) + (alert (+ "Playlist \"" (ps:@ playlist name) "\" is empty")))) + (alert (+ "Error loading playlist: " (or (ps:@ data message) "Unknown error"))))))) + (catch (lambda (error) + (ps:chain console (error "Error loading playlist:" error)) + (alert (+ "Error loading playlist: " (ps:@ error message))))))) + + ;; Stream quality configuration + (defun get-live-stream-config (stream-base-url quality) + (let ((config (ps:create + :aac (ps:create :url (+ stream-base-url "/asteroid.aac") :type "audio/aac" :mount "asteroid.aac") - :mp3 (ps:create + :mp3 (ps:create :url (+ stream-base-url "/asteroid.mp3") :type "audio/mpeg" :mount "asteroid.mp3") - :low (ps:create + :low (ps:create :url (+ stream-base-url "/asteroid-low.mp3") :type "audio/mpeg" :mount "asteroid-low.mp3")))) - (aref config quality))) - - ;; Change live stream quality - (defun change-live-stream-quality () - (let ((stream-base-url (ps:chain (ps:chain document (get-element-by-id "stream-base-url")) value)) - (selector (ps:chain document (get-element-by-id "live-stream-quality"))) - (config (get-live-stream-config - (ps:chain (ps:chain document (get-element-by-id "stream-base-url")) value) - (ps:chain (ps:chain document (get-element-by-id "live-stream-quality")) value)))) - - ;; Update audio player - (let ((audio-element (ps:chain document (get-element-by-id "live-stream-audio"))) - (source-element (ps:chain document (get-element-by-id "live-stream-source"))) - (was-playing (not (ps:chain (ps:chain document (get-element-by-id "live-stream-audio")) paused)))) - - (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))))))))) - - ;; Update now playing information - (defun update-now-playing () - (ps:chain - (fetch "/api/asteroid/partial/now-playing") - (then (lambda (response) - (let ((content-type (ps:chain response headers (get "content-type")))) - (if (ps:chain content-type (includes "text/html")) - (ps:chain response (text)) - (progn - (ps:chain console (log "Error connecting to stream")) - ""))))) - (then (lambda (data) - (setf (ps:chain document (get-element-by-id "now-playing") inner-h-t-m-l) data))) + (aref config quality))) - (catch (lambda (error) - (ps:chain console (log "Could not fetch stream status:" error)))))) - - ;; Initial update after 1 second - (set-timeout update-now-playing 1000) - ;; Update live stream info every 10 seconds - (set-interval update-now-playing 10000) - - ;; Make functions globally accessible for onclick handlers - (defvar window (ps:@ window)) - (setf (ps:@ window play-track) play-track) - (setf (ps:@ window add-to-queue) add-to-queue) - (setf (ps:@ window remove-from-queue) remove-from-queue) - (setf (ps:@ window library-go-to-page) library-go-to-page) - (setf (ps:@ window library-previous-page) library-previous-page) - (setf (ps:@ window library-next-page) library-next-page) - (setf (ps:@ window library-go-to-last-page) library-go-to-last-page) - (setf (ps:@ window change-library-tracks-per-page) change-library-tracks-per-page) - (setf (ps:@ window load-playlist) load-playlist) - (setf (ps:@ window delete-playlist) delete-playlist) - (setf (ps:@ window view-playlist) view-playlist) - (setf (ps:@ window show-add-to-playlist-menu) show-add-to-playlist-menu) - (setf (ps:@ window add-track-to-playlist) add-track-to-playlist))) + ;; Change live stream quality + (defun change-live-stream-quality () + (let ((stream-base-url (ps:chain (ps:chain document (get-element-by-id "stream-base-url")) value)) + (selector (ps:chain document (get-element-by-id "live-stream-quality"))) + (config (get-live-stream-config + (ps:chain (ps:chain document (get-element-by-id "stream-base-url")) value) + (ps:chain (ps:chain document (get-element-by-id "live-stream-quality")) value)))) + + ;; Update audio player + (let ((audio-element (ps:chain document (get-element-by-id "live-stream-audio"))) + (source-element (ps:chain document (get-element-by-id "live-stream-source"))) + (was-playing (not (ps:chain (ps:chain document (get-element-by-id "live-stream-audio")) paused)))) + + (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))))))))) + + ;; Update now playing information + (defun update-now-playing () + (ps:chain + (fetch "/api/asteroid/partial/now-playing") + (then (lambda (response) + (let ((content-type (ps:chain response headers (get "content-type")))) + (if (ps:chain content-type (includes "text/html")) + (ps:chain response (text)) + (progn + (ps:chain console (log "Error connecting to stream")) + ""))))) + (then (lambda (data) + (setf (ps:chain document (get-element-by-id "now-playing") inner-h-t-m-l) data) + (update-media-session))) + + (catch (lambda (error) + (ps:chain console (log "Could not fetch stream status:" error)))))) + + ;; Initial update after 1 second + (set-timeout update-now-playing 1000) + ;; Update live stream info every 10 seconds + (set-interval update-now-playing 10000) + + ;; Make functions globally accessible for onclick handlers + (defvar window (ps:@ window)) + (setf (ps:@ window play-track) play-track) + (setf (ps:@ window add-to-queue) add-to-queue) + (setf (ps:@ window remove-from-queue) remove-from-queue) + (setf (ps:@ window library-go-to-page) library-go-to-page) + (setf (ps:@ window library-previous-page) library-previous-page) + (setf (ps:@ window library-next-page) library-next-page) + (setf (ps:@ window library-go-to-last-page) library-go-to-last-page) + (setf (ps:@ window change-library-tracks-per-page) change-library-tracks-per-page) + (setf (ps:@ window load-playlist) load-playlist) + (setf (ps:@ window delete-playlist) delete-playlist) + (setf (ps:@ window view-playlist) view-playlist) + (setf (ps:@ window show-add-to-playlist-menu) show-add-to-playlist-menu) + (setf (ps:@ window add-track-to-playlist) add-track-to-playlist))) "Compiled JavaScript for web player - generated at load time") (defun generate-player-js () "Generate JavaScript code for the web player" - *player-js*) + (ps-join + *common-player-js* + *player-js*)) diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp index d903ecf..d6a76f6 100644 --- a/parenscript/stream-player.lisp +++ b/parenscript/stream-player.lisp @@ -524,6 +524,7 @@ (setf (ps:@ el text-content) title) ;; Check if this track is in user's favorites (check-favorite-status-mini)) + (update-media-session title) (when track-id-el (let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id)))) (setf (ps:@ track-id-el value) (or track-id "")))) @@ -634,7 +635,8 @@ (when title-el (setf (ps:@ title-el text-content) (ps:chain track-text (trim)))) (when artist-el - (setf (ps:@ artist-el text-content) "Asteroid Radio")))))))) + (setf (ps:@ artist-el text-content) "Asteroid Radio"))))) + (update-media-session track-text)))) (catch (lambda (error) (ps:chain console (error "Error updating now playing:" error))))))) @@ -1082,4 +1084,6 @@ (defun generate-stream-player-js () "Generate JavaScript code for the stream player" - *stream-player-js*) + (ps-join + *common-player-js* + *stream-player-js*)) diff --git a/static/asteroid-squared.png b/static/asteroid-squared.png new file mode 100644 index 0000000000000000000000000000000000000000..88ed11376465477da1c4763546b8525fd4896250 GIT binary patch literal 44369 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 z�RE6vh;u&>HQ>w?24x8G5vAsV4v~GNP*7osaEQv(0RnwTgPR(T;~&u(GAXS}>s> znLUZ&z$7=#1inw;`U!5e3~VFAx6!|EBYMzQRkel@hG~31fnmc5#u!ciI1x&ui&q!+ h*4cx-o_`ku{r^Wsf1}Jx&jbJf002ovPDHLkV1kaIl!5>N literal 0 HcmV?d00001