From 98ce5afbf8ded846149e1e723ceed2f45dda563d Mon Sep 17 00:00:00 2001 From: Tom Doggett <810877+nocoolnametom@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:24:20 -0400 Subject: [PATCH] Allow crontab schedule --- plugins/stash-scheduler/stash-scheduler.yml | 16 +++--- plugins/stash-scheduler/stash_scheduler.py | 54 +++++++++++++++++++-- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/plugins/stash-scheduler/stash-scheduler.yml b/plugins/stash-scheduler/stash-scheduler.yml index 6578d05b..5efea8b3 100644 --- a/plugins/stash-scheduler/stash-scheduler.yml +++ b/plugins/stash-scheduler/stash-scheduler.yml @@ -46,24 +46,28 @@ settings: displayName: Scan Frequency description: > How often to run the library scan. - Valid values: hourly, daily, weekly. - Hourly ignores the Time of Day setting. + Simple values: hourly, daily, weekly. Hourly ignores the Time of Day + setting. For more control, enter a standard 5-field crontab expression + instead (e.g. "10 7,17 * * *" fires at 7:10 AM and 5:10 PM every day; + "0 3 * * 1" fires at 3:00 AM every Monday). When a crontab expression + is entered, the Time of Day and Day of Week settings below are ignored. + Cron field order: minute hour day-of-month month day-of-week. type: STRING time_of_day: displayName: Time of Day (HH:MM) description: > The time to run the scan in 24-hour HH:MM format. Used for Daily and - Weekly schedules; ignored for Hourly. Examples: 02:00, 14:30, 20:45. - Defaults to 02:00 if not set. + Weekly schedules; ignored for Hourly and when Frequency is a crontab + expression. Examples: 02:00, 14:30, 20:45. Defaults to 02:00 if not set. type: STRING day_of_week: displayName: Day of Week (Weekly only) description: > The day of the week on which to run the scan when Frequency is set to - Weekly. Valid values: mon, tue, wed, thu, fri, sat, sun. - Defaults to sun if not set. + Weekly. Valid values: mon, tue, wed, thu, fri, sat, sun. Ignored when + Frequency is a crontab expression. Defaults to sun if not set. type: STRING timezone: diff --git a/plugins/stash-scheduler/stash_scheduler.py b/plugins/stash-scheduler/stash_scheduler.py index 9535eea4..2dfd5f2f 100644 --- a/plugins/stash-scheduler/stash_scheduler.py +++ b/plugins/stash-scheduler/stash_scheduler.py @@ -183,15 +183,47 @@ def _parse_time_of_day(raw, warn): return 2, 0 +def _is_cron_expr(value): + """Return True if value looks like a 5-field crontab expression.""" + import re as _re + parts = _re.split(r'\s+', str(value).strip()) + return len(parts) == 5 and any(c in value for c in (' ', '\t')) + + def validate_and_coerce_settings(settings, warn): VALID_FREQUENCIES = {"hourly", "daily", "weekly"} VALID_DAYS = {"mon", "tue", "wed", "thu", "fri", "sat", "sun"} - freq = str(settings.get("frequency", "daily")).strip().lower() - if freq not in VALID_FREQUENCIES: - warn(f"[Stash Scheduler] Invalid frequency {freq!r} — defaulting to 'daily'.") - freq = "daily" - settings["frequency"] = freq + raw_freq = str(settings.get("frequency", "daily")).strip() + + if _is_cron_expr(raw_freq): + # Validate the cron expression via APScheduler if available; otherwise + # trust the structural check and let the daemon catch any errors at start. + cron_valid = True + try: + from apscheduler.triggers.cron import CronTrigger + CronTrigger.from_crontab(raw_freq) + except ImportError: + pass # APScheduler not installed yet; daemon will catch invalid exprs at start + except Exception as exc: + cron_valid = False + warn( + f"[Stash Scheduler] Invalid cron expression {raw_freq!r} ({exc})" + " — defaulting to 'daily'." + ) + + if cron_valid: + settings["frequency"] = "cron" + settings["cron_expr"] = raw_freq + else: + raw_freq = "daily" + + if settings.get("frequency") not in ("cron",): + freq = raw_freq.lower() + if freq not in VALID_FREQUENCIES: + warn(f"[Stash Scheduler] Invalid frequency {freq!r} — defaulting to 'daily'.") + freq = "daily" + settings["frequency"] = freq raw_time = settings.get("time_of_day", "02:00") hour, minute = _parse_time_of_day(raw_time, warn) @@ -604,6 +636,16 @@ def scheduled_job(): if frequency == "hourly": scheduler.add_job(trigger="cron", minute=0, **job_kwargs) log.info(f"Schedule: every hour at :00 ({timezone})") + elif frequency == "cron": + from apscheduler.triggers.cron import CronTrigger + cron_expr = settings["cron_expr"] + try: + cron_trigger = CronTrigger.from_crontab(cron_expr, timezone=timezone) + except Exception as exc: + log.error(f"Invalid cron expression {cron_expr!r}: {exc}. Daemon exiting.") + sys.exit(1) + scheduler.add_job(trigger=cron_trigger, **job_kwargs) + log.info(f"Schedule: crontab '{cron_expr}' ({timezone})") elif frequency == "weekly": scheduler.add_job( trigger="cron", day_of_week=day_of_week, hour=hour, minute=minute, **job_kwargs @@ -691,6 +733,8 @@ def task_start_scheduler(stash, server_connection, settings): tz = settings.get("timezone", "UTC") if freq == "hourly": schedule_desc = f"every hour at :00 ({tz})" + elif freq == "cron": + schedule_desc = f"crontab '{settings['cron_expr']}' ({tz})" elif freq == "weekly": schedule_desc = f"weekly on {dow.upper()} at {time_str} ({tz})" else: