diff --git a/cl-streamer/harmony-backend.lisp b/cl-streamer/harmony-backend.lisp index b907f6e..fd0a6f4 100644 --- a/cl-streamer/harmony-backend.lisp +++ b/cl-streamer/harmony-backend.lisp @@ -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)) diff --git a/dj-session.lisp b/dj-session.lisp index 4d30b30..2a458d1 100644 --- a/dj-session.lisp +++ b/dj-session.lisp @@ -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,19 +467,14 @@ 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) - :rows))) + (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)) ("title" . ,(or (second row) "")) diff --git a/frontend-partials.lisp b/frontend-partials.lisp index 07d4803..f30c14d 100644 --- a/frontend-partials.lisp +++ b/frontend-partials.lisp @@ -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. diff --git a/parenscript/dj-console.lisp b/parenscript/dj-console.lisp index 4aa73f0..6ccc761 100644 --- a/parenscript/dj-console.lisp +++ b/parenscript/dj-console.lisp @@ -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))))