"""ESPresso Frame server: pulls photos from Immich, pre-processes them for the panel, and serves the ESP32 a ready-to-display frame.""" from __future__ import annotations import io import random import httpx from fastapi import FastAPI, Form, HTTPException, Request from fastapi.responses import HTMLResponse, Response from fastapi.templating import Jinja2Templates from PIL import Image from . import config from .image_pipeline import render_frame from .immich_client import ImmichClient app = FastAPI(title="ESPresso Frame Server") templates = Jinja2Templates(directory="app/templates") @app.get("/health") def health() -> dict: return {"status": "ok"} @app.get("/", response_class=HTMLResponse) def index(request: Request): cfg = config.load() return templates.TemplateResponse("index.html", {"request": request, "cfg": cfg}) @app.get("/api/albums") def api_albums(): cfg = config.load() if not cfg.immich_url or not cfg.immich_api_key: raise HTTPException(400, "Immich URL/API key not configured yet") try: albums = ImmichClient(cfg.immich_url, cfg.immich_api_key).list_albums() except httpx.HTTPError as e: raise HTTPException(502, f"Could not reach Immich at {cfg.immich_url}: {e}") from e return [{"id": a["id"], "name": a["albumName"], "count": a.get("assetCount", 0)} for a in albums] @app.post("/api/config") def api_config_save( immich_url: str = Form(""), immich_api_key: str = Form(""), album_id: str = Form(""), order: str = Form("sequential"), ): cfg = config.load() cfg.immich_url = immich_url.strip() cfg.immich_api_key = immich_api_key.strip() if album_id != cfg.album_id: cfg.cursor = 0 # restart from the top of a newly selected album cfg.album_id = album_id cfg.order = order if order in ("sequential", "shuffle") else "sequential" config.save(cfg) return {"status": "saved"} @app.get("/frame/image") def frame_image(): cfg = config.load() if not cfg.immich_url or not cfg.immich_api_key: raise HTTPException(400, "Immich URL/API key not configured yet") if not cfg.album_id: raise HTTPException(400, "No album configured yet") client = ImmichClient(cfg.immich_url, cfg.immich_api_key) try: assets = client.list_album_assets(cfg.album_id) except httpx.HTTPError as e: raise HTTPException(502, f"Could not reach Immich at {cfg.immich_url}: {e}") from e if not assets: raise HTTPException(404, "Album has no photos") if cfg.order == "shuffle": asset = random.choice(assets) else: index = cfg.cursor % len(assets) asset = assets[index] cfg.cursor = (index + 1) % len(assets) config.save(cfg) try: jpeg_bytes = client.download_asset_preview(asset["id"]) except httpx.HTTPError as e: raise HTTPException(502, f"Could not download asset from Immich: {e}") from e source = Image.open(io.BytesIO(jpeg_bytes)) frame_bytes = render_frame(source) return Response(content=frame_bytes, media_type="application/octet-stream")