Add standalone FastAPI server backend (Phase 3)
Some checks failed
Validate / Hassfest (push) Has been cancelled
Some checks failed
Validate / Hassfest (push) Has been cancelled
Build a complete standalone web server for Immich album change notifications, independent of Home Assistant. Uses the shared core library from Phase 1. Server features: - FastAPI with async SQLite (SQLModel + aiosqlite) - Multi-user auth with JWT (admin/user roles, setup wizard) - CRUD APIs: Immich servers, album trackers, message templates, notification targets (Telegram + webhook), user management - APScheduler background polling per tracker - Jinja2 template rendering with live preview - Album browser proxied from Immich API - Event logging and dashboard status endpoint - Docker deployment (single container, SQLite in volume) 39 API routes, 14 integration tests passing. Also adds Phase 6 (Claude AI Telegram bot enhancement) to the primary plan as an optional future phase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Background job scheduler for album polling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from ..database.engine import get_engine
|
||||
from ..database.models import AlbumTracker
|
||||
from .watcher import check_tracker
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_scheduler: AsyncIOScheduler | None = None
|
||||
|
||||
|
||||
def get_scheduler() -> AsyncIOScheduler:
|
||||
"""Get the global scheduler instance."""
|
||||
global _scheduler
|
||||
if _scheduler is None:
|
||||
_scheduler = AsyncIOScheduler()
|
||||
return _scheduler
|
||||
|
||||
|
||||
async def start_scheduler() -> None:
|
||||
"""Start the scheduler and load all enabled trackers."""
|
||||
scheduler = get_scheduler()
|
||||
|
||||
engine = get_engine()
|
||||
async with AsyncSession(engine) as session:
|
||||
result = await session.exec(
|
||||
select(AlbumTracker).where(AlbumTracker.enabled == True) # noqa: E712
|
||||
)
|
||||
trackers = result.all()
|
||||
|
||||
for tracker in trackers:
|
||||
_add_tracker_job(scheduler, tracker.id, tracker.scan_interval)
|
||||
|
||||
scheduler.start()
|
||||
_LOGGER.info("Scheduler started with %d tracker jobs", len(trackers))
|
||||
|
||||
|
||||
async def stop_scheduler() -> None:
|
||||
"""Stop the scheduler."""
|
||||
scheduler = get_scheduler()
|
||||
if scheduler.running:
|
||||
scheduler.shutdown(wait=False)
|
||||
_LOGGER.info("Scheduler stopped")
|
||||
|
||||
|
||||
def add_tracker_job(tracker_id: int, scan_interval: int) -> None:
|
||||
"""Add or update a scheduler job for a tracker."""
|
||||
scheduler = get_scheduler()
|
||||
_add_tracker_job(scheduler, tracker_id, scan_interval)
|
||||
|
||||
|
||||
def remove_tracker_job(tracker_id: int) -> None:
|
||||
"""Remove a scheduler job for a tracker."""
|
||||
scheduler = get_scheduler()
|
||||
job_id = f"tracker_{tracker_id}"
|
||||
if scheduler.get_job(job_id):
|
||||
scheduler.remove_job(job_id)
|
||||
_LOGGER.debug("Removed scheduler job for tracker %d", tracker_id)
|
||||
|
||||
|
||||
def _add_tracker_job(
|
||||
scheduler: AsyncIOScheduler, tracker_id: int, scan_interval: int
|
||||
) -> None:
|
||||
"""Add or replace a scheduler job."""
|
||||
job_id = f"tracker_{tracker_id}"
|
||||
|
||||
# Remove existing job if present
|
||||
if scheduler.get_job(job_id):
|
||||
scheduler.remove_job(job_id)
|
||||
|
||||
scheduler.add_job(
|
||||
_run_tracker_check,
|
||||
trigger=IntervalTrigger(seconds=scan_interval),
|
||||
id=job_id,
|
||||
args=[tracker_id],
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Scheduled tracker %d every %d seconds", tracker_id, scan_interval
|
||||
)
|
||||
|
||||
|
||||
async def _run_tracker_check(tracker_id: int) -> None:
|
||||
"""Run a single tracker check (called by scheduler)."""
|
||||
try:
|
||||
result = await check_tracker(tracker_id)
|
||||
_LOGGER.debug("Tracker %d check result: %s", tracker_id, result)
|
||||
except Exception:
|
||||
_LOGGER.exception("Error checking tracker %d", tracker_id)
|
||||
Reference in New Issue
Block a user