Compare commits
No commits in common. "c129959eafb1286895e0ab19e4565c9433e87393" and "f8e37ac02a659ab14aa1cdde08db86a2272b844b" have entirely different histories.
c129959eaf
...
f8e37ac02a
|
|
@ -1,27 +0,0 @@
|
|||
-- Migration 004: Playlist Schedule Table
|
||||
-- Description: Store playlist schedule configuration in database for persistence
|
||||
|
||||
CREATE TABLE IF NOT EXISTS playlist_schedule (
|
||||
_id SERIAL PRIMARY KEY,
|
||||
hour INTEGER NOT NULL UNIQUE CHECK (hour >= 0 AND hour <= 23),
|
||||
playlist VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert default schedule
|
||||
INSERT INTO playlist_schedule (hour, playlist) VALUES
|
||||
(0, 'midnight-ambient.m3u'),
|
||||
(6, 'morning-drift.m3u'),
|
||||
(12, 'afternoon-orbit.m3u'),
|
||||
(18, 'evening-descent.m3u')
|
||||
ON CONFLICT (hour) DO NOTHING;
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL PRIVILEGES ON playlist_schedule TO asteroid;
|
||||
GRANT ALL PRIVILEGES ON playlist_schedule__id_seq TO asteroid;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Playlist schedule table created successfully!';
|
||||
END $$;
|
||||
|
|
@ -1158,17 +1158,7 @@
|
|||
(setf (ps:@ status-el inner-h-t-m-l)
|
||||
"<span style=\"color: #ffaa00;\">🟡 Disabled</span>"))))
|
||||
|
||||
;; Update available playlists dropdown
|
||||
(let ((playlist-select (ps:chain document (get-element-by-id "schedule-playlist")))
|
||||
(available (ps:@ data available_playlists)))
|
||||
(when (and playlist-select available)
|
||||
(let ((html "<option value=\"\">-- Select Playlist --</option>"))
|
||||
(ps:chain available
|
||||
(for-each (lambda (p)
|
||||
(setf html (+ html "<option value=\"" p "\">" p "</option>")))))
|
||||
(setf (ps:@ playlist-select inner-h-t-m-l) html))))
|
||||
|
||||
;; Update schedule table with edit/delete buttons
|
||||
;; Update schedule table
|
||||
(let ((table-body (ps:chain document (get-element-by-id "scheduler-table-body")))
|
||||
(schedule (ps:@ data schedule))
|
||||
(current-hour (when (ps:@ data server_time) (ps:@ data server_time utc_hour))))
|
||||
|
|
@ -1178,65 +1168,21 @@
|
|||
(for-each (lambda (entry)
|
||||
(let* ((hour (ps:@ entry hour))
|
||||
(playlist (ps:@ entry playlist))
|
||||
(is-active (= playlist (ps:@ data current_playlist))))
|
||||
(is-active (and current-hour
|
||||
(>= current-hour hour)
|
||||
(or (not (ps:chain schedule (find (lambda (e) (and (> (ps:@ e hour) hour) (<= (ps:@ e hour) current-hour))))))
|
||||
t))))
|
||||
(setf html
|
||||
(+ html
|
||||
"<tr" (if is-active " style=\"background: #1a3a1a;\"" "") ">"
|
||||
"<tr" (if (= playlist (ps:@ data current_playlist)) " style=\"background: #1a3a1a;\"" "") ">"
|
||||
"<td>" (if (< hour 10) "0" "") hour ":00 UTC</td>"
|
||||
"<td>" playlist "</td>"
|
||||
"<td>" (if is-active "▶️ Active" "") "</td>"
|
||||
"<td><button class=\"btn btn-danger btn-sm\" onclick=\"removeScheduleEntry(" hour ")\">🗑️</button></td>"
|
||||
"<td>" (if (= playlist (ps:@ data current_playlist)) "▶️ Active" "") "</td>"
|
||||
"</tr>"))))))
|
||||
(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
|
||||
|
|
@ -1307,8 +1253,6 @@
|
|||
(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")
|
||||
|
||||
|
|
|
|||
|
|
@ -95,53 +95,10 @@
|
|||
(stop-playlist-scheduler)
|
||||
(start-playlist-scheduler))
|
||||
|
||||
;;; 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))))
|
||||
;;; Schedule Management
|
||||
|
||||
(defun add-scheduled-playlist (hour playlist-name)
|
||||
"Add or update a playlist in the schedule (persists to database)."
|
||||
(save-schedule-entry-to-db hour playlist-name)
|
||||
"Add or update a playlist in the schedule."
|
||||
(setf *playlist-schedule*
|
||||
(cons (cons hour playlist-name)
|
||||
(remove hour *playlist-schedule* :key #'car)))
|
||||
|
|
@ -150,8 +107,7 @@
|
|||
*playlist-schedule*)
|
||||
|
||||
(defun remove-scheduled-playlist (hour)
|
||||
"Remove a playlist from the schedule (persists to database)."
|
||||
(delete-schedule-entry-from-db hour)
|
||||
"Remove a playlist from the schedule."
|
||||
(setf *playlist-schedule*
|
||||
(remove hour *playlist-schedule* :key #'car))
|
||||
(when *scheduler-running*
|
||||
|
|
@ -162,13 +118,6 @@
|
|||
"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))
|
||||
|
|
@ -198,8 +147,7 @@
|
|||
(require-role :admin)
|
||||
(with-error-handling
|
||||
(let* ((status (get-scheduler-status))
|
||||
(time-info (getf status :server-time))
|
||||
(available-playlists (get-available-playlists)))
|
||||
(time-info (getf status :server-time)))
|
||||
(api-output `(("status" . "success")
|
||||
("enabled" . ,(if (getf status :enabled) t :json-false))
|
||||
("running" . ,(if (getf status :running) t :json-false))
|
||||
|
|
@ -210,8 +158,7 @@
|
|||
("schedule" . ,(mapcar (lambda (entry)
|
||||
`(("hour" . ,(car entry))
|
||||
("playlist" . ,(cdr entry))))
|
||||
(getf status :schedule)))
|
||||
("available_playlists" . ,(coerce available-playlists 'vector)))))))
|
||||
(getf status :schedule))))))))
|
||||
|
||||
(define-api asteroid/scheduler/enable () ()
|
||||
"Enable the playlist scheduler"
|
||||
|
|
@ -299,19 +246,12 @@
|
|||
;;; Auto-start scheduler when database is connected
|
||||
;;; This ensures the scheduler starts after the server is fully initialized
|
||||
|
||||
(define-trigger db:connected ()
|
||||
(define-trigger db:connected :after ()
|
||||
"Start the playlist scheduler after database connection is established"
|
||||
(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)))
|
||||
(when current-playlist
|
||||
(format t "~&[SCHEDULER] Loading current scheduled playlist: ~a~%" current-playlist)
|
||||
(load-scheduled-playlist current-playlist)))
|
||||
(format t "~&[SCHEDULER] Scheduler auto-started successfully~%"))
|
||||
(error (e)
|
||||
(format t "~&[SCHEDULER] Warning: Could not auto-start scheduler: ~a~%" e))))
|
||||
|
|
|
|||
|
|
@ -253,61 +253,16 @@
|
|||
<th>Time (UTC)</th>
|
||||
<th>Playlist</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scheduler-table-body">
|
||||
<tr><td colspan="4" style="color: #888;">Loading...</td></tr>
|
||||
<tr><td colspan="3" style="color: #888;">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Add New Schedule Entry -->
|
||||
<div style="margin-top: 15px; padding: 15px; background: #1a1a1a; border-radius: 8px;">
|
||||
<h4 style="margin-top: 0;">➕ Add/Update Schedule Entry</h4>
|
||||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||
<label>
|
||||
Hour (UTC):
|
||||
<select id="schedule-hour" style="margin-left: 5px; padding: 5px;">
|
||||
<option value="0">00:00</option>
|
||||
<option value="1">01:00</option>
|
||||
<option value="2">02:00</option>
|
||||
<option value="3">03:00</option>
|
||||
<option value="4">04:00</option>
|
||||
<option value="5">05:00</option>
|
||||
<option value="6">06:00</option>
|
||||
<option value="7">07:00</option>
|
||||
<option value="8">08:00</option>
|
||||
<option value="9">09:00</option>
|
||||
<option value="10">10:00</option>
|
||||
<option value="11">11:00</option>
|
||||
<option value="12">12:00</option>
|
||||
<option value="13">13:00</option>
|
||||
<option value="14">14:00</option>
|
||||
<option value="15">15:00</option>
|
||||
<option value="16">16:00</option>
|
||||
<option value="17">17:00</option>
|
||||
<option value="18">18:00</option>
|
||||
<option value="19">19:00</option>
|
||||
<option value="20">20:00</option>
|
||||
<option value="21">21:00</option>
|
||||
<option value="22">22:00</option>
|
||||
<option value="23">23:00</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Playlist:
|
||||
<select id="schedule-playlist" style="margin-left: 5px; padding: 5px;">
|
||||
<option value="">-- Select Playlist --</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn btn-success" onclick="addScheduleEntry()">➕ Add/Update</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 15px; font-size: 0.9em; color: #888;">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue