diff --git a/VERSION b/VERSION index 48f7a71..a37255a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.28.1 +0.28.2 diff --git a/backend/app/plex/client.py b/backend/app/plex/client.py index e9237c4..5871d8c 100644 --- a/backend/app/plex/client.py +++ b/backend/app/plex/client.py @@ -6,6 +6,7 @@ metadata, image proxying, and (P5) optional watch-state write-back. Requests JSO ``Accept: application/json`` header (Plex serves XML by default). """ import logging +from urllib.parse import quote import httpx from sqlalchemy.orm import Session @@ -112,11 +113,24 @@ class PlexClient: items = mc.get("Metadata", []) or [] return items[0] if items else None - def image_bytes(self, image_path: str) -> tuple[bytes, str]: + def image_bytes( + self, image_path: str, width: int | None = None, height: int | None = None + ) -> tuple[bytes, str]: """Raw bytes + content-type of a Plex thumb/art image (proxied to the browser). The - image_path is a Plex path like ``/library/metadata/123/thumb/456``.""" + image_path is a Plex path like ``/library/metadata/123/thumb/456``. + + With width+height, Plex's photo transcoder resizes server-side so the browser fetches a small + image (~tens of KB) instead of the multi-MB full-res original — far faster grid scroll + a + much smaller disk cache. ``minSize=1`` fits within the box; ``upscale=0`` never enlarges.""" + if width and height: + path = ( + f"/photo/:/transcode?width={width}&height={height}" + f"&minSize=1&upscale=0&url={quote(image_path, safe='')}" + ) + else: + path = image_path try: - r = self._http.get(f"{self.base}{image_path}") + r = self._http.get(f"{self.base}{path}") r.raise_for_status() except httpx.HTTPError as e: raise PlexError(str(e)) from e diff --git a/backend/app/routes/plex.py b/backend/app/routes/plex.py index ff8db26..a724cab 100644 --- a/backend/app/routes/plex.py +++ b/backend/app/routes/plex.py @@ -534,6 +534,9 @@ def _image_key(db: Session, rating_key: str, variant: str) -> str | None: _IMG_CACHE = Path(settings.download_root) / ".plex-img-cache" _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. +_IMG_SIZES = {"thumb": (400, 600), "art": (1280, 720)} def _img_cache_read(key: str) -> tuple[bytes, str] | None: @@ -584,7 +587,11 @@ def image(rating_key: str, request: Request, variant: str = "thumb") -> Response fetch. Auth is the signed-session check (no DB user-load).""" _require_session(request) v = "art" if variant == "art" else "thumb" - cache_key = f"item_{rating_key}_{v}" + w, h = _IMG_SIZES[v] + # Size is part of the cache key: Plex resizes server-side (see PlexClient.image_bytes), so we cache + # the small variant. The width tag also means changing a target size just re-caches, never serves a + # stale differently-sized file (old full-size files under the un-tagged key are simply orphaned). + cache_key = f"item_{rating_key}_{v}_{w}" hit = _img_cache_read(cache_key) if hit: return _img_response(*hit) @@ -599,7 +606,7 @@ def image(rating_key: str, request: Request, variant: str = "thumb") -> Response raise HTTPException(status_code=400, detail=str(e)) try: with plex: - data, ct = plex.image_bytes(key) + data, ct = plex.image_bytes(key, w, h) except PlexError as e: raise HTTPException(status_code=502, detail=f"Plex image fetch failed: {e}") _img_cache_write(cache_key, data, ct) diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index 0b00e5e..4131761 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -14,6 +14,14 @@ export interface ReleaseEntry { } export const RELEASE_NOTES: ReleaseEntry[] = [ + { + version: "0.28.2", + date: "2026-07-06", + summary: "Much faster, smoother Plex browsing — right-sized poster images.", + features: [ + "Plex images: posters and art are now resized by Plex to the size actually shown (instead of shipping the full-resolution originals — some were several MB each), so the movie grid scrolls smoothly and images pop in almost instantly. Also shrinks the image cache dramatically.", + ], + }, { version: "0.28.1", date: "2026-07-06",