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:
parent
e712009d79
commit
f594daabf8
|
|
@ -27,7 +27,8 @@
|
||||||
#:update-all-mounts-metadata
|
#:update-all-mounts-metadata
|
||||||
;; DJ support
|
;; DJ support
|
||||||
#:pipeline-harmony-server
|
#:pipeline-harmony-server
|
||||||
#:volume-ramp))
|
#:volume-ramp
|
||||||
|
#:pipeline-stop-all-voices))
|
||||||
|
|
||||||
(in-package #:cl-streamer/harmony)
|
(in-package #:cl-streamer/harmony)
|
||||||
|
|
||||||
|
|
@ -221,6 +222,21 @@
|
||||||
(setf (pipeline-file-queue pipeline) nil))
|
(setf (pipeline-file-queue pipeline) nil))
|
||||||
(log:info "Queue cleared"))
|
(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)
|
(defun pipeline-pop-queue (pipeline)
|
||||||
"Pop the next entry from the file queue (internal use)."
|
"Pop the next entry from the file queue (internal use)."
|
||||||
(bt:with-lock-held ((pipeline-queue-lock pipeline))
|
(bt:with-lock-held ((pipeline-queue-lock pipeline))
|
||||||
|
|
|
||||||
|
|
@ -86,17 +86,20 @@
|
||||||
;;; ---- Auto-Playlist Pause / Resume ----
|
;;; ---- Auto-Playlist Pause / Resume ----
|
||||||
|
|
||||||
(defun pause-auto-playlist ()
|
(defun pause-auto-playlist ()
|
||||||
"Pause the auto-playlist by skipping and clearing the queue.
|
"Pause the auto-playlist by immediately stopping all voices and clearing the queue.
|
||||||
The play-list thread will exit when it has no more tracks.
|
The play-list thread will exit when it sees the empty queue and skip flag.
|
||||||
Returns saved state for restoration."
|
Returns saved state for restoration."
|
||||||
(when *harmony-pipeline*
|
(when *harmony-pipeline*
|
||||||
(let ((state (list :playlist-path (when *current-playlist-path*
|
(let ((state (list :playlist-path (when *current-playlist-path*
|
||||||
(namestring *current-playlist-path*))
|
(namestring *current-playlist-path*))
|
||||||
:current-track (cl-streamer/harmony:pipeline-current-track
|
:current-track (cl-streamer/harmony:pipeline-current-track
|
||||||
*harmony-pipeline*))))
|
*harmony-pipeline*))))
|
||||||
;; Skip current track to stop playback, then clear queue
|
;; 1. Clear the queue so play-list has nothing to advance to
|
||||||
(cl-streamer/harmony:pipeline-skip *harmony-pipeline*)
|
|
||||||
(cl-streamer/harmony:pipeline-clear-queue *harmony-pipeline*)
|
(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")
|
(log:info "Auto-playlist paused for DJ session")
|
||||||
state)))
|
state)))
|
||||||
|
|
||||||
|
|
@ -464,19 +467,14 @@
|
||||||
Returns a list of alists with track info."
|
Returns a list of alists with track info."
|
||||||
(handler-case
|
(handler-case
|
||||||
(with-db
|
(with-db
|
||||||
(let ((results (postmodern:query
|
(let* ((pattern (format nil "%~A%" query))
|
||||||
(:limit
|
(results
|
||||||
(:offset
|
(postmodern:query
|
||||||
(:order-by
|
(:raw (format nil
|
||||||
(:select '_id 'title 'artist 'album 'file-path
|
"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"
|
||||||
:from 'tracks
|
limit offset))
|
||||||
:where (:or (:ilike 'title (format nil "%~A%" query))
|
pattern
|
||||||
(:ilike 'artist (format nil "%~A%" query))
|
:rows)))
|
||||||
(:ilike 'album (format nil "%~A%" query))))
|
|
||||||
'artist 'title)
|
|
||||||
offset)
|
|
||||||
limit)
|
|
||||||
:rows)))
|
|
||||||
(mapcar (lambda (row)
|
(mapcar (lambda (row)
|
||||||
`(("id" . ,(first row))
|
`(("id" . ,(first row))
|
||||||
("title" . ,(or (second row) ""))
|
("title" . ,(or (second row) ""))
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,32 @@
|
||||||
|
|
||||||
(defun get-now-playing-stats (&optional (mount "asteroid.mp3"))
|
(defun get-now-playing-stats (&optional (mount "asteroid.mp3"))
|
||||||
"Get now-playing stats from the Harmony pipeline.
|
"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."
|
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)
|
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
|
||||||
"Get Partial HTML with live now-playing status.
|
"Get Partial HTML with live now-playing status.
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,23 @@
|
||||||
"POST to an API endpoint with form params"
|
"POST to an API endpoint with form params"
|
||||||
(let ((xhr (ps:new (-x-m-l-http-request))))
|
(let ((xhr (ps:new (-x-m-l-http-request))))
|
||||||
(ps:chain xhr (open "POST" url true))
|
(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)
|
(setf (ps:@ xhr onload)
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(let ((data (ps:chain -j-s-o-n (parse (ps:@ xhr response-text)))))
|
(ps:chain console (log "DJ POST" url (ps:@ xhr status) (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)))
|
||||||
|
(show-message "API response error" "error")))))
|
||||||
(setf (ps:@ xhr onerror)
|
(setf (ps:@ xhr onerror)
|
||||||
(lambda () (show-message "Network error" "error")))
|
(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)
|
(defun api-get (url callback)
|
||||||
"GET from an API endpoint"
|
"GET from an API endpoint"
|
||||||
|
|
@ -40,8 +49,12 @@
|
||||||
(ps:chain xhr (open "GET" url true))
|
(ps:chain xhr (open "GET" url true))
|
||||||
(setf (ps:@ xhr onload)
|
(setf (ps:@ xhr onload)
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(let ((data (ps:chain -j-s-o-n (parse (ps:@ xhr response-text)))))
|
(ps:try
|
||||||
(when callback (funcall callback data)))))
|
(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)
|
(setf (ps:@ xhr onerror)
|
||||||
(lambda () (show-message "Network error" "error")))
|
(lambda () (show-message "Network error" "error")))
|
||||||
(ps:chain xhr (send))))
|
(ps:chain xhr (send))))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue