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 ---- ;;; ---- Metadata ----
(defun ensure-simple-string (s) (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) (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) (defun read-audio-metadata (file-path)
"Read metadata (artist, title, album) from an audio file using taglib. "Read metadata (artist, title, album) from an audio file using taglib.
Returns a plist (:artist ... :title ... :album ...) or NIL on failure." Returns a plist (:artist ... :title ... :album ...) or NIL on failure."
(handler-case (handler-case
(let ((audio-file (audio-streams:open-audio-file (namestring file-path)))) (let ((audio-file (audio-streams:open-audio-file (namestring file-path))))
(list :artist (ensure-simple-string (abstract-tag:artist audio-file)) (list :artist (safe-tag #'abstract-tag:artist audio-file)
:title (ensure-simple-string (abstract-tag:title audio-file)) :title (safe-tag #'abstract-tag:title audio-file)
:album (ensure-simple-string (abstract-tag:album audio-file)))) :album (safe-tag #'abstract-tag:album audio-file)))
(error (e) (error (e)
(log:debug "Could not read tags from ~A: ~A" file-path e) (log:debug "Could not read tags from ~A: ~A" file-path e)
nil))) nil)))

View File

@ -101,16 +101,30 @@
;;; Cron Job Management ;;; 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 () (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* (unless *scheduler-running*
(dolist (entry *playlist-schedule*) (dolist (entry *playlist-schedule*)
(let ((hour (car entry)) (let* ((utc-hour (car entry))
(playlist (cdr 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 (cl-cron:make-cron-job
(scheduled-playlist-loader hour playlist) (scheduled-playlist-loader utc-hour playlist)
:minute 0 :minute 0
:hour hour))) :hour local-hour)))
(setf *scheduler-running* t))) (setf *scheduler-running* t)))
(defun start-playlist-scheduler () (defun start-playlist-scheduler ()