feat(immich): per-album scheduled/memory dispatch + template tooling
Dispatch: honor {kind}_collection_mode on TrackingConfig — "per_collection"
fans out one event per album; "combined" pools assets as before. Extract
build_immich_dispatch_events shared by cron and test paths.
Assets: collect_scheduled_assets attaches album_name/album_url/album_public_url
to each asset so combined-mode templates can attribute rows to their source
album. Default scheduled_assets templates render a multi-album header with
inline album list and per-row album link; memory_mode follows the same pattern.
UI: "Reset to default" buttons on notification and command template slots
(per-slot and whole-template), backed by new GET /*-template-configs/defaults
endpoints. tracking-configs "Preview template" now opens an inline preview
modal with locale tabs instead of navigating away; Edit button deep-links
with ?edit_slot=<name> so the destination auto-opens the config and scrolls
to the slot. Reset confirmations use ConfirmModal instead of window.confirm.
Fixes:
* NotificationDispatcher._session_ctx infinite recursion when no shared
aiohttp.ClientSession was passed — broke test dispatch for periodic/
scheduled/memory (cron path was unaffected).
* telegram-bots /chats/{id}/test now resolves chat.language_override /
language_code instead of using the raw ?locale query param, matching
the resolution the tracker-target test endpoint already used.
* scheduled_assets default template no longer emits a blank line between
header and the first asset when the multi-album branch is taken.
This commit is contained in:
@@ -120,22 +120,43 @@ async def dispatch_test_notification(
|
||||
),
|
||||
}
|
||||
|
||||
# Fetch assets and build event
|
||||
# Build events (single or per-album) via the shared helper so test and
|
||||
# cron dispatch stay in lockstep on the mode decision.
|
||||
try:
|
||||
event = await _build_event(
|
||||
provider_type=provider.type,
|
||||
provider_config=provider_config,
|
||||
provider_name=provider.name or provider.type,
|
||||
tracker_name=tracker.name or "",
|
||||
tracker_filters=dict(tracker.filters) if tracker.filters else {},
|
||||
collection_ids=collection_ids,
|
||||
test_type=test_type,
|
||||
tracking_config=tracking_config,
|
||||
)
|
||||
if provider.type == "immich" and test_type in ("periodic", "scheduled", "memory"):
|
||||
events = await build_immich_dispatch_events(
|
||||
provider_config=provider_config,
|
||||
provider_name=provider.name or provider.type,
|
||||
tracker_name=tracker.name or "",
|
||||
collection_ids=collection_ids,
|
||||
kind=test_type,
|
||||
tracking_config=tracking_config,
|
||||
)
|
||||
else:
|
||||
ev = await _build_event(
|
||||
provider_type=provider.type,
|
||||
provider_config=provider_config,
|
||||
provider_name=provider.name or provider.type,
|
||||
tracker_name=tracker.name or "",
|
||||
tracker_filters=dict(tracker.filters) if tracker.filters else {},
|
||||
collection_ids=collection_ids,
|
||||
test_type=test_type,
|
||||
tracking_config=tracking_config,
|
||||
)
|
||||
events = [ev] if ev is not None else []
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.exception("Test dispatch event build failed")
|
||||
return {"success": False, "error": f"Provider connection failed: {err}"}
|
||||
if event is None:
|
||||
|
||||
if not events:
|
||||
if test_type in ("scheduled", "memory"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"No matching assets found. Verify the tracker's albums contain assets "
|
||||
"that pass the tracking config filters (favorites only, rating, asset type)."
|
||||
) + (" for today" if test_type == "memory" else ""),
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
@@ -143,24 +164,92 @@ async def dispatch_test_notification(
|
||||
"credentials are valid, and the tracker has collections configured."
|
||||
),
|
||||
}
|
||||
# Periodic summary only needs album stats (extra.albums), not assets — skip the asset check.
|
||||
if not event.added_assets and test_type in ("scheduled", "memory"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"No matching assets found. Verify the tracker's albums contain assets "
|
||||
"that pass the tracking config filters (favorites only, rating, asset type)."
|
||||
) + (" for today" if test_type == "memory" else ""),
|
||||
}
|
||||
|
||||
# Dispatch through the real NotificationDispatcher
|
||||
# Dispatch each event to the same target (per-album fan-out sends N messages).
|
||||
url_cache, asset_cache = await _get_telegram_caches()
|
||||
dispatcher = NotificationDispatcher(url_cache=url_cache, asset_cache=asset_cache)
|
||||
results = await dispatcher.dispatch(event, [target_cfg])
|
||||
all_results: list[dict[str, Any]] = []
|
||||
for event in events:
|
||||
results = await dispatcher.dispatch(event, [target_cfg])
|
||||
if results:
|
||||
all_results.append(results[0])
|
||||
|
||||
if not results:
|
||||
if not all_results:
|
||||
return {"success": False, "error": "No dispatch results"}
|
||||
return results[0]
|
||||
all_ok = all(r.get("success") for r in all_results)
|
||||
if all_ok:
|
||||
return {"success": True, "dispatched": len(all_results)}
|
||||
first_err = next(
|
||||
(r.get("error") for r in all_results if not r.get("success")),
|
||||
"Unknown error",
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": first_err,
|
||||
"dispatched": sum(1 for r in all_results if r.get("success")),
|
||||
"failed": sum(1 for r in all_results if not r.get("success")),
|
||||
}
|
||||
|
||||
|
||||
async def build_immich_dispatch_events(
|
||||
*,
|
||||
provider_config: dict,
|
||||
provider_name: str,
|
||||
tracker_name: str,
|
||||
collection_ids: list[str],
|
||||
kind: str,
|
||||
tracking_config: TrackingConfig | None,
|
||||
) -> list[ServiceEvent]:
|
||||
"""Build the list of ServiceEvents to dispatch for an Immich scheduled kind.
|
||||
|
||||
Single source of truth for the mode decision: ``periodic`` is always one
|
||||
summary event; ``scheduled``/``memory`` honour the ``{kind}_collection_mode``
|
||||
on the tracking config and fan out one event per album in ``per_collection``
|
||||
mode, or one combined event in ``combined`` mode.
|
||||
|
||||
Empty-payload filtering (no assets matched) is applied here so callers get
|
||||
back only events that should actually dispatch. ``periodic`` is exempt —
|
||||
a zero-asset summary is still meaningful (shows album stats only).
|
||||
"""
|
||||
if kind == "periodic":
|
||||
ev = await _build_immich_periodic_event(
|
||||
provider_config=provider_config,
|
||||
provider_name=provider_name,
|
||||
tracker_name=tracker_name,
|
||||
collection_ids=collection_ids,
|
||||
)
|
||||
return [ev] if ev is not None else []
|
||||
|
||||
mode = getattr(
|
||||
tracking_config, f"{kind}_collection_mode", "combined"
|
||||
) or "combined"
|
||||
|
||||
if mode == "per_collection" and len(collection_ids) > 1:
|
||||
events: list[ServiceEvent] = []
|
||||
for aid in collection_ids:
|
||||
ev = await _build_immich_event(
|
||||
provider_config=provider_config,
|
||||
provider_name=provider_name,
|
||||
tracker_name=tracker_name,
|
||||
collection_ids=[aid],
|
||||
test_type=kind,
|
||||
tracking_config=tracking_config,
|
||||
)
|
||||
if ev is not None and ev.added_assets:
|
||||
events.append(ev)
|
||||
return events
|
||||
|
||||
ev = await _build_immich_event(
|
||||
provider_config=provider_config,
|
||||
provider_name=provider_name,
|
||||
tracker_name=tracker_name,
|
||||
collection_ids=collection_ids,
|
||||
test_type=kind,
|
||||
tracking_config=tracking_config,
|
||||
)
|
||||
if ev is None or not ev.added_assets:
|
||||
return []
|
||||
return [ev]
|
||||
|
||||
|
||||
async def _build_event(
|
||||
|
||||
Reference in New Issue
Block a user