"""JSON-file-backed config: Immich connection, selected album, and cursor state (which photo /frame/image serves next).""" from __future__ import annotations import json import os from pathlib import Path from threading import Lock from pydantic import BaseModel CONFIG_PATH = Path(os.environ.get("CONFIG_PATH", "/data/config.json")) _lock = Lock() class FrameConfig(BaseModel): immich_url: str = "" immich_api_key: str = "" management_token: str = "" # gates the web UI (see main.py); empty = no gate, open on trusted LAN album_id: str = "" order: str = "sequential" # or "shuffle" refresh_interval_s: int = 3600 smart_crop_faces: bool = True # Current photo + upcoming queue (see app/photo_queue.py). current_asset_set_at # is what lets the server decide "has it been long enough to advance" on its # own clock, independent of how/why the device asked for a photo. current_asset_id: str = "" current_asset_set_at: float = 0.0 queue: list[str] = [] queue_cursor: int = 0 # internal bookkeeping for sequential queue top-up; not user-facing queue_target_len: int = 20 # how many upcoming photos to keep queued/shown in the web UI def load() -> FrameConfig: with _lock: if not CONFIG_PATH.exists(): cfg = FrameConfig() else: cfg = FrameConfig(**json.loads(CONFIG_PATH.read_text())) # IMMICH_URL/IMMICH_API_KEY/MANAGEMENT_TOKEN set in the environment # (e.g. docker-compose.yml, see docker-compose.yml.example) take # precedence over whatever's saved in CONFIG_PATH, so credentials never # need to go through the web UI. env_url = os.environ.get("IMMICH_URL") env_key = os.environ.get("IMMICH_API_KEY") env_token = os.environ.get("MANAGEMENT_TOKEN") if env_url: cfg.immich_url = env_url if env_key: cfg.immich_api_key = env_key if env_token: cfg.management_token = env_token return cfg def save(cfg: FrameConfig) -> None: with _lock: CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) CONFIG_PATH.write_text(cfg.model_dump_json(indent=2))