asteroid/frontend-partials.lisp

99 lines
5.3 KiB
Common Lisp

(in-package :asteroid)
(defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3"))
"Fetch now-playing information from Icecast server.
ICECAST-BASE-URL - Base URL of the Icecast server (e.g. http://localhost:8000)
MOUNT - Mount point to fetch metadata from (default: asteroid.mp3)
Returns a plist with :listenurl, :title, and :listeners, or NIL on error."
(let* ((icecast-url (format nil "~a/admin/stats.xml" icecast-base-url))
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
(when response
(let ((xml-string (if (stringp response)
response
(babel:octets-to-string response :encoding :utf-8))))
;; Extract total listener count from root <listeners> tag (sums all mount points)
;; Extract title from specified mount point
(let* ((total-listeners (multiple-value-bind (match groups)
(cl-ppcre:scan-to-strings "<listeners>(\\d+)</listeners>" xml-string)
(if (and match groups)
(parse-integer (aref groups 0) :junk-allowed t)
0)))
;; Escape dots in mount name for regex
(mount-pattern (format nil "<source mount=\"/~a\">"
(cl-ppcre:regex-replace-all "\\." mount "\\\\.")))
(mount-start (cl-ppcre:scan mount-pattern xml-string))
(title (if mount-start
(let* ((source-section (subseq xml-string mount-start
(or (cl-ppcre:scan "</source>" xml-string :start mount-start)
(length xml-string)))))
(multiple-value-bind (match groups)
(cl-ppcre:scan-to-strings "<title>(.*?)</title>" source-section)
(if (and match groups)
(plump:decode-entities (aref groups 0))
"Unknown")))
"Unknown")))
;; Track recently played if title changed
;; Use appropriate last-known-track and list based on stream type
(let* ((is-shuffle (string= mount "asteroid-shuffle.mp3"))
(last-known (if is-shuffle *last-known-track-shuffle* *last-known-track-curated*))
(stream-type (if is-shuffle :shuffle :curated)))
(when (and title
(not (string= title "Unknown"))
(not (equal title last-known)))
(if is-shuffle
(setf *last-known-track-shuffle* title)
(setf *last-known-track-curated* title))
(add-recently-played (list :title title
:timestamp (get-universal-time))
stream-type)))
`((:listenurl . ,(format nil "~a/~a" *stream-base-url* mount))
(:title . ,title)
(:listeners . ,total-listeners)))))))
(define-api asteroid/partial/now-playing (&optional mount) ()
"Get Partial HTML with live status from Icecast server.
Optional MOUNT parameter specifies which stream to get metadata from.
Always polls both streams to keep recently played lists updated."
(with-error-handling
(let* ((mount-name (or mount "asteroid.mp3"))
;; Always poll both streams to keep recently played lists updated
(dummy-curated (when (not (string= mount-name "asteroid.mp3"))
(icecast-now-playing *stream-base-url* "asteroid.mp3")))
(dummy-shuffle (when (not (string= mount-name "asteroid-shuffle.mp3"))
(icecast-now-playing *stream-base-url* "asteroid-shuffle.mp3")))
(now-playing-stats (icecast-now-playing *stream-base-url* mount-name)))
(if now-playing-stats
(progn
;; TODO: it should be able to define a custom api-output for this
;; (api-output <clip-parser> :format "html"))
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(load-template "partial/now-playing")
:stats now-playing-stats))
(progn
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(load-template "partial/now-playing")
:connection-error t
:stats nil))))))
(define-api asteroid/partial/now-playing-inline (&optional mount) ()
"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 (icecast-now-playing *stream-base-url* 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")))))