Files
siftlode/backend/app/plex/imgcache.py
T
peter e008c48284 fix(plex): address S1 code-review findings
- Restore synchronize_session=False on the remove-bulk Core delete (the 2.0
  default is 'auto'; keep the pre-conversion contract explicit).
- _episode_nav: list(...scalars()) instead of [r[0] for r in ...all()], matching
  the house style used across the conversion.
- Extract app/plex/lookups.by_rating_key() and migrate the get-by-rating_key
  pattern at 13 sites (imgcache x4, _shared.item_or_404, playback show/season,
  browse x6).
- Coverage: add TestFacetsWithData (the converted wsq aggregation with real
  data — the highest-risk untested builder), TestPlaylistAndCollectionEndpoints
  (playlist CRUD + collections list), and an item-state playback test.

Full suite 256 passed, 2 skipped; ruff clean.
2026-08-16 07:06:19 +02:00

69 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
from app.plex.lookups import by_rating_key
_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 = by_rating_key(db, PlexItem, rating_key)
if it is not None:
return it.art_key if variant == "art" else it.thumb_key
sh = by_rating_key(db, PlexShow, rating_key)
if sh is not None:
return sh.art_key if variant == "art" else sh.thumb_key
se = by_rating_key(db, PlexSeason, rating_key)
if se is not None:
return se.thumb_key
col = by_rating_key(db, PlexCollection, rating_key)
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"}
)