feat: Add automatic playlist scheduler with cl-cron

- Add playlist-scheduler.lisp with cl-cron based scheduling
- Schedule playlists at 00:00, 06:00, 12:00, 18:00 UTC
- Auto-start scheduler on database connection
- Add API endpoints for admin control:
  - /api/asteroid/scheduler/status
  - /api/asteroid/scheduler/enable
  - /api/asteroid/scheduler/disable
  - /api/asteroid/scheduler/load-current
  - /api/asteroid/scheduler/schedule
  - /api/asteroid/scheduler/update
  - /api/asteroid/scheduler/remove
- Add cl-cron dependency to asteroid.asd
- Extend morning-drift.m3u to ~6 hours (101 tracks)
This commit is contained in:
glenneth 2025-12-17 14:22:53 +03:00
parent f1ba25c6bf
commit bafc43c9f4
3 changed files with 368 additions and 0 deletions

View File

@ -28,6 +28,7 @@
:cl-fad
:bordeaux-threads
:drakma
:cl-cron
;; radiance interfaces
:i-log4cl
:i-postmodern
@ -60,6 +61,7 @@
(:file "user-management")
(:file "playlist-management")
(:file "stream-control")
(:file "playlist-scheduler")
(:file "listener-stats")
(:file "auth-routes")
(:file "frontend-partials")

238
playlist-scheduler.lisp Normal file
View File

@ -0,0 +1,238 @@
;;;; playlist-scheduler.lisp - Automatic Playlist Scheduling for Asteroid Radio
;;;; Uses cl-cron to load time-based playlists at scheduled times
(in-package :asteroid)
;;; Scheduler Configuration
(defvar *playlist-schedule*
'((0 . "midnight-ambient.m3u") ; 00:00 UTC
(6 . "morning-drift.m3u") ; 06:00 UTC
(12 . "afternoon-orbit.m3u") ; 12:00 UTC
(18 . "evening-descent.m3u")) ; 18:00 UTC
"Association list mapping hours (UTC) to playlist filenames.
Each entry is (hour . playlist-filename).")
(defvar *scheduler-enabled* t
"When true, the playlist scheduler is active.")
(defvar *scheduler-running* nil
"Internal flag tracking if scheduler cron jobs are registered.")
;;; Scheduler Functions
(defun get-scheduled-playlist-for-hour (hour)
"Get the playlist filename scheduled for a given hour.
Returns the playlist for the most recent scheduled time slot."
(let ((sorted-schedule (sort (copy-list *playlist-schedule*) #'> :key #'car)))
(or (cdr (find-if (lambda (entry) (<= (car entry) hour)) sorted-schedule))
(cdar (last sorted-schedule)))))
(defun get-current-scheduled-playlist ()
"Get the playlist that should be playing right now based on UTC time."
(let ((current-hour (local-time:timestamp-hour (local-time:now) :timezone local-time:+utc-zone+)))
(get-scheduled-playlist-for-hour current-hour)))
(defun load-scheduled-playlist (playlist-name)
"Load a playlist by name, copying it to stream-queue.m3u and triggering playback."
(let ((playlist-path (merge-pathnames playlist-name (get-playlists-directory))))
(if (probe-file playlist-path)
(progn
(format t "~&[SCHEDULER] Loading playlist: ~a~%" playlist-name)
(copy-playlist-to-stream-queue playlist-path)
(load-queue-from-m3u-file)
(handler-case
(progn
(liquidsoap-command "stream-queue_m3u.skip")
(format t "~&[SCHEDULER] Playlist ~a loaded and crossfade triggered~%" playlist-name))
(error (e)
(format t "~&[SCHEDULER] Warning: Could not skip track: ~a~%" e)))
t)
(progn
(format t "~&[SCHEDULER] Error: Playlist not found: ~a~%" playlist-name)
nil))))
(defun scheduled-playlist-loader (hour playlist-name)
"Create a function that loads a specific playlist. Used by cl-cron jobs."
(lambda ()
(when *scheduler-enabled*
(format t "~&[SCHEDULER] Triggered at hour ~a UTC - loading ~a~%" hour playlist-name)
(load-scheduled-playlist playlist-name))))
;;; Cron Job Management
(defun setup-playlist-cron-jobs ()
"Set up cl-cron jobs for all scheduled playlists."
(unless *scheduler-running*
(format t "~&[SCHEDULER] Setting up playlist schedule:~%")
(dolist (entry *playlist-schedule*)
(let ((hour (car entry))
(playlist (cdr entry)))
(format t "~&[SCHEDULER] ~2,'0d:00 UTC -> ~a~%" hour playlist)
(cl-cron:make-cron-job
(scheduled-playlist-loader hour playlist)
:minute 0
:hour hour)))
(setf *scheduler-running* t)
(format t "~&[SCHEDULER] Playlist schedule configured~%")))
(defun start-playlist-scheduler ()
"Start the playlist scheduler. Sets up cron jobs and starts cl-cron."
(setup-playlist-cron-jobs)
(cl-cron:start-cron)
(format t "~&[SCHEDULER] Playlist scheduler started~%")
t)
(defun stop-playlist-scheduler ()
"Stop the playlist scheduler."
(cl-cron:stop-cron)
(setf *scheduler-running* nil)
(format t "~&[SCHEDULER] Playlist scheduler stopped~%")
t)
(defun restart-playlist-scheduler ()
"Restart the playlist scheduler with current configuration."
(stop-playlist-scheduler)
(start-playlist-scheduler))
;;; Schedule Management
(defun add-scheduled-playlist (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)))
(when *scheduler-running*
(restart-playlist-scheduler))
*playlist-schedule*)
(defun remove-scheduled-playlist (hour)
"Remove a playlist from the schedule."
(setf *playlist-schedule*
(remove hour *playlist-schedule* :key #'car))
(when *scheduler-running*
(restart-playlist-scheduler))
*playlist-schedule*)
(defun get-schedule ()
"Get the current playlist schedule as a sorted list."
(sort (copy-list *playlist-schedule*) #'< :key #'car))
(defun get-scheduler-status ()
"Get the current status of the scheduler."
(list :enabled *scheduler-enabled*
:running *scheduler-running*
:current-playlist (get-current-scheduled-playlist)
:schedule (get-schedule)))
;;; API Endpoints for Admin Interface
(define-api asteroid/scheduler/status () ()
"Get the current scheduler status"
(require-role :admin)
(with-error-handling
(let ((status (get-scheduler-status)))
(api-output `(("status" . "success")
("enabled" . ,(if (getf status :enabled) t :json-false))
("running" . ,(if (getf status :running) t :json-false))
("current_playlist" . ,(getf status :current-playlist))
("schedule" . ,(mapcar (lambda (entry)
`(("hour" . ,(car entry))
("playlist" . ,(cdr entry))))
(getf status :schedule))))))))
(define-api asteroid/scheduler/enable () ()
"Enable the playlist scheduler"
(require-role :admin)
(with-error-handling
(setf *scheduler-enabled* t)
(unless *scheduler-running*
(start-playlist-scheduler))
(api-output `(("status" . "success")
("message" . "Scheduler enabled")))))
(define-api asteroid/scheduler/disable () ()
"Disable the playlist scheduler (stops automatic playlist changes)"
(require-role :admin)
(with-error-handling
(setf *scheduler-enabled* nil)
(api-output `(("status" . "success")
("message" . "Scheduler disabled - playlists will not auto-change")))))
(define-api asteroid/scheduler/load-current () ()
"Manually load the playlist that should be playing now based on schedule"
(require-role :admin)
(with-error-handling
(let ((playlist (get-current-scheduled-playlist)))
(if (load-scheduled-playlist playlist)
(api-output `(("status" . "success")
("message" . ,(format nil "Loaded scheduled playlist: ~a" playlist))
("playlist" . ,playlist)))
(api-output `(("status" . "error")
("message" . ,(format nil "Failed to load playlist: ~a" playlist)))
:status 500)))))
(define-api asteroid/scheduler/schedule () ()
"Get the current playlist schedule"
(require-role :admin)
(with-error-handling
(api-output `(("status" . "success")
("schedule" . ,(mapcar (lambda (entry)
`(("hour" . ,(car entry))
("playlist" . ,(cdr entry))
("time_label" . ,(format nil "~2,'0d:00 UTC" (car entry)))))
(get-schedule)))))))
(define-api asteroid/scheduler/update (hour playlist) ()
"Add or update a scheduled playlist (hour is 0-23 UTC)"
(require-role :admin)
(with-error-handling
(let ((hour-int (parse-integer hour :junk-allowed t)))
(if (and hour-int (>= hour-int 0) (<= hour-int 23))
(let ((playlist-path (merge-pathnames playlist (get-playlists-directory))))
(if (probe-file playlist-path)
(progn
(add-scheduled-playlist hour-int playlist)
(api-output `(("status" . "success")
("message" . ,(format nil "Schedule updated: ~2,'0d:00 UTC -> ~a" hour-int playlist))
("schedule" . ,(mapcar (lambda (entry)
`(("hour" . ,(car entry))
("playlist" . ,(cdr entry))))
(get-schedule))))))
(api-output `(("status" . "error")
("message" . ,(format nil "Playlist not found: ~a" playlist)))
:status 404)))
(api-output `(("status" . "error")
("message" . "Invalid hour - must be 0-23"))
:status 400)))))
(define-api asteroid/scheduler/remove (hour) ()
"Remove a scheduled playlist"
(require-role :admin)
(with-error-handling
(let ((hour-int (parse-integer hour :junk-allowed t)))
(if (and hour-int (>= hour-int 0) (<= hour-int 23))
(progn
(remove-scheduled-playlist hour-int)
(api-output `(("status" . "success")
("message" . ,(format nil "Removed schedule for ~2,'0d:00 UTC" hour-int))
("schedule" . ,(mapcar (lambda (entry)
`(("hour" . ,(car entry))
("playlist" . ,(cdr entry))))
(get-schedule))))))
(api-output `(("status" . "error")
("message" . "Invalid hour - must be 0-23"))
:status 400)))))
;;; Auto-start scheduler when database is connected
;;; This ensures the scheduler starts after the server is fully initialized
(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
(start-playlist-scheduler)
(format t "~&[SCHEDULER] Scheduler auto-started successfully~%"))
(error (e)
(format t "~&[SCHEDULER] Warning: Could not auto-start scheduler: ~a~%" e))))

View File

@ -77,3 +77,131 @@
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/10 Making Gardens Out of Silence.flac
#EXTINF:-1,Tangerine Dream - Virtue Is Its Own Reward
/app/music/Tangerine Dream - Ambient Monkeys (flac)/10 Tangerine Dream - Virtue Is Its Own Reward.flac
#EXTINF:-1,Brian Eno - Small Craft On A Milk Sea
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A3 Small Craft On A Milk Sea.flac
#EXTINF:-1,Tycho - Horizon
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/02 - Horizon.flac
#EXTINF:-1,Four Tet - Two Thousand And Seventeen
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/02 Two Thousand And Seventeen.flac
#EXTINF:-1,Boards of Canada - Dayvan Cowboy
/app/music/Boards of Canada/Trans Canada Highway/01 - Dayvan Cowboy.mp3
#EXTINF:-1,Ulrich Schnauss - Melts into Air (2019 Version)
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/01. Melts into Air (2019 Version).flac
#EXTINF:-1,arovane - olopp_eleen
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/01. arovane - olopp_eleen.flac
#EXTINF:-1,Biosphere - Bergsbotn
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/03 - Bergsbotn.flac
#EXTINF:-1,F.S.Blumm & Nils Frahm - Perff
/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/02. F.S.Blumm & Nils Frahm - Perff.flac
#EXTINF:-1,Clark - Simple Homecoming Loop
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/02 - Simple Homecoming Loop.flac
#EXTINF:-1,Tycho - Slack
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/03 - Slack.flac
#EXTINF:-1,Johann Johannsson - A Game Of Croquet
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/07 - A Game Of Croquet.flac
#EXTINF:-1,FSOL - Motioned
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/04 - Motioned.flac
#EXTINF:-1,Brian Eno - Flint March
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A4 Flint March.flac
#EXTINF:-1,Four Tet - Lush
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/05 Lush.flac
#EXTINF:-1,Boards of Canada - Turquoise Hexagon Sun
/app/music/Boards of Canada/Hi Scores/02 - Turquoise Hexagon Sun.mp3
#EXTINF:-1,Ulrich Schnauss - Love Grows Out of Thin Air (2019 Version)
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/02. Love Grows Out of Thin Air (2019 Version).flac
#EXTINF:-1,arovane - wirkung
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/02. arovane - wirkung.flac
#EXTINF:-1,Biosphere - Berg
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/05 - Berg.flac
#EXTINF:-1,God is an Astronaut - Komorebi
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/05. Komorebi.flac
#EXTINF:-1,Tycho - Weather
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/01 - Weather.flac
#EXTINF:-1,Port Blue - Sunset Cruiser
/app/music/Port Blue - The Airship (2007)/04. Sunset Cruiser.flac
#EXTINF:-1,F.S.Blumm & Nils Frahm - Exercising Levitation
/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/07. F.S.Blumm & Nils Frahm - Exercising Levitation.flac
#EXTINF:-1,Four Tet - You Are Loved
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/08 You Are Loved.flac
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Asteroid 2467
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/01. Asteroid 2467.flac
#EXTINF:-1,Boards of Canada - Left Side Drive
/app/music/Boards of Canada/Trans Canada Highway/02 - Left Side Drive.mp3
#EXTINF:-1,Brian Eno - Who Gives a Thought
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/01 Who Gives a Thought.flac
#EXTINF:-1,arovane - find
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/03. arovane - find.flac
#EXTINF:-1,Tycho - Receiver
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/04 - Receiver.flac
#EXTINF:-1,Johann Johannsson - The Origins Of Time
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/08 - The Origins Of Time.flac
#EXTINF:-1,FSOL - Lichaen
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/05 - Lichaen.flac
#EXTINF:-1,Biosphere - Kyle
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/06 - Kyle.flac
#EXTINF:-1,Port Blue - The Grand Staircase
/app/music/Port Blue - The Airship (2007)/03. The Grand Staircase.flac
#EXTINF:-1,Vector Lovers - City Lights From A Train
/app/music/Vector Lovers/2005 - Capsule For One/01 - City Lights From A Train.mp3
#EXTINF:-1,Four Tet - Teenage Birdsong
/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/04 - Four Tet - Teenage Birdsong.flac
#EXTINF:-1,Ulrich Schnauss - The Magic in You (2019 Version)
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/03. The Magic in You (2019 Version).flac
#EXTINF:-1,Boards of Canada - Oirectine
/app/music/Boards of Canada/Twoism/02 - Oirectine.mp3
#EXTINF:-1,Clark - Bench
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/03 - Bench.flac
#EXTINF:-1,Tycho - Alright
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/02 - Alright.flac
#EXTINF:-1,Brian Eno - Lesser Heaven
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/C3 Lesser Heaven.flac
#EXTINF:-1,arovane - sloon
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/05. arovane - sloon.flac
#EXTINF:-1,God is an Astronaut - Epitaph
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/01. Epitaph.flac
#EXTINF:-1,F.S.Blumm & Nils Frahm - Silently Sharing
/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/10. F.S.Blumm & Nils Frahm - Silently Sharing.flac
#EXTINF:-1,Biosphere - Fjølhøgget
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/07 - Fjølhøgget.flac
#EXTINF:-1,Port Blue - Over Atlantic City
/app/music/Port Blue - The Airship (2007)/02. Over Atlantic City.flac
#EXTINF:-1,Johann Johannsson - Viva Voce
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/09 - Viva Voce.flac
#EXTINF:-1,Tangerine Dream - Symphony in A-minor
/app/music/Tangerine Dream - Ambient Monkeys (flac)/02 Tangerine Dream - Symphony in A-minor.flac
#EXTINF:-1,Tycho - Epoch
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/05 - Epoch.flac
#EXTINF:-1,Four Tet - Memories
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/11 Memories.flac
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Return To Burlington
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/02. Return To Burlington.flac
#EXTINF:-1,Boards of Canada - Skyliner
/app/music/Boards of Canada/Trans Canada Highway/04 - Skyliner.mp3
#EXTINF:-1,Vector Lovers - Melodies And Memory
/app/music/Vector Lovers/2005 - Capsule For One/07 - Melodies And Memory.mp3
#EXTINF:-1,Brian Eno - Icarus or Blériot
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/03 Icarus or Blériot.flac
#EXTINF:-1,arovane - noondt
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/07. arovane - noondt.flac
#EXTINF:-1,FSOL - Symphony for Halia
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/14 - Symphony for Halia.flac
#EXTINF:-1,Biosphere - Straumen
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/12 - Straumen.flac
#EXTINF:-1,Port Blue - The Gentle Descent
/app/music/Port Blue - The Airship (2007)/12. The Gentle Descent.flac
#EXTINF:-1,Proem - As They Go
/app/music/Proem/2019 - As They Go/Proem - As They Go - 01 As They Go.flac
#EXTINF:-1,Tycho - Outer Sunset
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/03 - Outer Sunset.flac
#EXTINF:-1,Four Tet - Planet
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/14 Planet.flac
#EXTINF:-1,Ulrich Schnauss - New Day Starts at Dawn (2019 Version)
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/07. New Day Starts at Dawn (2019 Version).flac
#EXTINF:-1,Clark - Goodnight Kiri
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/14 - Goodnight Kiri.flac
#EXTINF:-1,Biosphere - Steinfjord
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/13 - Steinfjord.flac
#EXTINF:-1,Tangerine Dream - Calyx Calamander
/app/music/Tangerine Dream - Ambient Monkeys (flac)/04 Tangerine Dream - Calyx Calamander.flac
#EXTINF:-1,Vector Lovers - To The Stars
/app/music/Vector Lovers/2005 - Capsule For One/12 - To The Stars.mp3