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>
100 lines
2.9 KiB
Python
100 lines
2.9 KiB
Python
"""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)
|