fix(server): honor periodic_interval_days for Immich periodic summary
The APScheduler cron job fires daily at every entry in `periodic_times`, and nothing in the dispatch path consulted `periodic_interval_days` or `periodic_start_date` — so a configured 3-day interval still produced a daily summary. Gate the dispatch in `dispatch_scheduled_for_tracker` for kind=periodic via `(today_in_app_tz - start_date).days % interval == 0`. Log a skip with reason `interval_not_due` on non-firing days so operators can tell suppressed-by-interval apart from other skip causes.
This commit is contained in:
@@ -22,7 +22,9 @@ the test-dispatch path in ``manual_dispatch``.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -64,6 +66,43 @@ _SKIP_REASON_NO_LINKS = "no_enabled_links"
|
|||||||
_SKIP_REASON_NO_EVENT = "provider_returned_no_event"
|
_SKIP_REASON_NO_EVENT = "provider_returned_no_event"
|
||||||
_SKIP_REASON_EMPTY_PAYLOAD = "zero_assets_matched"
|
_SKIP_REASON_EMPTY_PAYLOAD = "zero_assets_matched"
|
||||||
_SKIP_REASON_NO_TARGETS = "no_targets_after_filtering"
|
_SKIP_REASON_NO_TARGETS = "no_targets_after_filtering"
|
||||||
|
_SKIP_REASON_INTERVAL_NOT_DUE = "interval_not_due"
|
||||||
|
|
||||||
|
|
||||||
|
def _periodic_due_today(default_tc: TrackingConfig, app_tz: str) -> bool:
|
||||||
|
"""Return True iff today (in app tz) falls on the periodic schedule.
|
||||||
|
|
||||||
|
The schedule is ``start_date + N * interval_days`` for non-negative N.
|
||||||
|
The cron job fires every day at the configured ``periodic_times`` — this
|
||||||
|
gate skips the dispatch on days that aren't a multiple of the interval
|
||||||
|
from the start date, so a user-configured ``interval_days=3`` fires every
|
||||||
|
third day rather than every day.
|
||||||
|
|
||||||
|
Defensive: an interval ≤ 1 is treated as "fire daily"; an unparseable
|
||||||
|
``periodic_start_date`` is treated as "fire" so a bad config doesn't
|
||||||
|
silently stop all summaries (operators see notifications and can fix it).
|
||||||
|
"""
|
||||||
|
interval = max(1, int(getattr(default_tc, "periodic_interval_days", 1) or 1))
|
||||||
|
if interval <= 1:
|
||||||
|
return True
|
||||||
|
raw_start = getattr(default_tc, "periodic_start_date", "") or ""
|
||||||
|
try:
|
||||||
|
start = datetime.strptime(raw_start, "%Y-%m-%d").date()
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Invalid periodic_start_date %r; treating periodic as due today",
|
||||||
|
raw_start,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
tz = ZoneInfo(app_tz)
|
||||||
|
except (ZoneInfoNotFoundError, ValueError):
|
||||||
|
tz = ZoneInfo("UTC")
|
||||||
|
today = datetime.now(tz).date()
|
||||||
|
delta_days = (today - start).days
|
||||||
|
if delta_days < 0:
|
||||||
|
return False
|
||||||
|
return delta_days % interval == 0
|
||||||
|
|
||||||
|
|
||||||
async def _log_skip(
|
async def _log_skip(
|
||||||
@@ -181,6 +220,28 @@ async def dispatch_scheduled_for_tracker(
|
|||||||
collection_ids = list(tracker.collection_ids or [])
|
collection_ids = list(tracker.collection_ids or [])
|
||||||
|
|
||||||
app_tz = await get_app_timezone(session)
|
app_tz = await get_app_timezone(session)
|
||||||
|
|
||||||
|
# Periodic summary respects an N-day interval starting from
|
||||||
|
# ``periodic_start_date`` — the cron job fires every day at
|
||||||
|
# ``periodic_times``, so without this gate a configured
|
||||||
|
# ``interval_days=3`` would still dispatch daily. Other kinds
|
||||||
|
# (scheduled/memory) don't have an interval setting.
|
||||||
|
if kind == "periodic" and not _periodic_due_today(default_tc, app_tz):
|
||||||
|
_LOGGER.info(
|
||||||
|
"Periodic for tracker %d: not due today (interval=%s, start=%s)",
|
||||||
|
tracker_id,
|
||||||
|
getattr(default_tc, "periodic_interval_days", None),
|
||||||
|
getattr(default_tc, "periodic_start_date", None),
|
||||||
|
)
|
||||||
|
await _log_skip(
|
||||||
|
tracker_id, kind, _SKIP_REASON_INTERVAL_NOT_DUE,
|
||||||
|
tracker_user_id=tracker.user_id,
|
||||||
|
tracker_name=tracker.name or "",
|
||||||
|
provider_id=provider.id,
|
||||||
|
provider_name=provider.name or provider.type,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
link_data = await load_link_data(session, tracker_id)
|
link_data = await load_link_data(session, tracker_id)
|
||||||
|
|
||||||
if not link_data:
|
if not link_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user