(in-package :asteroid) (defun find-track-by-title (title) "Find a track in the database by its title. Returns track ID or nil. Handles 'Artist - Title' format from stream metadata." (when (and title (not (string= title "Unknown"))) (handler-case (with-db ;; Parse 'Artist - Title' format if present (let* ((parts (cl-ppcre:split " - " title :limit 2)) (has-artist (> (length parts) 1)) (artist-part (when has-artist (first parts))) (title-part (if has-artist (second parts) title)) (result (if has-artist ;; Search by both artist and title (postmodern:query (:limit (:select '_id :from 'tracks :where (:and (:ilike 'artist (format nil "%~a%" artist-part)) (:ilike 'title (format nil "%~a%" title-part)))) 1) :single) ;; Fallback: search by title only (postmodern:query (:limit (:select '_id :from 'tracks :where (:ilike 'title (format nil "%~a%" title-part))) 1) :single)))) result)) (error (e) (declare (ignore e)) nil)))) (defun get-now-playing-stats (&optional (mount "asteroid.mp3")) "Get now-playing stats from the Harmony pipeline. Returns an alist with :listenurl, :title, :listeners, :track-id, :favorite-count." (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. Optional MOUNT parameter specifies which stream to get metadata from. Returns partial HTML with current track info." (with-error-handling (let* ((mount-name (or mount "asteroid.mp3")) (now-playing-stats (get-now-playing-stats mount-name))) (if now-playing-stats (let* ((title (cdr (assoc :title now-playing-stats))) (favorite-count (or (get-track-favorite-count title) 0))) (setf (header "Content-Type") "text/html") (clip:process-to-string (load-template "partial/now-playing") :stats now-playing-stats :track-id (cdr (assoc :track-id now-playing-stats)) :favorite-count favorite-count)) (progn (setf (header "Content-Type") "text/html") (clip:process-to-string (load-template "partial/now-playing") :connection-error t :stats nil)))))) (define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 10 :timeout 1) "Get inline text with now playing info (for admin dashboard and widgets). Optional MOUNT parameter specifies which stream to get metadata from." (with-error-handling (let* ((mount-name (or mount "asteroid.mp3")) (now-playing-stats (get-now-playing-stats mount-name))) (if now-playing-stats (progn (setf (header "Content-Type") "text/plain") (cdr (assoc :title now-playing-stats))) (progn (setf (header "Content-Type") "text/plain") "Stream Offline"))))) (define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 10 :timeout 1) "Get JSON with now playing info including track ID for favorites. Optional MOUNT parameter specifies which stream to get metadata from." ;; Register web listener for geo stats (keeps listener active during playback) (register-web-listener) (with-error-handling (let* ((mount-name (or mount "asteroid.mp3")) (now-playing-stats (get-now-playing-stats mount-name))) (if now-playing-stats (let* ((title (cdr (assoc :title now-playing-stats))) (favorite-count (or (get-track-favorite-count title) 0)) (parsed (parse-track-title title)) (artist (getf parsed :artist)) (song (getf parsed :song)) (search-url (generate-music-search-url artist song))) (api-output `(("status" . "success") ("title" . ,title) ("listeners" . ,(cdr (assoc :listeners now-playing-stats))) ("track_id" . ,(cdr (assoc :track-id now-playing-stats))) ("favorite_count" . ,favorite-count) ("search_url" . ,search-url)))) (api-output `(("status" . "offline") ("title" . "Stream Offline") ("track_id" . nil))))))) (define-api-with-limit asteroid/channel-name () (:limit 180 :timeout 60) "Get the current curated channel name for live updates. Returns JSON with the channel name from the current playlist's PHASE header." (with-error-handling (api-output `(("channel_name" . ,(get-curated-channel-name))))))