;;;; player.lisp - ParenScript version of player.js
;;;; Web Player functionality including audio playback, playlists, queue management, and live streaming
(in-package #:asteroid)
(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
(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) 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))
(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)
(+ "")))
(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))
(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)))))))
(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 "\"?"))
(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
:url (+ stream-base-url "/asteroid.mp3")
:type "audio/mpeg"
:mount "asteroid.mp3")
: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)))
(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*)