"""Thin on-disk cache + proxy helpers for Plex poster/art images. Posters/art/cast-photos are proxied on demand; without a cache every scroll re-hits Plex over the LAN (janky). We cache the fetched bytes on local disk keyed by a stable id, so repeat views (and other users) serve instantly from disk. On-demand + bounded by the library size; no eviction (a few hundred MB at most for a big library — acceptable, matching the download disk trade-offs). The cache directory (``cache.CACHE_DIR``) is shared with the subtitle cache (see ``subtitles.py``). """ from fastapi import Response from sqlalchemy.orm import Session from app.models import PlexCollection, PlexItem, PlexSeason, PlexShow from app.plex.cache import CACHE_DIR _EXT_CT = [("jpg", "image/jpeg"), ("png", "image/png"), ("webp", "image/webp")] _CT_EXT = {ct: ext for ext, ct in _EXT_CT} # Target box per variant — Plex resizes to these server-side. Posters fill ≤176px cells (grid/info/ # strips), so 400px covers 2× DPR; the faint backdrop art only needs ~1280px. Keeps images ~tens of KB. SIZES = {"thumb": (400, 600), "art": (1280, 720)} def key_for(db: Session, rating_key: str, variant: str) -> str | None: """The stored Plex image path for a known rating_key (item/show/season). Only mirrored keys are proxyable — this is not an open image proxy.""" it = db.query(PlexItem).filter_by(rating_key=rating_key).first() if it is not None: return it.art_key if variant == "art" else it.thumb_key sh = db.query(PlexShow).filter_by(rating_key=rating_key).first() if sh is not None: return sh.art_key if variant == "art" else sh.thumb_key se = db.query(PlexSeason).filter_by(rating_key=rating_key).first() if se is not None: return se.thumb_key col = db.query(PlexCollection).filter_by(rating_key=rating_key).first() if col is not None: return col.thumb_key return None def read(key: str) -> tuple[bytes, str] | None: for ext, ct in _EXT_CT: p = CACHE_DIR / f"{key}.{ext}" try: if p.exists() and p.stat().st_size > 0: return p.read_bytes(), ct except OSError: pass return None def write(key: str, data: bytes, ct: str) -> None: ext = _CT_EXT.get((ct or "").split(";")[0].strip(), "jpg") try: CACHE_DIR.mkdir(parents=True, exist_ok=True) tmp = CACHE_DIR / f"{key}.{ext}.tmp" tmp.write_bytes(data) tmp.replace(CACHE_DIR / f"{key}.{ext}") # atomic publish except OSError: pass # cache is best-effort def response(data: bytes, ct: str) -> Response: return Response( content=data, media_type=ct, headers={"Cache-Control": "public, max-age=86400"} )