refactor: unify test dispatch with real NotificationDispatcher

- Route scheduled/memory test sends through the same NotificationDispatcher
  the watcher uses — identical template rendering, media handling, caching
- Add preview_url field to MediaAsset (transcoded mid-size), separate from
  thumbnail_url (small) and full_url (original). Dispatcher prefers preview_url
- Fix sendMediaGroup cache: extract file_ids from Telegram response and store
  via async_set_many so repeat sends use cached file_ids
- Parallelize asset downloads in _send_media_group with asyncio.gather
- Filter unprocessed assets (archived/trashed/offline/no-thumbhash) at album
  parse time in ImmichAlbumData.from_api_response
- Extract shared asset_to_media + collect_scheduled_assets into asset_utils.py
  (single source for test dispatch and future real scheduler)
- Respect tracking config filters: limit, asset_type, favorite_only, min_rating
- Random asset sampling for scheduled sends
- Memory mode: "On This Day" date filter (same month+day, previous year)
- Skip dispatch when no matching assets found
- Remove ~250 lines of duplicated send logic from notifier.py
- Fix restart-backend.sh: proper env var export, Python path resolution, error log
This commit is contained in:
2026-03-24 19:32:40 +03:00
parent 1a8c95e942
commit d4cb388c74
12 changed files with 746 additions and 370 deletions
@@ -21,7 +21,8 @@ from ..database.models import (
TrackingConfig,
User,
)
from ..services.notifier import send_real_data_notification, send_test_notification
from ..services.notifier import send_test_notification
from ..services.test_dispatch import dispatch_test_notification
_LOGGER = logging.getLogger(__name__)
@@ -242,62 +243,14 @@ async def test_notification_tracker_target(
r = await send_test_notification(target, locale=effective_locale)
return {"target": target.name, **r}
# For periodic/scheduled/memory — fetch real data from provider
template_config = None
template_str = ""
if tt.template_config_id:
template_config = await session.get(TemplateConfig, tt.template_config_id)
if template_config:
slot_map = {
"periodic": "periodic_summary_message",
"scheduled": "scheduled_assets_message",
"memory": "memory_mode_message",
}
slot_name = slot_map[test_type]
slot_result = await session.exec(
select(TemplateSlot).where(
TemplateSlot.config_id == template_config.id,
TemplateSlot.slot_name == slot_name,
TemplateSlot.locale == effective_locale,
)
)
slot = slot_result.first()
if not slot:
# Fallback: any locale
slot_result2 = await session.exec(
select(TemplateSlot).where(
TemplateSlot.config_id == template_config.id,
TemplateSlot.slot_name == slot_name,
)
)
slot = slot_result2.first()
template_str = slot.template if slot else ""
# Load provider and tracker data eagerly before aiohttp context
provider = await session.get(ServiceProvider, tracker.provider_id)
if not provider:
raise HTTPException(status_code=404, detail="Provider not found")
provider_config = dict(provider.config)
collection_ids = list(tracker.collection_ids or [])
# Load tracking config to get memory_source
memory_source = "albums"
if tt.tracking_config_id:
tracking_config = await session.get(TrackingConfig, tt.tracking_config_id)
if tracking_config:
memory_source = tracking_config.memory_source or "albums"
# Fetch real data from provider
r = await send_real_data_notification(
# For periodic/scheduled/memory — dispatch through the real NotificationDispatcher
r = await dispatch_test_notification(
session=session,
tracker=tracker,
tt=tt,
target=target,
template_str=template_str,
test_type=test_type,
provider_type=provider.type,
provider_config=provider_config,
collection_ids=collection_ids,
date_format=template_config.date_format if template_config else "%d.%m.%Y, %H:%M UTC",
date_only_format=template_config.date_only_format if template_config and template_config.date_only_format else "%d.%m.%Y",
memory_source=memory_source,
locale=effective_locale,
)
return {"target": target.name, **r}