"))))))
(setf (ps:@ table-body inner-h-t-m-l) html))))))))
(catch (lambda (error)
(ps:chain console (error "Error loading scheduler status:" error))))))
+ ;; Add or update schedule entry
+ (defun add-schedule-entry ()
+ (let ((hour-select (ps:chain document (get-element-by-id "schedule-hour")))
+ (playlist-select (ps:chain document (get-element-by-id "schedule-playlist"))))
+ (when (and hour-select playlist-select)
+ (let ((hour (parse-int (ps:@ hour-select value)))
+ (playlist (ps:@ playlist-select value)))
+ (if (= playlist "")
+ (alert "Please select a playlist")
+ (ps:chain
+ (fetch "/api/asteroid/scheduler/update"
+ (ps:create :method "POST"
+ :headers (ps:create "Content-Type" "application/x-www-form-urlencoded")
+ :body (+ "hour=" hour "&playlist=" (encode-u-r-i-component playlist))))
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (let ((data (or (ps:@ result data) result)))
+ (if (= (ps:@ data status) "success")
+ (progn
+ (show-toast (+ "✓ Schedule updated: " hour ":00 → " playlist))
+ (refresh-scheduler-status))
+ (alert (+ "Error: " (or (ps:@ data message) "Unknown error")))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error updating schedule:" error))
+ (alert "Error updating schedule")))))))))
+
+ ;; Remove schedule entry
+ (defun remove-schedule-entry (hour)
+ (when (confirm (+ "Remove schedule entry for " (if (< hour 10) "0" "") hour ":00 UTC?"))
+ (ps:chain
+ (fetch "/api/asteroid/scheduler/remove"
+ (ps:create :method "POST"
+ :headers (ps:create "Content-Type" "application/x-www-form-urlencoded")
+ :body (+ "hour=" hour)))
+ (then (lambda (response) (ps:chain response (json))))
+ (then (lambda (result)
+ (let ((data (or (ps:@ result data) result)))
+ (if (= (ps:@ data status) "success")
+ (progn
+ (show-toast (+ "✓ Removed schedule entry for " hour ":00"))
+ (refresh-scheduler-status))
+ (alert (+ "Error: " (or (ps:@ data message) "Unknown error")))))))
+ (catch (lambda (error)
+ (ps:chain console (error "Error removing schedule entry:" error))
+ (alert "Error removing schedule entry"))))))
+
;; Enable scheduler
(defun enable-scheduler ()
(ps:chain
@@ -1253,6 +1307,8 @@
(setf (ps:@ window enable-scheduler) enable-scheduler)
(setf (ps:@ window disable-scheduler) disable-scheduler)
(setf (ps:@ window load-current-scheduled-playlist) load-current-scheduled-playlist)
+ (setf (ps:@ window add-schedule-entry) add-schedule-entry)
+ (setf (ps:@ window remove-schedule-entry) remove-schedule-entry)
))
"Compiled JavaScript for admin dashboard - generated at load time")
diff --git a/playlist-scheduler.lisp b/playlist-scheduler.lisp
index b5e8685..392d2cf 100644
--- a/playlist-scheduler.lisp
+++ b/playlist-scheduler.lisp
@@ -95,10 +95,53 @@
(stop-playlist-scheduler)
(start-playlist-scheduler))
-;;; Schedule Management
+;;; Schedule Management (Database-backed)
+
+(defun load-schedule-from-db ()
+ "Load the playlist schedule from the database into *playlist-schedule*."
+ (handler-case
+ (with-db
+ (let ((rows (postmodern:query "SELECT hour, playlist FROM playlist_schedule ORDER BY hour")))
+ (when rows
+ (setf *playlist-schedule*
+ (mapcar (lambda (row)
+ (cons (first row) (second row)))
+ rows))
+ (format t "~&[SCHEDULER] Loaded ~a schedule entries from database~%" (length rows)))))
+ (error (e)
+ (format t "~&[SCHEDULER] Warning: Could not load schedule from DB: ~a~%" e)
+ (format t "~&[SCHEDULER] Using default schedule~%"))))
+
+(defun save-schedule-entry-to-db (hour playlist-name)
+ "Save or update a schedule entry in the database."
+ (handler-case
+ (with-db
+ (postmodern:query
+ (:insert-into 'playlist_schedule
+ :set 'hour hour 'playlist playlist-name 'updated_at (:now))
+ :on-conflict-update 'hour
+ :update-set 'playlist playlist-name 'updated_at (:now)))
+ (error (e)
+ ;; Try simpler upsert approach
+ (handler-case
+ (with-db
+ (postmodern:query
+ (format nil "INSERT INTO playlist_schedule (hour, playlist, updated_at) VALUES (~a, '~a', NOW()) ON CONFLICT (hour) DO UPDATE SET playlist = '~a', updated_at = NOW()"
+ hour playlist-name playlist-name)))
+ (error (e2)
+ (format t "~&[SCHEDULER] Warning: Could not save schedule entry: ~a~%" e2))))))
+
+(defun delete-schedule-entry-from-db (hour)
+ "Delete a schedule entry from the database."
+ (handler-case
+ (with-db
+ (postmodern:query (:delete-from 'playlist_schedule :where (:= 'hour hour))))
+ (error (e)
+ (format t "~&[SCHEDULER] Warning: Could not delete schedule entry: ~a~%" e))))
(defun add-scheduled-playlist (hour playlist-name)
- "Add or update a playlist in the schedule."
+ "Add or update a playlist in the schedule (persists to database)."
+ (save-schedule-entry-to-db hour playlist-name)
(setf *playlist-schedule*
(cons (cons hour playlist-name)
(remove hour *playlist-schedule* :key #'car)))
@@ -107,7 +150,8 @@
*playlist-schedule*)
(defun remove-scheduled-playlist (hour)
- "Remove a playlist from the schedule."
+ "Remove a playlist from the schedule (persists to database)."
+ (delete-schedule-entry-from-db hour)
(setf *playlist-schedule*
(remove hour *playlist-schedule* :key #'car))
(when *scheduler-running*
@@ -118,6 +162,13 @@
"Get the current playlist schedule as a sorted list."
(sort (copy-list *playlist-schedule*) #'< :key #'car))
+(defun get-available-playlists ()
+ "Get list of available playlist files from the playlists directory."
+ (let ((playlists-dir (get-playlists-directory)))
+ (when (probe-file playlists-dir)
+ (mapcar #'file-namestring
+ (directory (merge-pathnames "*.m3u" playlists-dir))))))
+
(defun get-server-time-info ()
"Get current server time information in both UTC and local timezone."
(let* ((now (local-time:now))
@@ -147,7 +198,8 @@
(require-role :admin)
(with-error-handling
(let* ((status (get-scheduler-status))
- (time-info (getf status :server-time)))
+ (time-info (getf status :server-time))
+ (available-playlists (get-available-playlists)))
(api-output `(("status" . "success")
("enabled" . ,(if (getf status :enabled) t :json-false))
("running" . ,(if (getf status :running) t :json-false))
@@ -158,7 +210,8 @@
("schedule" . ,(mapcar (lambda (entry)
`(("hour" . ,(car entry))
("playlist" . ,(cdr entry))))
- (getf status :schedule))))))))
+ (getf status :schedule)))
+ ("available_playlists" . ,(coerce available-playlists 'vector)))))))
(define-api asteroid/scheduler/enable () ()
"Enable the playlist scheduler"
@@ -251,6 +304,8 @@
(format t "~&[SCHEDULER] Database connected, starting playlist scheduler...~%")
(handler-case
(progn
+ ;; Load schedule from database first
+ (load-schedule-from-db)
(start-playlist-scheduler)
;; Load the current scheduled playlist on startup
(let ((current-playlist (get-current-scheduled-playlist)))
diff --git a/template/admin.ctml b/template/admin.ctml
index cfbce07..54232a8 100644
--- a/template/admin.ctml
+++ b/template/admin.ctml
@@ -253,16 +253,61 @@
Time (UTC)
Playlist
Status
+
Actions
-
Loading...
+
Loading...
+
+
+
➕ Add/Update Schedule Entry
+
+
+
+
+
+
+
The scheduler automatically loads the appropriate playlist at each scheduled time.
Use "Load Current" to manually sync to the correct playlist for the current time.
+ Changes are saved to the database and persist across restarts.