Fix DJ Console: 400 POST error, library search SQL, auto-playlist pause, now-playing override

- Fix api-post: skip Content-Type header on empty-body POSTs (Hunchentoot 400)
- Fix api-post/api-get: unwrap Radiance data wrapper, add try/catch + console logging
- Fix search-library-tracks: use raw SQL with parameterized ILIKE (S-SQL :offset broken)
- Fix search-library-tracks: quote file-path column name for Radiance hyphenated columns
- Add pipeline-stop-all-voices: immediately silence all Harmony voices on mixer
- Fix pause-auto-playlist: clear queue + skip + stop all voices (no more overlap)
- Override get-now-playing-stats during DJ session to show active deck info
This commit is contained in:
Glenn Thompson 2026-03-06 08:06:02 +03:00
parent e712009d79
commit f594daabf8
4 changed files with 76 additions and 25 deletions

View File

@ -27,7 +27,8 @@
#:update-all-mounts-metadata
;; DJ support
#:pipeline-harmony-server
#:volume-ramp))
#:volume-ramp
#:pipeline-stop-all-voices))
(in-package #:cl-streamer/harmony)
@ -221,6 +222,21 @@
(setf (pipeline-file-queue pipeline) nil))
(log:info "Queue cleared"))
(defun pipeline-stop-all-voices (pipeline)
"Immediately stop all active voices on the Harmony mixer.
Used by DJ session to silence the auto-playlist before mixing."
(let ((server (pipeline-harmony-server pipeline)))
(when server
(let ((harmony:*server* server))
(dolist (voice (harmony:voices server))
(handler-case
(progn
(setf (mixed:volume voice) 0.0)
(harmony:stop voice))
(error (e)
(log:debug "Error stopping voice: ~A" e))))
(log:info "All voices stopped on mixer")))))
(defun pipeline-pop-queue (pipeline)
"Pop the next entry from the file queue (internal use)."
(bt:with-lock-held ((pipeline-queue-lock pipeline))

View File

@ -86,17 +86,20 @@
;;; ---- Auto-Playlist Pause / Resume ----
(defun pause-auto-playlist ()
"Pause the auto-playlist by skipping and clearing the queue.
The play-list thread will exit when it has no more tracks.
"Pause the auto-playlist by immediately stopping all voices and clearing the queue.
The play-list thread will exit when it sees the empty queue and skip flag.
Returns saved state for restoration."
(when *harmony-pipeline*
(let ((state (list :playlist-path (when *current-playlist-path*
(namestring *current-playlist-path*))
:current-track (cl-streamer/harmony:pipeline-current-track
*harmony-pipeline*))))
;; Skip current track to stop playback, then clear queue
(cl-streamer/harmony:pipeline-skip *harmony-pipeline*)
;; 1. Clear the queue so play-list has nothing to advance to
(cl-streamer/harmony:pipeline-clear-queue *harmony-pipeline*)
;; 2. Set skip flag so the play-list loop exits its wait
(cl-streamer/harmony:pipeline-skip *harmony-pipeline*)
;; 3. Immediately silence and stop all voices on the mixer
(cl-streamer/harmony:pipeline-stop-all-voices *harmony-pipeline*)
(log:info "Auto-playlist paused for DJ session")
state)))
@ -464,18 +467,13 @@
Returns a list of alists with track info."
(handler-case
(with-db
(let ((results (postmodern:query
(:limit
(:offset
(:order-by
(:select '_id 'title 'artist 'album 'file-path
:from 'tracks
:where (:or (:ilike 'title (format nil "%~A%" query))
(:ilike 'artist (format nil "%~A%" query))
(:ilike 'album (format nil "%~A%" query))))
'artist 'title)
offset)
limit)
(let* ((pattern (format nil "%~A%" query))
(results
(postmodern:query
(:raw (format nil
"SELECT _id, title, artist, album, \"file-path\" FROM tracks WHERE (title ILIKE $1 OR artist ILIKE $1 OR album ILIKE $1) ORDER BY artist, title LIMIT ~A OFFSET ~A"
limit offset))
pattern
:rows)))
(mapcar (lambda (row)
`(("id" . ,(first row))

View File

@ -37,8 +37,32 @@
(defun get-now-playing-stats (&optional (mount "asteroid.mp3"))
"Get now-playing stats from the Harmony pipeline.
When a DJ session is active, returns the DJ deck's current track info instead.
Returns an alist with :listenurl, :title, :listeners, :track-id, :favorite-count."
(harmony-now-playing mount))
(if (dj-session-active-p)
;; DJ session is live — show the active deck's track info
(let* ((status (dj-session-status))
(deck-a (cdr (assoc "deckA" status :test #'string=)))
(deck-b (cdr (assoc "deckB" status :test #'string=)))
(crossfader (or (cdr (assoc "crossfader" status :test #'string=)) 0.5))
;; Pick the dominant deck based on crossfader position
(active-deck (if (<= crossfader 0.5) deck-a deck-b))
(track-info (when active-deck
(cdr (assoc "trackInfo" active-deck :test #'string=))))
(display-title (if track-info
(or (cdr (assoc "displayTitle" track-info :test #'string=))
"DJ Live")
"DJ Live"))
(owner (or (cdr (assoc "owner" status :test #'string=)) "DJ"))
(title (format nil "~A [DJ: ~A]" display-title owner))
(listeners (or (cl-streamer:get-listener-count) 0)))
`((:listenurl . ,(format nil "~A/~A" *stream-base-url* mount))
(:title . ,title)
(:listeners . ,listeners)
(:track-id . nil)
(:favorite-count . 0)))
;; Normal auto-playlist mode
(harmony-now-playing mount)))
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
"Get Partial HTML with live now-playing status.

View File

@ -25,14 +25,23 @@
"POST to an API endpoint with form params"
(let ((xhr (ps:new (-x-m-l-http-request))))
(ps:chain xhr (open "POST" url true))
(ps:chain xhr (set-request-header "Content-Type" "application/x-www-form-urlencoded"))
(when (and params (> (ps:@ params length) 0))
(ps:chain xhr (set-request-header "Content-Type" "application/x-www-form-urlencoded")))
(setf (ps:@ xhr onload)
(lambda ()
(let ((data (ps:chain -j-s-o-n (parse (ps:@ xhr response-text)))))
(when callback (funcall callback data)))))
(ps:chain console (log "DJ POST" url (ps:@ xhr status) (ps:@ xhr response-text)))
(ps:try
(let* ((raw (ps:chain -j-s-o-n (parse (ps:@ xhr response-text))))
(data (or (ps:@ raw data) raw)))
(when callback (funcall callback data)))
(:catch (e)
(ps:chain console (error "DJ API parse error:" e (ps:@ xhr response-text)))
(show-message "API response error" "error")))))
(setf (ps:@ xhr onerror)
(lambda () (show-message "Network error" "error")))
(ps:chain xhr (send params))))
(if (and params (> (ps:@ params length) 0))
(ps:chain xhr (send params))
(ps:chain xhr (send)))))
(defun api-get (url callback)
"GET from an API endpoint"
@ -40,8 +49,12 @@
(ps:chain xhr (open "GET" url true))
(setf (ps:@ xhr onload)
(lambda ()
(let ((data (ps:chain -j-s-o-n (parse (ps:@ xhr response-text)))))
(when callback (funcall callback data)))))
(ps:try
(let* ((raw (ps:chain -j-s-o-n (parse (ps:@ xhr response-text))))
(data (or (ps:@ raw data) raw)))
(when callback (funcall callback data)))
(:catch (e)
(ps:chain console (error "DJ API parse error:" e (ps:@ xhr response-text)))))))
(setf (ps:@ xhr onerror)
(lambda () (show-message "Network error" "error")))
(ps:chain xhr (send))))