feat(immich): multi-time-point scheduling for scheduled/periodic/memory
Replace the single comma-separated text box with an add/remove list of native HH:MM pickers for the Immich scheduled-assets, periodic-summary, and memory slots. The backend already stored comma-separated *_times and scheduled one cron job per time; this makes entering several times per day discoverable and hardens the read/write path. Backend: - services/time_list.py: normalize_time_list (validate / dedup / sort / cap at 24, raising TimeListError) + lenient parse_hhmm_list; the scheduler now uses the shared parser (drops its private copy). - tracking_configs API normalizes *_times on every write (422 on bad input) and rejects enabling a slot whose times list is empty. - scheduler warns when an enabled slot has zero or dropped fire times, restoring the observability lost with the old per-call warning. Frontend: - TimeListEditor.svelte: add/remove native time rows, dedupe + sort on emit, per-day cap, collapses on-screen duplicates, aria-labelled rows; syncs from the value prop only on external changes (untrack guard) so keyboard entry isn't clobbered mid-edit. - Descriptor-driven save guard: an enabled feature section must have at least one time. - i18n (en/ru) keys; refreshed help text; removed dead invalidTimeList. Tests: time_list normalization/parsing (incl. non-ASCII/odd shapes) and the enabled-implies-times validation.
This commit is contained in:
@@ -7,6 +7,8 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from .time_list import parse_hhmm_list
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -896,30 +898,6 @@ _IMMICH_DISPATCH_KINDS = ("scheduled", "periodic", "memory")
|
||||
_IMMICH_DISPATCH_PREFIX = "immich_dispatch_"
|
||||
|
||||
|
||||
def _parse_hhmm_list(raw: str) -> list[tuple[int, int]]:
|
||||
"""Parse ``"09:00,18:30"`` → ``[(9, 0), (18, 30)]``, skipping bad entries.
|
||||
|
||||
A typo in one slot must not prevent the others from scheduling — we log
|
||||
and move on rather than raising.
|
||||
"""
|
||||
out: list[tuple[int, int]] = []
|
||||
for part in (raw or "").split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
try:
|
||||
h_str, m_str = part.split(":", 1)
|
||||
hour, minute = int(h_str), int(m_str)
|
||||
except ValueError:
|
||||
_LOGGER.warning("Skipping invalid time literal %r", part)
|
||||
continue
|
||||
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||||
_LOGGER.warning("Skipping out-of-range time %r", part)
|
||||
continue
|
||||
out.append((hour, minute))
|
||||
return out
|
||||
|
||||
|
||||
async def _run_immich_dispatch(tracker_id: int, kind: str) -> None:
|
||||
"""APScheduler entry point — wraps the dispatch helper to swallow errors."""
|
||||
from .scheduled_dispatch import dispatch_scheduled_for_tracker
|
||||
@@ -994,7 +972,24 @@ async def _load_immich_dispatch_jobs() -> None:
|
||||
if not getattr(tc, f"{kind}_enabled", False):
|
||||
continue
|
||||
times_raw = getattr(tc, f"{kind}_times", "") or ""
|
||||
for hour, minute in _parse_hhmm_list(times_raw):
|
||||
parsed = parse_hhmm_list(times_raw)
|
||||
# Observability for misconfigured/legacy data: warn when some tokens
|
||||
# were unparseable (the lenient parser drops them silently) and when
|
||||
# an enabled slot resolves to zero fire times (it will never fire).
|
||||
raw_tokens = [p for p in times_raw.split(",") if p.strip()]
|
||||
if len(parsed) < len(raw_tokens):
|
||||
_LOGGER.warning(
|
||||
"Tracker %d %s: dropped %d unparseable time(s) from %r",
|
||||
tracker.id, kind, len(raw_tokens) - len(parsed), times_raw,
|
||||
)
|
||||
if not parsed:
|
||||
_LOGGER.warning(
|
||||
"Tracker %d has %s enabled but no valid fire times (%r); "
|
||||
"slot will not fire",
|
||||
tracker.id, kind, times_raw,
|
||||
)
|
||||
continue
|
||||
for hour, minute in parsed:
|
||||
job_id = f"{_IMMICH_DISPATCH_PREFIX}{kind}_{tracker.id}_{hour:02d}{minute:02d}"
|
||||
scheduler.add_job(
|
||||
_run_immich_dispatch,
|
||||
|
||||
Reference in New Issue
Block a user