-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstream-control.lisp
More file actions
209 lines (185 loc) · 8.42 KB
/
stream-control.lisp
File metadata and controls
209 lines (185 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
;;;; 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-str (dm:field playlist "track-ids"))
(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
(dm:field track "file-path"))))
(defun convert-to-docker-path (host-path)
"Convert host file path to Docker container path"
;; Replace the music library path with /app/music/
(let ((library-prefix (namestring *music-library-path*)))
(if (and (stringp host-path)
(>= (length host-path) (length library-prefix))
(string= host-path library-prefix :end1 (length library-prefix)))
(format nil "/app/music/~a" (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.
NOTE: This writes to project root stream-queue.m3u, NOT playlists/stream-queue.m3u
which is what Liquidsoap actually reads. This function may be deprecated."
(let ((playlist-path (merge-pathnames "stream-queue.m3u"
(asdf:system-source-directory :asteroid))))
(if (null *stream-queue*)
;; DISABLED: Don't dump all tracks when queue is empty
;; This was overwriting files with all library tracks unexpectedly
;; (let ((all-tracks (dm:get "tracks" (db:query :all))))
;; (generate-m3u-playlist
;; (mapcar (lambda (track)
;; (dm:id track))
;; all-tracks)
;; playlist-path))
(format t "Stream queue is empty, not generating playlist file~%")
;; 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-str (dm:field playlist "track-ids"))
(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 (dm:get "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)
(dm:id track))
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 (dm:get "tracks" (db:query :all))))
(let ((matching-tracks
(remove-if-not
(lambda (track)
(let ((artist (dm:field track "artist")))
(when artist
(search artist-name artist :test #'char-equal))))
tracks)))
(let ((track-ids (mapcar (lambda (track)
(dm:id track))
matching-tracks)))
(setf *stream-queue* (subseq track-ids 0 (min count (length track-ids))))
(regenerate-stream-playlist)
*stream-queue*))))
(defun convert-from-docker-path (docker-path)
"Convert Docker container path back to host file path"
(if (and (stringp docker-path)
(>= (length docker-path) 11)
(string= docker-path "/app/music/" :end1 11))
(format nil "~a~a"
(namestring *music-library-path*)
(subseq docker-path 11))
docker-path))
(defun load-queue-from-m3u-file ()
"Load the stream queue from the stream-queue.m3u file"
(let* ((m3u-path (merge-pathnames "stream-queue.m3u"
(asdf:system-source-directory :asteroid)))
(track-ids '())
(all-tracks (dm:get "tracks" (db:query :all))))
(when (probe-file m3u-path)
(with-open-file (stream m3u-path :direction :input)
(loop for line = (read-line stream nil)
while line
do (unless (or (string= line "")
(char= (char line 0) #\#))
;; This is a file path line
(let* ((docker-path (string-trim '(#\Space #\Tab #\Return #\Newline) line))
(host-path (convert-from-docker-path docker-path)))
;; Find track by file path
(let ((track (find-if
(lambda (trk)
(let ((file-path (dm:field trk "file-path")))
(string= file-path host-path)))
all-tracks)))
(when track
(push (dm:id track) track-ids))))))))
;; Reverse to maintain order from file
(setf track-ids (nreverse track-ids))
(setf *stream-queue* track-ids)
(regenerate-stream-playlist)
(length track-ids)))