Backend plumbing for upcoming weekly + monthly Auto-Sync schedules.
PR 1 of 4 in the schedule-types feature — see
``memory/project_auto_sync_schedule_types.md`` for the full plan.
Net behaviour change in this PR: zero. The automation engine still
computes next_run via its existing inline ``_calc_delay_seconds`` /
``_next_weekly_occurrence`` helpers; this module is unused until PR 2
wires the engine through. Lands separately so the foundation can sit
on dev for a beat before the engine change.
``core/automation/schedule.py:next_run_at(trigger_type, trigger_config,
now_utc, default_tz)``:
- Pure function. ``now_utc`` injected (tests freeze time without
monkeypatching ``datetime.now``); ``default_tz`` injected (so daily /
weekly / monthly schedules compute against the USER's timezone, not
the server's — the same class of bug that produced the May 2026
"Auto-Sync next in 8h" timezone fix).
- Returns aware-UTC ``datetime`` ready to serialise to the DB
``next_run`` column, or ``None`` for unrecognised / event-based
triggers (callers should not write a next_run for those).
- Naive ``now_utc`` inputs are assumed UTC for defensive symmetry
with the engine's DB-string parser convention.
Trigger types covered:
- ``schedule``: ``{interval: N, unit: 'minutes'|'hours'|'days'|'weeks'}``
— matches engine's existing ``_calc_delay_seconds``. Unknown unit
defaults to hours; zero/negative interval clamps to 1 (preserves
the engine's guard against scheduling for the past); non-numeric
interval falls back to 1.
- ``daily_time``: ``{time: 'HH:MM', tz: '<IANA>'}`` — DST-aware via
``zoneinfo``; ``tz`` falls back to ``default_tz``; unknown IANA
string falls back to UTC; garbage ``time`` falls back to 00:00.
- ``weekly_time``: ``{time, days: ['mon',...], tz}`` — empty / all-
invalid ``days`` list means "every day" (matches engine fallback);
abbreviations case-insensitive; 8-day scan finds the next match.
- ``monthly_time``: ``{time, day_of_month: 1-31, tz}`` — NEW shape.
Day clamped to [1, 31]. Months too short for the target day clamp
to the LAST valid day rather than skipping a month (standard cron
convention; running a day early in February is less surprising
than missing the whole month). 12-iteration loop cap so a
pathological config can't infinite-loop.
Tests (36 cases, all passing):
- Interval: every unit, unknown-unit fallback, zero/negative/garbage
interval clamp, tz field ignored on interval (wall-clock-independent).
- Daily: today-at-future-time runs today, today-at-past-time rolls to
tomorrow, exact-match rolls to tomorrow (no schedule-now-then-schedule-
again-immediately), user-tz vs server-tz, default_tz fallback,
garbage time / unknown tz defensive returns.
- Weekly: same-day-still-future qualifies, same-day-past rolls to next
allowed day, wraps across week boundary, empty days = every day,
garbage abbreviations dropped, case-insensitive, tz across day
boundary (LA Wednesday evening is Thursday UTC).
- Monthly: target day this month, rolls to next month when passed,
Feb 31 → Feb 28 / Feb 29 leap year, day_of_month above 31 / below
1 clamp, Dec → Jan year roll, user-tz pre-midnight edge case.
- Result-shape contract: every returned datetime is aware UTC at
offset zero (engine relies on this when serialising to the
``next_run`` string column).
Added ``tzdata==2026.2`` to requirements.txt. Windows ``zoneinfo`` and
minimal Docker base images ship without the system tz database;
without ``tzdata`` ``ZoneInfo('America/Los_Angeles')`` raises
``ZoneInfoNotFoundError`` and the helper silently falls back to UTC.
No WHATS_NEW entry — no user-visible behaviour change in this PR.
PR 2 (engine wire-through) will land the user-facing changelog entry
when ``monthly_time`` becomes a real schedulable trigger.
55 lines
1.2 KiB
Text
55 lines
1.2 KiB
Text
# SoulSync requirements
|
|
# Web application dependencies only
|
|
# All dependencies pinned for reproducible builds.
|
|
|
|
# Core web framework
|
|
Flask==3.1.3
|
|
Flask-Limiter==4.1.1
|
|
|
|
# Music service APIs
|
|
spotipy==2.26.0
|
|
PlexAPI==4.18.1
|
|
|
|
# HTTP and async support
|
|
requests==2.33.1
|
|
aiohttp==3.13.5
|
|
|
|
# Security and encryption
|
|
cryptography==48.0.0
|
|
|
|
# Media metadata handling
|
|
mutagen==1.47.0
|
|
Pillow==12.2.0
|
|
|
|
# Text processing
|
|
Unidecode==1.4.0
|
|
beautifulsoup4==4.14.3
|
|
|
|
# System monitoring
|
|
psutil==7.2.2
|
|
|
|
# IANA timezone data — required by ``zoneinfo`` on Windows hosts and
|
|
# minimal Docker base images that ship without the system tz database.
|
|
# Consumed by ``core/automation/schedule.py`` for daily / weekly /
|
|
# monthly schedule next-run computation in the user's local timezone.
|
|
tzdata==2026.2
|
|
|
|
# YouTube support -- unpinned; yt-dlp must track upstream releases to stay functional
|
|
yt-dlp>=2026.3.17
|
|
|
|
# Lyrics support
|
|
lrclibapi==0.3.1
|
|
|
|
# Audio fingerprinting for download verification
|
|
pyacoustid==1.3.1
|
|
|
|
# WebSocket client for Hydrabase connection
|
|
websocket-client==1.9.0
|
|
|
|
# Tidal download support
|
|
tidalapi==0.8.11
|
|
|
|
# WebSocket server for real-time UI updates
|
|
flask-socketio==5.6.1
|
|
gunicorn==26.0.0
|
|
simple-websocket==1.1.0
|