"""Thin wrapper around the bits of the Immich API this project needs.""" from __future__ import annotations import httpx class ImmichClient: def __init__(self, base_url: str, api_key: str): self.base_url = base_url.rstrip("/") self._headers = {"x-api-key": api_key} def list_albums(self) -> list[dict]: resp = httpx.get(f"{self.base_url}/api/albums", headers=self._headers, timeout=10) resp.raise_for_status() return resp.json() def list_album_assets(self, album_id: str) -> list[dict]: """GET /api/albums/{id} doesn't embed assets in this Immich version (AlbumResponseDto only has assetCount) -- assets live behind the general search API instead, filtered by albumIds. Only returns the first page. Fine for a photo frame cycling through an album; worth adding nextPage pagination if someone points this at an album large enough to need it. """ resp = httpx.post( f"{self.base_url}/api/search/metadata", headers=self._headers, json={"albumIds": [album_id]}, timeout=15, ) resp.raise_for_status() return resp.json().get("assets", {}).get("items", []) def get_asset_faces(self, asset_id: str) -> list[dict]: """Face bounding boxes Immich already computed for its own People feature -- reused here to bias cropping toward keeping faces on screen instead of running our own detection.""" resp = httpx.get( f"{self.base_url}/api/faces", params={"id": asset_id}, headers=self._headers, timeout=10, ) resp.raise_for_status() return resp.json() def download_asset_preview(self, asset_id: str) -> bytes: resp = httpx.get( f"{self.base_url}/api/assets/{asset_id}/thumbnail", params={"size": "preview"}, headers=self._headers, timeout=30, ) resp.raise_for_status() return resp.content def download_asset_thumbnail(self, asset_id: str) -> tuple[bytes, str]: """Smaller than download_asset_preview -- used for the web UI's upcoming-photos list, not the actual rendered frame. Returns (content, content_type) since this one gets proxied straight to a browser tag and needs a correct Content-Type header.""" resp = httpx.get( f"{self.base_url}/api/assets/{asset_id}/thumbnail", params={"size": "thumbnail"}, headers=self._headers, timeout=30, ) resp.raise_for_status() return resp.content, resp.headers.get("content-type", "image/jpeg")