From 47e6c5da466b8f53b5571638c451d9c6d4d58dbb Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Thu, 5 Mar 2026 15:16:21 +0300 Subject: [PATCH] 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 --- cl-streamer/harmony-backend.lisp | 17 ++++++++++++----- playlist-scheduler.lisp | 24 +++++++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/cl-streamer/harmony-backend.lisp b/cl-streamer/harmony-backend.lisp index e87315d..4ee5831 100644 --- a/cl-streamer/harmony-backend.lisp +++ b/cl-streamer/harmony-backend.lisp @@ -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))) diff --git a/playlist-scheduler.lisp b/playlist-scheduler.lisp index 944d5ce..3922f0e 100644 --- a/playlist-scheduler.lisp +++ b/playlist-scheduler.lisp @@ -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 ()