Files
haos-hacs-immich-album-watcher/packages/server/src/immich_watcher_server/api/template_configs.py
alexei.dolgolyov 90b4713d5c
Some checks failed
Validate / Hassfest (push) Has been cancelled
Restructure data model: TrackingConfig + TemplateConfig entities
Major model restructuring for clean separation of concerns:

New entities:
- TrackingConfig: What to react to (event types, asset filters,
  periodic/scheduled/memory mode config) - reusable across targets
- TemplateConfig: All ~15 template slots from blueprint (event
  messages, asset formatting, date/location, scheduled messages)
  with full defaults - separate entities per locale

Changed entities:
- AlbumTracker: Simplified to album selection + polling + target_ids
  (removed event_types, template_id, all filter fields)
- NotificationTarget: Extended with tracking_config_id and
  template_config_id FKs (many-to-one, reusable configs)

Removed entities:
- MessageTemplate (replaced by TemplateConfig)
- ScheduledJob (absorbed into TrackingConfig)

Updated services:
- watcher.py: Each target checked against its own tracking_config
  for event filtering before sending notification
- notifier.py: Uses target's template_config to select the right
  template slot based on event type

New API routes:
- /api/tracking-configs/* (CRUD)
- /api/template-configs/* (CRUD + per-slot preview)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:57:19 +03:00

161 lines
5.3 KiB
Python

"""Template configuration CRUD API routes."""
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from jinja2.sandbox import SandboxedEnvironment
from ..auth.dependencies import get_current_user
from ..database.engine import get_session
from ..database.models import TemplateConfig, User
router = APIRouter(prefix="/api/template-configs", tags=["template-configs"])
# Sample data for template preview
_SAMPLE_CONTEXT = {
"album_name": "Family Photos",
"album_url": "https://immich.example.com/share/abc123",
"album_created": "01.01.2024",
"album_updated": "19.03.2026",
"added_count": 3,
"removed_count": 0,
"change_type": "assets_added",
"people": "Alice, Bob",
"assets": "\n • 🖼️ IMG_001.jpg\n • 🖼️ IMG_002.jpg\n • 🎬 VID_003.mp4",
"common_date": " from 19.03.2026",
"common_location": " in Paris, France",
"video_warning": "",
"old_name": "Old Album",
"new_name": "New Album",
"album_count": 5,
"albums": "\n • Family Photos: https://example.com/share/abc",
"asset_count": 10,
"more_count": 7,
}
class TemplateConfigCreate(BaseModel):
name: str
message_assets_added: str | None = None
message_assets_removed: str | None = None
message_album_renamed: str | None = None
message_album_deleted: str | None = None
message_asset_image: str | None = None
message_asset_video: str | None = None
message_assets_format: str | None = None
message_assets_more: str | None = None
message_people_format: str | None = None
date_format: str | None = None
common_date_template: str | None = None
date_if_unique_template: str | None = None
location_format: str | None = None
common_location_template: str | None = None
location_if_unique_template: str | None = None
favorite_indicator: str | None = None
periodic_summary_message: str | None = None
periodic_album_template: str | None = None
scheduled_assets_message: str | None = None
memory_mode_message: str | None = None
video_warning: str | None = None
TemplateConfigUpdate = TemplateConfigCreate # Same shape, all optional
@router.get("")
async def list_configs(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
result = await session.exec(
select(TemplateConfig).where(TemplateConfig.user_id == user.id)
)
return [_response(c) for c in result.all()]
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_config(
body: TemplateConfigCreate,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
data = {k: v for k, v in body.model_dump().items() if v is not None}
config = TemplateConfig(user_id=user.id, **data)
session.add(config)
await session.commit()
await session.refresh(config)
return _response(config)
@router.get("/{config_id}")
async def get_config(
config_id: int,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
return _response(await _get(session, config_id, user.id))
@router.put("/{config_id}")
async def update_config(
config_id: int,
body: TemplateConfigUpdate,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
config = await _get(session, config_id, user.id)
for field, value in body.model_dump(exclude_unset=True).items():
if value is not None:
setattr(config, field, value)
session.add(config)
await session.commit()
await session.refresh(config)
return _response(config)
@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_config(
config_id: int,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
config = await _get(session, config_id, user.id)
await session.delete(config)
await session.commit()
@router.post("/{config_id}/preview")
async def preview_config(
config_id: int,
slot: str = "message_assets_added",
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Render a specific template slot with sample data."""
config = await _get(session, config_id, user.id)
template_body = getattr(config, slot, None)
if template_body is None:
raise HTTPException(status_code=400, detail=f"Unknown slot: {slot}")
try:
env = SandboxedEnvironment(autoescape=False)
tmpl = env.from_string(template_body)
rendered = tmpl.render(**_SAMPLE_CONTEXT)
return {"slot": slot, "rendered": rendered}
except Exception as e:
raise HTTPException(status_code=400, detail=f"Template error: {e}")
def _response(c: TemplateConfig) -> dict:
return {k: getattr(c, k) for k in TemplateConfig.model_fields if k != "user_id"} | {
"created_at": c.created_at.isoformat()
}
async def _get(session: AsyncSession, config_id: int, user_id: int) -> TemplateConfig:
config = await session.get(TemplateConfig, config_id)
if not config or config.user_id != user_id:
raise HTTPException(status_code=404, detail="Template config not found")
return config