180 lines
7.1 KiB
Common Lisp
180 lines
7.1 KiB
Common Lisp
;;;; stream-control.lisp - Stream Queue and Playlist Control for Asteroid Radio
|
|
;;;; Manages the main broadcast stream queue and generates M3U playlists for Liquidsoap
|
|
|
|
(in-package :asteroid)
|
|
|
|
;;; Stream Queue Management
|
|
;;; The stream queue represents what will play on the main broadcast
|
|
|
|
(defvar *stream-queue* '() "List of track IDs queued for streaming")
|
|
(defvar *stream-history* '() "List of recently played track IDs")
|
|
(defvar *max-history-size* 50 "Maximum number of tracks to keep in history")
|
|
|
|
(defun get-stream-queue ()
|
|
"Get the current stream queue"
|
|
*stream-queue*)
|
|
|
|
(defun add-to-stream-queue (track-id &optional (position :end))
|
|
"Add a track to the stream queue at specified position (:end or :next)"
|
|
(case position
|
|
(:next (push track-id *stream-queue*))
|
|
(:end (setf *stream-queue* (append *stream-queue* (list track-id))))
|
|
(t (error "Position must be :next or :end")))
|
|
(regenerate-stream-playlist)
|
|
t)
|
|
|
|
(defun remove-from-stream-queue (track-id)
|
|
"Remove a track from the stream queue"
|
|
(setf *stream-queue* (remove track-id *stream-queue* :test #'equal))
|
|
(regenerate-stream-playlist)
|
|
t)
|
|
|
|
(defun clear-stream-queue ()
|
|
"Clear the entire stream queue"
|
|
(setf *stream-queue* '())
|
|
(regenerate-stream-playlist)
|
|
t)
|
|
|
|
(defun reorder-stream-queue (track-ids)
|
|
"Reorder the stream queue with a new list of track IDs"
|
|
(setf *stream-queue* track-ids)
|
|
(regenerate-stream-playlist)
|
|
t)
|
|
|
|
(defun add-playlist-to-stream-queue (playlist-id)
|
|
"Add all tracks from a playlist to the stream queue"
|
|
(let ((playlist (get-playlist-by-id playlist-id)))
|
|
(when playlist
|
|
(let* ((track-ids-raw (gethash "track-ids" playlist))
|
|
(track-ids-str (if (listp track-ids-raw)
|
|
(first track-ids-raw)
|
|
track-ids-raw))
|
|
(track-ids (if (and track-ids-str
|
|
(stringp track-ids-str)
|
|
(not (string= track-ids-str "")))
|
|
(mapcar #'parse-integer
|
|
(cl-ppcre:split "," track-ids-str))
|
|
nil)))
|
|
(dolist (track-id track-ids)
|
|
(add-to-stream-queue track-id :end))
|
|
t))))
|
|
|
|
;;; M3U Playlist Generation
|
|
|
|
(defun get-track-file-path (track-id)
|
|
"Get the file path for a track by ID"
|
|
(let ((track (get-track-by-id track-id)))
|
|
(when track
|
|
(let ((file-path (gethash "file-path" track)))
|
|
(if (listp file-path)
|
|
(first file-path)
|
|
file-path)))))
|
|
|
|
(defun convert-to-docker-path (host-path)
|
|
"Convert host file path to Docker container path"
|
|
;; Replace /home/glenn/Projects/Code/asteroid/music/library/ with /app/music/
|
|
(let ((library-prefix "/home/glenn/Projects/Code/asteroid/music/library/"))
|
|
(if (and (stringp host-path)
|
|
(>= (length host-path) (length library-prefix))
|
|
(string= host-path library-prefix :end1 (length library-prefix)))
|
|
(concatenate 'string "/app/music/"
|
|
(subseq host-path (length library-prefix)))
|
|
host-path)))
|
|
|
|
(defun generate-m3u-playlist (track-ids output-path)
|
|
"Generate an M3U playlist file from a list of track IDs"
|
|
(with-open-file (stream output-path
|
|
:direction :output
|
|
:if-exists :supersede
|
|
:if-does-not-exist :create)
|
|
(format stream "#EXTM3U~%")
|
|
(dolist (track-id track-ids)
|
|
(let ((file-path (get-track-file-path track-id)))
|
|
(when file-path
|
|
(let ((docker-path (convert-to-docker-path file-path)))
|
|
(format stream "#EXTINF:0,~%")
|
|
(format stream "~a~%" docker-path))))))
|
|
t)
|
|
|
|
(defun regenerate-stream-playlist ()
|
|
"Regenerate the main stream playlist from the current queue"
|
|
(let ((playlist-path (merge-pathnames "stream-queue.m3u"
|
|
(asdf:system-source-directory :asteroid))))
|
|
(if (null *stream-queue*)
|
|
;; If queue is empty, generate from all tracks (fallback)
|
|
(let ((all-tracks (db:select "tracks" (db:query :all))))
|
|
(generate-m3u-playlist
|
|
(mapcar (lambda (track)
|
|
(let ((id (gethash "_id" track)))
|
|
(if (listp id) (first id) id)))
|
|
all-tracks)
|
|
playlist-path))
|
|
;; Generate from queue
|
|
(generate-m3u-playlist *stream-queue* playlist-path))))
|
|
|
|
(defun export-playlist-to-m3u (playlist-id output-path)
|
|
"Export a user playlist to an M3U file"
|
|
(let ((playlist (get-playlist-by-id playlist-id)))
|
|
(when playlist
|
|
(let* ((track-ids-raw (gethash "track-ids" playlist))
|
|
(track-ids-str (if (listp track-ids-raw)
|
|
(first track-ids-raw)
|
|
track-ids-raw))
|
|
(track-ids (if (and track-ids-str
|
|
(stringp track-ids-str)
|
|
(not (string= track-ids-str "")))
|
|
(mapcar #'parse-integer
|
|
(cl-ppcre:split "," track-ids-str))
|
|
nil)))
|
|
(generate-m3u-playlist track-ids output-path)))))
|
|
|
|
;;; Stream History Management
|
|
|
|
(defun add-to-stream-history (track-id)
|
|
"Add a track to the stream history"
|
|
(push track-id *stream-history*)
|
|
;; Keep history size limited
|
|
(when (> (length *stream-history*) *max-history-size*)
|
|
(setf *stream-history* (subseq *stream-history* 0 *max-history-size*)))
|
|
t)
|
|
|
|
(defun get-stream-history (&optional (count 10))
|
|
"Get recent stream history (default 10 tracks)"
|
|
(subseq *stream-history* 0 (min count (length *stream-history*))))
|
|
|
|
;;; Smart Queue Building
|
|
|
|
(defun build-smart-queue (genre &optional (count 20))
|
|
"Build a smart queue based on genre"
|
|
(let ((tracks (db:select "tracks" (db:query :all))))
|
|
;; For now, just add random tracks
|
|
;; TODO: Implement genre filtering when we have genre metadata
|
|
(let ((track-ids (mapcar (lambda (track)
|
|
(let ((id (gethash "_id" track)))
|
|
(if (listp id) (first id) id)))
|
|
tracks)))
|
|
(setf *stream-queue* (subseq (alexandria:shuffle track-ids)
|
|
0
|
|
(min count (length track-ids))))
|
|
(regenerate-stream-playlist)
|
|
*stream-queue*)))
|
|
|
|
(defun build-queue-from-artist (artist-name &optional (count 20))
|
|
"Build a queue from tracks by a specific artist"
|
|
(let ((tracks (db:select "tracks" (db:query :all))))
|
|
(let ((matching-tracks
|
|
(remove-if-not
|
|
(lambda (track)
|
|
(let ((artist (gethash "artist" track)))
|
|
(when artist
|
|
(let ((artist-str (if (listp artist) (first artist) artist)))
|
|
(search artist-name artist-str :test #'char-equal)))))
|
|
tracks)))
|
|
(let ((track-ids (mapcar (lambda (track)
|
|
(let ((id (gethash "_id" track)))
|
|
(if (listp id) (first id) id)))
|
|
matching-tracks)))
|
|
(setf *stream-queue* (subseq track-ids 0 (min count (length track-ids))))
|
|
(regenerate-stream-playlist)
|
|
*stream-queue*))))
|