Fix scheduler timezone and taglib type errors

- cl-cron uses local time, not UTC: add utc-hour-to-local-hour
  conversion so schedule hours fire at correct UTC times
  (e.g. 12:00 UTC now fires at 15:00 local on UTC+3)

- Wrap each taglib field read in safe-tag with per-field error
  handling so a type error in one field (e.g. album with non-simple
  string) doesn't crash play-file or skip the track

- Use (coerce ... 'simple-string) instead of copy-seq for
  guaranteed simple-string output from ensure-simple-string
This commit is contained in:
Glenn Thompson 2026-03-05 15:16:21 +03:00
parent 9ae7546466
commit 47e6c5da46
2 changed files with 31 additions and 10 deletions

View File

@ -217,18 +217,25 @@
;;; ---- Metadata ----
(defun ensure-simple-string (s)
"Coerce S to a simple-string if it's a string, or return NIL."
"Coerce S to a simple-string if it's a string, or return NIL.
Uses coerce to guarantee SIMPLE-STRING type for downstream consumers."
(when (stringp s)
(copy-seq (string-trim '(#\Space #\Nul) s))))
(coerce (string-trim '(#\Space #\Nul) s) 'simple-string)))
(defun safe-tag (fn audio-file)
"Safely read a tag field, coercing to simple-string. Returns NIL on any error."
(handler-case
(ensure-simple-string (funcall fn audio-file))
(error () nil)))
(defun read-audio-metadata (file-path)
"Read metadata (artist, title, album) from an audio file using taglib.
Returns a plist (:artist ... :title ... :album ...) or NIL on failure."
(handler-case
(let ((audio-file (audio-streams:open-audio-file (namestring file-path))))
(list :artist (ensure-simple-string (abstract-tag:artist audio-file))
:title (ensure-simple-string (abstract-tag:title audio-file))
:album (ensure-simple-string (abstract-tag:album audio-file))))
(list :artist (safe-tag #'abstract-tag:artist audio-file)
:title (safe-tag #'abstract-tag:title audio-file)
:album (safe-tag #'abstract-tag:album audio-file)))
(error (e)
(log:debug "Could not read tags from ~A: ~A" file-path e)
nil)))

View File

@ -101,16 +101,30 @@
;;; Cron Job Management
(defun utc-hour-to-local-hour (utc-hour)
"Convert a UTC hour (0-23) to the local timezone hour for cl-cron.
cl-cron uses decode-universal-time which returns local time."
(let* ((now (get-universal-time))
(local-hour (nth-value 2 (decode-universal-time now)))
(utc-hour-now (nth-value 2 (decode-universal-time now 0)))
(offset (- local-hour utc-hour-now)))
(mod (+ utc-hour offset) 24)))
(defun setup-playlist-cron-jobs ()
"Set up cl-cron jobs for all scheduled playlists."
"Set up cl-cron jobs for all scheduled playlists.
Schedule hours are in UTC; cl-cron fires in local time,
so we convert UTC hours to local hours."
(unless *scheduler-running*
(dolist (entry *playlist-schedule*)
(let ((hour (car entry))
(playlist (cdr entry)))
(let* ((utc-hour (car entry))
(playlist (cdr entry))
(local-hour (utc-hour-to-local-hour utc-hour)))
(log:info "Scheduling ~A at ~2,'0d:00 UTC (~2,'0d:00 local)"
playlist utc-hour local-hour)
(cl-cron:make-cron-job
(scheduled-playlist-loader hour playlist)
(scheduled-playlist-loader utc-hour playlist)
:minute 0
:hour hour)))
:hour local-hour)))
(setf *scheduler-running* t)))
(defun start-playlist-scheduler ()