/ parenscript / player.lisp
player.lisp
  1  ;;;; player.lisp - ParenScript version of player.js
  2  ;;;; Web Player functionality including audio playback, playlists, queue management, and live streaming
  3  
  4  (in-package #:asteroid)
  5  
  6  (defparameter *player-js*
  7    (ps:ps*
  8     '(progn
  9      
 10      ;; Global variables
 11      (defvar *tracks* (array))
 12      (defvar *current-track* nil)
 13      (defvar *current-track-index* -1)
 14      (defvar *play-queue* (array))
 15      (defvar *is-shuffled* nil)
 16      (defvar *is-repeating* nil)
 17      (defvar *audio-player* nil)
 18      
 19      ;; Pagination variables for track library
 20      (defvar *library-current-page* 1)
 21      (defvar *library-tracks-per-page* 20)
 22      (defvar *filtered-library-tracks* (array))
 23      
 24      ;; Initialize player on page load
 25      (ps:chain document
 26                (add-event-listener
 27                 "DOMContentLoaded"
 28                 (lambda ()
 29                   (setf *audio-player* (ps:chain document (get-element-by-id "audio-player")))
 30                   (redirect-when-frame)
 31                   (load-tracks)
 32                   (load-playlists)
 33                   (setup-event-listeners)
 34                   (update-player-display)
 35                   (update-volume)
 36                   
 37                   ;; Setup live stream with reduced buffering and reconnect logic
 38                   (let ((live-audio (ps:chain document (get-element-by-id "live-stream-audio"))))
 39                     (when live-audio
 40                       ;; Reduce buffer to minimize delay
 41                       (setf (ps:@ live-audio preload) "none")
 42                       
 43                       ;; Add reconnect logic for long pauses
 44                       (let ((pause-timestamp nil)
 45                             (is-reconnecting false)
 46                             (needs-reconnect false)
 47                             (pause-reconnect-threshold 10000))
 48                         
 49                         (ps:chain live-audio
 50                                   (add-event-listener "pause"
 51                                     (lambda ()
 52                                       (setf pause-timestamp (ps:chain |Date| (now)))
 53                                       (ps:chain console (log "Live stream paused at:" pause-timestamp)))))
 54                         
 55                         (ps:chain live-audio
 56                                   (add-event-listener "play"
 57                                     (lambda ()
 58                                       (when (and (not is-reconnecting)
 59                                                  pause-timestamp
 60                                                  (> (- (ps:chain |Date| (now)) pause-timestamp) pause-reconnect-threshold))
 61                                         (setf needs-reconnect true)
 62                                         (ps:chain console (log "Long pause detected, will reconnect when playing starts...")))
 63                                       (setf pause-timestamp nil))))
 64                         
 65                         (ps:chain live-audio
 66                                   (add-event-listener "playing"
 67                                     (lambda ()
 68                                       (when (and needs-reconnect (not is-reconnecting))
 69                                         (setf is-reconnecting true)
 70                                         (setf needs-reconnect false)
 71                                         (ps:chain console (log "Reconnecting live stream after long pause to clear stale buffers..."))
 72                                         
 73                                         (ps:chain live-audio (pause))
 74                                         
 75                                         (when (ps:@ window |resetSpectrumAnalyzer|)
 76                                           (ps:chain window (reset-spectrum-analyzer)))
 77                                         
 78                                         (ps:chain live-audio (load))
 79                                         
 80                                         (set-timeout
 81                                           (lambda ()
 82                                             (ps:chain live-audio (play)
 83                                                       (catch (lambda (err)
 84                                                                (ps:chain console (log "Reconnect play failed:" err)))))
 85                                             
 86                                             (when (ps:@ window |initSpectrumAnalyzer|)
 87                                               (ps:chain window (init-spectrum-analyzer))
 88                                               (ps:chain console (log "Spectrum analyzer reinitialized after reconnect")))
 89                                             
 90                                             (setf is-reconnecting false))
 91                                           200)))))
 92                         )))
 93                   
 94                   ;; Restore user quality preference
 95                   (let ((selector (ps:chain document (get-element-by-id "live-stream-quality")))
 96                         (stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac")))
 97                     (when (and selector (not (= (ps:@ selector value) stream-quality)))
 98                       (setf (ps:@ selector value) stream-quality)
 99                       (ps:chain selector (dispatch-event (ps:new (-Event "change")))))))))
100      
101      ;; Frame redirection logic
102      (defun redirect-when-frame ()
103        (let* ((path (ps:@ window location pathname))
104              (is-frameset-page (not (= (ps:@ window parent) (ps:@ window self))))
105              (is-content-frame (ps:chain path (includes "player-content"))))
106          
107          (when (and is-frameset-page (not is-content-frame))
108            (setf (ps:@ window location href) "/asteroid/player-content"))
109          
110          (when (and (not is-frameset-page) is-content-frame)
111            (setf (ps:@ window location href) "/asteroid/player"))))
112      
113      ;; Setup all event listeners
114      (defun setup-event-listeners ()
115        ;; Search
116        (ps:chain (ps:chain document (get-element-by-id "search-tracks"))
117                  (add-event-listener "input" filter-tracks))
118        
119        ;; Player controls
120        (ps:chain (ps:chain document (get-element-by-id "play-pause-btn"))
121                  (add-event-listener "click" toggle-play-pause))
122        (ps:chain (ps:chain document (get-element-by-id "prev-btn"))
123                  (add-event-listener "click" play-previous))
124        (ps:chain (ps:chain document (get-element-by-id "next-btn"))
125                  (add-event-listener "click" play-next))
126        (ps:chain (ps:chain document (get-element-by-id "shuffle-btn"))
127                  (add-event-listener "click" toggle-shuffle))
128        (ps:chain (ps:chain document (get-element-by-id "repeat-btn"))
129                  (add-event-listener "click" toggle-repeat))
130        
131        ;; Volume control
132        (ps:chain (ps:chain document (get-element-by-id "volume-slider"))
133                  (add-event-listener "input" update-volume))
134        
135        ;; Audio player events
136        (when (and *audio-player* (ps:chain *audio-player* add-event-listener))
137          (ps:chain *audio-player* (add-event-listener "loadedmetadata" update-time-display))
138          (ps:chain *audio-player* (add-event-listener "timeupdate" update-time-display))
139          (ps:chain *audio-player* (add-event-listener "ended" handle-track-end))
140          (ps:chain *audio-player* (add-event-listener "play" (lambda () (update-play-button "⏸️ Pause"))))
141          (ps:chain *audio-player* (add-event-listener "pause" (lambda () (update-play-button "▶️ Play")))))
142        
143        ;; Playlist controls
144        (ps:chain (ps:chain document (get-element-by-id "create-playlist"))
145                  (add-event-listener "click" create-playlist))
146        (ps:chain (ps:chain document (get-element-by-id "clear-queue"))
147                  (add-event-listener "click" clear-queue))
148        (ps:chain (ps:chain document (get-element-by-id "save-queue"))
149                  (add-event-listener "click" save-queue-as-playlist)))
150      
151      ;; Load tracks from API
152      (defun load-tracks ()
153        (ps:chain
154         (ps:chain (fetch "/api/asteroid/tracks"))
155         (then (lambda (response)
156                 (if (ps:@ response ok)
157                     (ps:chain response (json))
158                     (progn
159                       (ps:chain console (error (+ "HTTP " (ps:@ response status))))
160                       (ps:create :status "error" :tracks (array))))))
161         (then (lambda (result)
162                 ;; Handle RADIANCE API wrapper format
163                 (let ((data (or (ps:@ result data) result)))
164                   (if (= (ps:@ data status) "success")
165                       (progn
166                         (setf *tracks* (or (ps:@ data tracks) (array)))
167                         (display-tracks *tracks*))
168                       (progn
169                         (ps:chain console (error "Error loading tracks:" (ps:@ data error)))
170                         (setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l)
171                               "<div class=\"error\">Error loading tracks</div>"))))))
172         (catch (lambda (error)
173                  (ps:chain console (error "Error loading tracks:" error))
174                  (setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l)
175                        "<div class=\"error\">Error loading tracks</div>")))))
176      
177      ;; Display tracks in library
178      (defun display-tracks (track-list)
179        (setf *filtered-library-tracks* track-list)
180        (setf *library-current-page* 1)
181        (render-library-page))
182      
183      ;; Render current library page
184      (defun render-library-page ()
185        (let ((container (ps:chain document (get-element-by-id "track-list")))
186              (pagination-controls (ps:chain document (get-element-by-id "library-pagination-controls"))))
187          
188          (if (= (ps:@ *filtered-library-tracks* length) 0)
189              (progn
190                (setf (ps:@ container inner-h-t-m-l) "<div class=\"no-tracks\">No tracks found</div>")
191                (setf (ps:@ pagination-controls style display) "none")
192                (return)))
193          
194          ;; Calculate pagination
195          (let* ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))
196                (start-index (* (- *library-current-page* 1) *library-tracks-per-page*))
197                (end-index (+ start-index *library-tracks-per-page*))
198                (tracks-to-show (ps:chain *filtered-library-tracks* (slice start-index end-index))))
199            
200            ;; Render tracks for current page
201            (let ((tracks-html (ps:chain tracks-to-show
202                                        (map (lambda (track page-index)
203                                               ;; Find the actual index in the full tracks array
204                                               (let ((actual-index (ps:chain *tracks* 
205                                                                         (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ track id)))))))
206                                                 (+ "<div class=\"track-item\" data-track-id=\"" (ps:@ track id) "\" data-index=\"" actual-index "\">"
207                                                    "<div class=\"track-info\">"
208                                                    "<div class=\"track-title\">" (or (ps:@ track title) "Unknown Title") "</div>"
209                                                    "<div class=\"track-meta\">" (or (ps:@ track artist) "Unknown Artist") " • " (or (ps:@ track album) "Unknown Album") "</div>"
210                                                    "</div>"
211                                                    "<div class=\"track-actions\">"
212                                                    "<button onclick=\"playTrack(" actual-index ")\" class=\"btn btn-sm btn-success\" title=\"Play\">▶️</button>"
213                                                    "<button onclick=\"addToQueue(" actual-index ")\" class=\"btn btn-sm btn-info\" title=\"Add to queue\">➕</button>"
214                                                    "<button onclick=\"showAddToPlaylistMenu(" (ps:@ track id) ", event)\" class=\"btn btn-sm btn-secondary\" title=\"Add to playlist\">📋</button>"
215                                                    "</div>"
216                                                    "</div>"))))
217                                        (join ""))))
218              
219              (setf (ps:@ container inner-h-t-m-l) tracks-html)
220              
221              ;; Update pagination controls
222              (setf (ps:chain (ps:chain document (get-element-by-id "library-page-info")) text-content)
223                    (+ "Page " *library-current-page* " of " total-pages " (" (ps:@ *filtered-library-tracks* length) " tracks)"))
224              (setf (ps:@ pagination-controls style display) 
225                    (if (> total-pages 1) "block" "none"))))))
226      
227      ;; Library pagination functions
228      (defun library-go-to-page (page)
229        (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*))))
230          (when (and (>= page 1) (<= page total-pages))
231            (setf *library-current-page* page)
232            (render-library-page))))
233      
234      (defun library-previous-page ()
235        (when (> *library-current-page* 1)
236          (setf *library-current-page* (- *library-current-page* 1))
237          (render-library-page)))
238      
239      (defun library-next-page ()
240        (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*))))
241          (when (< *library-current-page* total-pages)
242            (setf *library-current-page* (+ *library-current-page* 1))
243            (render-library-page))))
244      
245      (defun library-go-to-last-page ()
246        (let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*))))
247          (setf *library-current-page* total-pages)
248          (render-library-page)))
249      
250      (defun change-library-tracks-per-page ()
251        (setf *library-tracks-per-page* 
252              (parse-int (ps:chain (ps:chain document (get-element-by-id "library-tracks-per-page")) value)))
253        (setf *library-current-page* 1)
254        (render-library-page))
255      
256      ;; Filter tracks based on search query
257      (defun filter-tracks ()
258        (let ((query (ps:chain (ps:chain document (get-element-by-id "search-tracks")) value (to-lower-case))))
259          (let ((filtered (ps:chain *tracks* 
260                                     (filter (lambda (track)
261                                               (or (ps:chain (or (ps:@ track title) "") (to-lower-case) (includes query))
262                                                   (ps:chain (or (ps:@ track artist) "") (to-lower-case) (includes query))
263                                                   (ps:chain (or (ps:@ track album) "") (to-lower-case) (includes query))))))))
264            (display-tracks filtered))))
265      
266      ;; Play a specific track by index
267      (defun play-track (index)
268        (when (and (>= index 0) (< index (ps:@ *tracks* length)))
269          (setf *current-track* (aref *tracks* index))
270          (setf *current-track-index* index)
271          
272          ;; Load track into audio player
273          (setf (ps:@ *audio-player* src) (+ "/asteroid/tracks/" (ps:@ *current-track* id) "/stream"))
274          (ps:chain *audio-player* (load))
275          (ps:chain *audio-player* 
276                    (play)
277                    (catch (lambda (error)
278                             (ps:chain console (error "Playback error:" error))
279                             (alert "Error playing track. The track may not be available."))))
280          
281          (update-player-display)
282          
283          ;; Update server-side player state
284          (ps:chain (fetch (+ "/api/asteroid/player/play?track-id=" (ps:@ *current-track* id))
285                           (ps:create :method "POST"))
286                    (catch (lambda (error)
287                             (ps:chain console (error "API update error:" error)))))))
288      
289      ;; Toggle play/pause
290      (defun toggle-play-pause ()
291        (if *current-track*
292            (if (ps:@ *audio-player* paused)
293                (ps:chain *audio-player* (play))
294                (ps:chain *audio-player* (pause)))
295            (alert "Please select a track to play")))
296      
297      ;; Play previous track
298      (defun play-previous ()
299        (if (> (ps:@ *play-queue* length) 0)
300            ;; Play from queue
301            (let ((prev-index (max 0 (- *current-track-index* 1))))
302              (play-track prev-index))
303            ;; Play previous track in library
304            (let ((prev-index (if (> *current-track-index* 0) 
305                                  (- *current-track-index* 1) 
306                                  (- (ps:@ *tracks* length) 1))))
307              (play-track prev-index))))
308      
309      ;; Play next track
310      (defun play-next ()
311        (if (> (ps:@ *play-queue* length) 0)
312            ;; Play from queue
313            (let ((next-track (ps:chain *play-queue* (shift))))
314              (play-track (ps:chain *tracks* 
315                                    (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ next-track id))))))
316              (update-queue-display))
317            ;; Play next track in library
318            (let ((next-index (if *is-shuffled*
319                                  (floor (* (random) (ps:@ *tracks* length)))
320                                  (mod (+ *current-track-index* 1) (ps:@ *tracks* length)))))
321              (play-track next-index))))
322      
323      ;; Handle track end
324      (defun handle-track-end ()
325        (if *is-repeating*
326            (progn
327              (setf (ps:@ *audio-player* current-time) 0)
328              (ps:chain *audio-player* (play)))
329            (play-next)))
330      
331      ;; Toggle shuffle mode
332      (defun toggle-shuffle ()
333        (setf *is-shuffled* (not *is-shuffled*))
334        (let ((btn (ps:chain document (get-element-by-id "shuffle-btn"))))
335          (setf (ps:@ btn text-content) (if *is-shuffled* "🔀 Shuffle ON" "🔀 Shuffle"))
336          (ps:chain btn (class-list toggle "active" *is-shuffled*))))
337      
338      ;; Toggle repeat mode
339      (defun toggle-repeat ()
340        (setf *is-repeating* (not *is-repeating*))
341        (let ((btn (ps:chain document (get-element-by-id "repeat-btn"))))
342          (setf (ps:@ btn text-content) (if *is-repeating* "🔁 Repeat ON" "🔁 Repeat"))
343          (ps:chain btn (class-list toggle "active" *is-repeating*))))
344      
345      ;; Update volume
346      (defun update-volume ()
347        (let ((volume (/ (parse-int (ps:chain (ps:chain document (get-element-by-id "volume-slider")) value)) 100)))
348          (when *audio-player*
349            (setf (ps:@ *audio-player* volume) volume))))
350      
351      ;; Update time display
352      (defun update-time-display ()
353        (let ((current (format-time (ps:@ *audio-player* current-time)))
354              (total (format-time (ps:@ *audio-player* duration))))
355          (setf (ps:chain (ps:chain document (get-element-by-id "current-time")) text-content) current)
356          (setf (ps:chain (ps:chain document (get-element-by-id "total-time")) text-content) total)))
357      
358      ;; Format time helper
359      (defun format-time (seconds)
360        (if (isNaN seconds)
361            "0:00"
362            (let ((mins (floor (/ seconds 60)))
363                  (secs (floor (mod seconds 60))))
364              (+ mins ":" (ps:chain secs (to-string)  (pad-start 2 "0"))))))
365      
366      ;; Update play button text
367      (defun update-play-button (text)
368        (setf (ps:chain (ps:chain document (get-element-by-id "play-pause-btn")) text-content) text))
369      
370      ;; Update player display with current track info
371      (defun update-player-display ()
372        (when *current-track*
373          (setf (ps:chain (ps:chain document (get-element-by-id "current-title")) text-content)
374                (or (ps:@ *current-track* title) "Unknown Title"))
375          (setf (ps:chain (ps:chain document (get-element-by-id "current-artist")) text-content)
376                (or (ps:@ *current-track* artist) "Unknown Artist"))
377          (setf (ps:chain (ps:chain document (get-element-by-id "current-album")) text-content)
378                (or (ps:@ *current-track* album) "Unknown Album"))))
379      
380      ;; Add track to queue
381      (defun add-to-queue (index)
382        (when (and (>= index 0) (< index (ps:@ *tracks* length)))
383          (setf (aref *play-queue* (ps:@ *play-queue* length)) (aref *tracks* index))
384          (update-queue-display)))
385      
386      ;; Update queue display
387      (defun update-queue-display ()
388        (let ((container (ps:chain document (get-element-by-id "play-queue"))))
389          (if (= (ps:@ *play-queue* length) 0)
390              (setf (ps:@ container inner-h-t-m-l) "<div class=\"empty-queue\">Queue is empty</div>")
391              (let ((queue-html (ps:chain *play-queue*
392                                           (map (lambda (track index)
393                                                  (+ "<div class=\"queue-item\">"
394                                                     "<div class=\"track-info\">"
395                                                     "<div class=\"track-title\">" (or (ps:@ track title) "Unknown Title") "</div>"
396                                                     "<div class=\"track-meta\">" (or (ps:@ track artist) "Unknown Artist") "</div>"
397                                                     "</div>"
398                                                     "<button onclick=\"removeFromQueue(" index ")\" class=\"btn btn-sm btn-danger\">✖️</button>"
399                                                     "</div>")))
400                                           (join ""))))
401                (setf (ps:@ container inner-h-t-m-l) queue-html)))))
402      
403      ;; Remove track from queue
404      (defun remove-from-queue (index)
405        (ps:chain *play-queue* (splice index 1))
406        (update-queue-display))
407      
408      ;; Clear queue
409      (defun clear-queue ()
410        (setf *play-queue* (array))
411        (update-queue-display))
412      
413      ;; Store playlists for the add-to-playlist menu
414      (defvar *user-playlists* (array))
415      
416      ;; Show add to playlist dropdown menu
417      (defun show-add-to-playlist-menu (track-id event)
418        (ps:chain event (stop-propagation))
419        ;; Remove any existing menu
420        (let ((existing-menu (ps:chain document (get-element-by-id "playlist-dropdown-menu"))))
421          (when existing-menu
422            (ps:chain existing-menu (remove))))
423        
424        ;; Fetch playlists and show menu
425        (ps:chain (fetch "/api/asteroid/playlists")
426                  (then (lambda (response) (ps:chain response (json))))
427                  (then (lambda (result)
428                          (let* ((data (or (ps:@ result data) result))
429                                 (playlists (or (ps:@ data playlists) (array)))
430                                 (menu (ps:chain document (create-element "div"))))
431                            (setf *user-playlists* playlists)
432                            (setf (ps:@ menu id) "playlist-dropdown-menu")
433                            (setf (ps:@ menu class-name) "playlist-dropdown-menu")
434                            (setf (ps:@ menu style position) "fixed")
435                            (setf (ps:@ menu style left) (+ (ps:@ event client-x) "px"))
436                            (setf (ps:@ menu style top) (+ (ps:@ event client-y) "px"))
437                            (setf (ps:@ menu style z-index) "1000")
438                            (setf (ps:@ menu style background) "#1a1a2e")
439                            (setf (ps:@ menu style border) "1px solid #00ff00")
440                            (setf (ps:@ menu style border-radius) "4px")
441                            (setf (ps:@ menu style padding) "5px 0")
442                            (setf (ps:@ menu style min-width) "150px")
443                            
444                            (if (= (ps:@ playlists length) 0)
445                                (setf (ps:@ menu inner-h-t-m-l) 
446                                      "<div style=\"padding: 8px 12px; color: #888;\">No playlists yet</div>")
447                                (setf (ps:@ menu inner-h-t-m-l)
448                                      (ps:chain playlists
449                                                (map (lambda (playlist)
450                                                       (+ "<div class=\"playlist-menu-item\" onclick=\"addTrackToPlaylist(" 
451                                                          (ps:@ playlist id) ", " track-id 
452                                                          ")\" style=\"padding: 8px 12px; cursor: pointer; color: #00ff00;\" "
453                                                          "onmouseover=\"this.style.background='#2a2a4e'\" "
454                                                          "onmouseout=\"this.style.background='transparent'\">"
455                                                          (ps:@ playlist name) " (" (ps:@ playlist "track-count") ")"
456                                                          "</div>")))
457                                                (join ""))))
458                            
459                            (ps:chain document body (append-child menu))
460                            
461                            ;; Close menu when clicking elsewhere
462                            (let ((close-handler (lambda (e)
463                                                   (when (not (ps:chain menu (contains (ps:@ e target))))
464                                                     (ps:chain menu (remove))
465                                                     (ps:chain document (remove-event-listener "click" close-handler))))))
466                              (set-timeout (lambda ()
467                                             (ps:chain document (add-event-listener "click" close-handler)))
468                                           100)))))
469                  (catch (lambda (error)
470                           (ps:chain console (error "Error loading playlists for menu:" error))))))
471      
472      ;; Add track to a specific playlist
473      (defun add-track-to-playlist (playlist-id track-id)
474        ;; Close the menu
475        (let ((menu (ps:chain document (get-element-by-id "playlist-dropdown-menu"))))
476          (when menu (ps:chain menu (remove))))
477        
478        (let ((form-data (ps:new (-Form-data))))
479          (ps:chain form-data (append "playlist-id" playlist-id))
480          (ps:chain form-data (append "track-id" track-id))
481          (ps:chain (fetch "/api/asteroid/playlists/add-track"
482                           (ps:create :method "POST" :body form-data))
483                    (then (lambda (response) (ps:chain response (json))))
484                    (then (lambda (result)
485                            (let ((data (or (ps:@ result data) result)))
486                              (if (= (ps:@ data status) "success")
487                                  (progn
488                                    ;; Find playlist name for feedback
489                                    (let ((playlist (ps:chain *user-playlists* 
490                                                              (find (lambda (p) (= (ps:@ p id) playlist-id))))))
491                                      (alert (+ "Track added to \"" (if playlist (ps:@ playlist name) "playlist") "\"")))
492                                    (load-playlists))
493                                  (alert (+ "Error: " (ps:@ data message)))))))
494                    (catch (lambda (error)
495                             (ps:chain console (error "Error adding track to playlist:" error))
496                             (alert "Error adding track to playlist"))))))
497      
498      ;; Create playlist
499      (defun create-playlist ()
500        (let ((name (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value (trim))))
501          (when (not (= name ""))
502            (let ((form-data (ps:new (-Form-data))))
503              (ps:chain form-data (append "name" name))
504              (ps:chain form-data (append "description" ""))
505              
506              (ps:chain (fetch "/api/asteroid/playlists/create"
507                               (ps:create :method "POST" :body form-data))
508                        (then (lambda (response)
509                                (ps:chain response (json))))
510                        (then (lambda (result)
511                                ;; Handle RADIANCE API wrapper format
512                                (let ((data (or (ps:@ result data) result)))
513                                  (if (= (ps:@ data status) "success")
514                                      (progn
515                                        (alert (+ "Playlist \"" name "\" created successfully!"))
516                                        (setf (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value) "")
517                                        
518                                        ;; Wait a moment then reload playlists
519                                        (set-timeout load-playlists 500))
520                                      (alert (+ "Error creating playlist: " (ps:@ data message)))))))
521                        (catch (lambda (error)
522                                 (ps:chain console (error "Error creating playlist:" error))
523                                 (alert (+ "Error creating playlist: " (ps:@ error message))))))))))
524      
525      ;; Save queue as playlist
526      (defun save-queue-as-playlist ()
527        (if (> (ps:@ *play-queue* length) 0)
528            (let ((name (prompt "Enter playlist name:")))
529              (when name
530                ;; Create the playlist
531                (let ((form-data (ps:new (-Form-data))))
532                  (ps:chain form-data (append "name" name))
533                  (ps:chain form-data (append "description" (+ "Created from queue with " (ps:@ *play-queue* length) " tracks")))
534                  
535                  (ps:chain (fetch "/api/asteroid/playlists/create"
536                                   (ps:create :method "POST" :body form-data))
537                            (then (lambda (response) (ps:chain response (json))))
538                            (then (lambda (create-result)
539                                    ;; Handle RADIANCE API wrapper format
540                                    (let ((create-data (or (ps:@ create-result data) create-result)))
541                                      (if (= (ps:@ create-data status) "success")
542                                          (progn
543                                            ;; Wait a moment for database to update, then fetch playlists
544                                            (set-timeout 
545                                             (lambda ()
546                                               ;; Get the new playlist ID by fetching playlists
547                                               (ps:chain (fetch "/api/asteroid/playlists")
548                                                         (then (lambda (response) (ps:chain response (json))))
549                                                         (then (lambda (playlists-result)
550                                                                 ;; Handle RADIANCE API wrapper format
551                                                                 (let ((playlist-result-data (or (ps:@ playlists-result data) playlists-result)))
552                                                                   (if (and (= (ps:@ playlist-result-data status) "success")
553                                                                            (> (ps:@ playlist-result-data playlists length) 0))
554                                                                       (progn
555                                                                         ;; Find the playlist with matching name (most recent)
556                                                                         (let ((new-playlist (or (ps:chain (ps:@ playlist-result-data playlists)
557                                                                                                           (find (lambda (p) (= (ps:@ p name) name))))
558                                                                                                 (aref (ps:@ playlist-result-data playlists)
559                                                                                                       (- (ps:@ playlist-result-data playlists length) 1)))))
560                                                                           
561                                                                           ;; Add all tracks from queue to playlist
562                                                                           (let ((added-count 0))
563                                                                             (ps:chain *play-queue*
564                                                                                       (for-each (lambda (track)
565                                                                                                   (let ((track-id (ps:@ track id)))
566                                                                                                     (when track-id
567                                                                                                       (let ((add-form-data (ps:new (-Form-data))))
568                                                                                                         (ps:chain add-form-data (append "playlist-id" (ps:@ new-playlist id)))
569                                                                                                         (ps:chain add-form-data (append "track-id" track-id))
570                                                                                                         
571                                                                                                         (ps:chain (fetch "/api/asteroid/playlists/add-track"
572                                                                                                                          (ps:create :method "POST" :body add-form-data))
573                                                                                                                   (then (lambda (response) (ps:chain response (json))))
574                                                                                                                   (then (lambda (add-result)
575                                                                                                                           (when (= (ps:@ add-result data status) "success")
576                                                                                                                             (setf added-count (+ added-count 1)))))
577                                                                                                                   (catch (lambda (err)
578                                                                                                                            (ps:chain console (log "Error adding track:" err)))))))))))
579                                                                             
580                                                                             (alert (+ "Playlist \"" name "\" created with " added-count " tracks!"))
581                                                                             (load-playlists))))
582                                                                       (progn
583                                                                         (alert (+ "Playlist created but could not add tracks. Error: "
584                                                                                   (or (ps:@ playlist-result-data message) "Unknown")))
585                                                                         (load-playlists))))))
586                                                         (catch (lambda (error)
587                                                                  (ps:chain console (error "Error fetching playlists:" error))
588                                                                  (alert "Playlist created but could not add tracks")))))
589                                             500))
590                                          (alert (+ "Error creating playlist: " (ps:@ create-data message)))))))
591                            (catch (lambda (error)
592                                     (ps:chain console (error "Error saving queue as playlist:" error))
593                                     (alert (+ "Error saving queue as playlist: " (ps:@ error message)))))))))
594            (alert "Queue is empty")))
595      
596      ;; Load playlists from API
597      (defun load-playlists ()
598        (ps:chain
599         (fetch "/api/asteroid/playlists")
600         (then (lambda (response) (ps:chain response (json))))
601         (then (lambda (result)
602                 (ps:chain console (log "Playlists API result:" result))
603                 (let ((playlists (cond
604                                    ((and (ps:@ result data) (= (ps:@ result data status) "success"))
605                                     (ps:chain console (log "Found playlists in result.data.playlists"))
606                                     (or (ps:@ result data playlists) (array)))
607                                    ((= (ps:@ result status) "success")
608                                     (ps:chain console (log "Found playlists in result.playlists"))
609                                     (or (ps:@ result playlists) (array)))
610                                    (t
611                                     (ps:chain console (log "No playlists found in response"))
612                                     (array)))))
613                   (ps:chain console (log "Playlists to display:" playlists))
614                   (display-playlists playlists))))
615         (catch (lambda (error)
616                  (ps:chain console (error "Error loading playlists:" error))
617                  (display-playlists (array))))))
618      
619      ;; Display playlists
620      (defun display-playlists (playlists)
621        (let ((container (ps:chain document (get-element-by-id "playlists-container"))))
622          
623          (if (or (not playlists) (= (ps:@ playlists length) 0))
624              (setf (ps:@ container inner-h-t-m-l) "<div class=\"no-playlists\">No playlists created yet.</div>")
625              (let ((playlists-html (ps:chain playlists
626                                          (map (lambda (playlist)
627                                                 (+ "<div class=\"playlist-item\" data-playlist-id=\"" (ps:@ playlist id) "\">"
628                                                    "<div class=\"playlist-info\">"
629                                                    "<div class=\"playlist-name\">" (ps:@ playlist name) "</div>"
630                                                    "<div class=\"playlist-meta\">" (ps:@ playlist "track-count") " tracks</div>"
631                                                    "</div>"
632                                                    "<div class=\"playlist-actions\">"
633                                                    "<button onclick=\"viewPlaylist(" (ps:@ playlist id) ")\" class=\"btn btn-sm btn-secondary\" title=\"View tracks\">👁️</button>"
634                                                    "<button onclick=\"loadPlaylist(" (ps:@ playlist id) ")\" class=\"btn btn-sm btn-info\" title=\"Load to queue\">📂</button>"
635                                                    "<button onclick=\"deletePlaylist(" (ps:@ playlist id) ", '" (ps:chain (ps:@ playlist name) (replace (ps:regex "/'/g") "\\\\'")) "')\" class=\"btn btn-sm btn-danger\" title=\"Delete playlist\">🗑️</button>"
636                                                    "</div>"
637                                                    "</div>")))
638                                          (join ""))))
639            
640                (setf (ps:@ container inner-h-t-m-l) playlists-html)))))
641      
642      ;; Delete playlist
643      (defun delete-playlist (playlist-id playlist-name)
644        (when (confirm (+ "Are you sure you want to delete playlist \"" playlist-name "\"?"))
645          (let ((form-data (ps:new (-Form-data))))
646            (ps:chain form-data (append "playlist-id" playlist-id))
647            (ps:chain (fetch "/api/asteroid/playlists/delete"
648                             (ps:create :method "POST" :body form-data))
649                      (then (lambda (response) (ps:chain response (json))))
650                      (then (lambda (result)
651                              (let ((data (or (ps:@ result data) result)))
652                                (if (= (ps:@ data status) "success")
653                                    (progn
654                                      (alert (+ "Playlist \"" playlist-name "\" deleted"))
655                                      (load-playlists))
656                                    (alert (+ "Error deleting playlist: " (ps:@ data message)))))))
657                      (catch (lambda (error)
658                               (ps:chain console (error "Error deleting playlist:" error))
659                               (alert "Error deleting playlist")))))))
660      
661      ;; View playlist contents
662      (defun view-playlist (playlist-id)
663        (ps:chain
664         (fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id))
665         (then (lambda (response) (ps:chain response (json))))
666         (then (lambda (result)
667                 (let ((data (or (ps:@ result data) result)))
668                   (if (and (= (ps:@ data status) "success") (ps:@ data playlist))
669                       (let* ((playlist (ps:@ data playlist))
670                              (tracks (or (ps:@ playlist tracks) (array)))
671                              (track-list (if (> (ps:@ tracks length) 0)
672                                              (ps:chain tracks
673                                                        (map (lambda (track index)
674                                                               (+ (+ index 1) ". " 
675                                                                  (or (ps:@ track artist) "Unknown") " - " 
676                                                                  (or (ps:@ track title) "Unknown"))))
677                                                        (join "\\n"))
678                                              "No tracks in playlist")))
679                         (alert (+ "Playlist: " (ps:@ playlist name) "\\n"
680                                   "Tracks: " (ps:@ playlist "track-count") "\\n\\n"
681                                   track-list)))
682                       (alert "Could not load playlist")))))
683         (catch (lambda (error)
684                  (ps:chain console (error "Error viewing playlist:" error))
685                  (alert "Error viewing playlist")))))
686      
687      ;; Load playlist into queue
688      (defun load-playlist (playlist-id)
689        (ps:chain
690         (ps:chain (fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id)))
691         (then (lambda (response) (ps:chain response (json))))
692         (then (lambda (result)
693                 ;; Handle RADIANCE API wrapper format
694                 (let ((data (or (ps:@ result data) result)))
695                   (if (and (= (ps:@ data status) "success") (ps:@ data playlist))
696                       (let ((playlist (ps:@ data playlist)))
697                         
698                         ;; Clear current queue
699                         (setf *play-queue* (array))
700                         
701                         ;; Add all playlist tracks to queue
702                         (if (and (ps:@ playlist tracks) (> (ps:@ playlist tracks length) 0))
703                             (progn
704                               (ps:chain (ps:@ playlist tracks)
705                                         (for-each (lambda (track)
706                                                     ;; Find the full track object from our tracks array
707                                                     (let ((full-track (ps:chain *tracks* 
708                                                                               (find (lambda (trk) (= (ps:@ trk id) (ps:@ track id)))))))
709                                                       (when full-track
710                                                         (setf (aref *play-queue* (ps:@ *play-queue* length)) full-track))))))
711                               
712                               (update-queue-display)
713                               (let ((loaded-count (ps:@ *play-queue* length)))
714                                 (alert (+ "Loaded " loaded-count " tracks from \"" (ps:@ playlist name) "\" into queue!"))
715                                 
716                                 ;; Optionally start playing the first track
717                                 (when (> loaded-count 0)
718                                   (let* ((first-track (aref *play-queue* 0))
719                                          (track-index (ps:chain *tracks* 
720                                                                 (find-index (lambda (trk) (= (ps:@ trk id) (ps:@ first-track id)))))))
721                                     ;; Remove first track from queue since we're playing it
722                                     (ps:chain *play-queue* (shift))
723                                     (update-queue-display)
724                                     (when (>= track-index 0)
725                                       (play-track track-index))))))
726                             (alert (+ "Playlist \"" (ps:@ playlist name) "\" is empty"))))
727                       (alert (+ "Error loading playlist: " (or (ps:@ data message) "Unknown error")))))))
728         (catch (lambda (error)
729                  (ps:chain console (error "Error loading playlist:" error))
730                  (alert (+ "Error loading playlist: " (ps:@ error message)))))))
731      
732      ;; Stream quality configuration
733      (defun get-live-stream-config (stream-base-url quality)
734        (let ((config (ps:create 
735                        :aac (ps:create 
736                               :url (+ stream-base-url "/asteroid.aac")
737                               :type "audio/aac"
738                               :mount "asteroid.aac")
739                        :mp3 (ps:create 
740                               :url (+ stream-base-url "/asteroid.mp3")
741                               :type "audio/mpeg"
742                               :mount "asteroid.mp3")
743                        :low (ps:create 
744                               :url (+ stream-base-url "/asteroid-low.mp3")
745                               :type "audio/mpeg"
746                               :mount "asteroid-low.mp3"))))
747          (aref config quality)))
748      
749      ;; Change live stream quality
750      (defun change-live-stream-quality ()
751        (let ((stream-base-url (ps:chain (ps:chain document (get-element-by-id "stream-base-url")) value))
752              (selector (ps:chain document (get-element-by-id "live-stream-quality")))
753              (config (get-live-stream-config 
754                       (ps:chain (ps:chain document (get-element-by-id "stream-base-url")) value)
755                       (ps:chain (ps:chain document (get-element-by-id "live-stream-quality")) value))))
756          
757          ;; Update audio player
758          (let ((audio-element (ps:chain document (get-element-by-id "live-stream-audio")))
759                (source-element (ps:chain document (get-element-by-id "live-stream-source")))
760                (was-playing (not (ps:chain (ps:chain document (get-element-by-id "live-stream-audio")) paused))))
761            
762            (setf (ps:@ source-element src) (ps:@ config url))
763            (setf (ps:@ source-element type) (ps:@ config type))
764            (ps:chain audio-element (load))
765            
766            ;; Resume playback if it was playing
767            (when was-playing
768              (ps:chain audio-element 
769                        (play)
770                        (catch (lambda (e) (ps:chain console (log "Autoplay prevented:" e)))))))))
771      
772      ;; Update now playing information
773      (defun update-now-playing ()
774        (ps:chain
775         (fetch "/api/asteroid/partial/now-playing")
776         (then (lambda (response)
777                 (let ((content-type (ps:chain response headers (get "content-type"))))
778                   (if (ps:chain content-type (includes "text/html"))
779                       (ps:chain response (text))
780                       (progn
781                         (ps:chain console (log "Error connecting to stream"))
782                         "")))))
783         (then (lambda (data)
784                 (setf (ps:chain document (get-element-by-id "now-playing") inner-h-t-m-l) data)))
785  
786         (catch (lambda (error)
787                  (ps:chain console (log "Could not fetch stream status:" error))))))
788      
789      ;; Initial update after 1 second
790      (set-timeout update-now-playing 1000)
791      ;; Update live stream info every 10 seconds
792      (set-interval update-now-playing 10000)
793      
794      ;; Make functions globally accessible for onclick handlers
795      (defvar window (ps:@ window))
796      (setf (ps:@ window play-track) play-track)
797      (setf (ps:@ window add-to-queue) add-to-queue)
798      (setf (ps:@ window remove-from-queue) remove-from-queue)
799      (setf (ps:@ window library-go-to-page) library-go-to-page)
800      (setf (ps:@ window library-previous-page) library-previous-page)
801      (setf (ps:@ window library-next-page) library-next-page)
802      (setf (ps:@ window library-go-to-last-page) library-go-to-last-page)
803      (setf (ps:@ window change-library-tracks-per-page) change-library-tracks-per-page)
804      (setf (ps:@ window load-playlist) load-playlist)
805      (setf (ps:@ window delete-playlist) delete-playlist)
806      (setf (ps:@ window view-playlist) view-playlist)
807      (setf (ps:@ window show-add-to-playlist-menu) show-add-to-playlist-menu)
808      (setf (ps:@ window add-track-to-playlist) add-track-to-playlist)))
809    "Compiled JavaScript for web player - generated at load time")
810  
811  (defun generate-player-js ()
812    "Generate JavaScript code for the web player"
813    *player-js*)