perf(plex): a card fetches its picture at a card's size, not the detail page's
Cards fetched the full cuts: a 400x600 poster (~38 KB) for a ~144px picture and the 1280x720 backdrop (~162 KB) for a ~212px shelf picture. The image proxy now also cuts thumb_sm (240x360) and art_sm (480x270), cached apart; card pictures (grid, shelves, collection tiles, Up next) carry both in srcset with sizes=auto, so the browser picks by laid-out width and DPR. Localdev: 17 KB a poster, 56 KB a backdrop. The detail page keeps the full cuts.
This commit is contained in:
@@ -23,9 +23,22 @@ log = logs.get_logger(__name__)
|
||||
|
||||
_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)}
|
||||
# Target box per variant — Plex resizes to these server-side. The full poster (400px) covers a detail
|
||||
# page's ≤224px poster at 2× DPR, the full art (1280px) a hero backdrop. A card is much smaller: a grid
|
||||
# poster is ~144px and a shelf's landscape art ~212px, so the `_sm` pair serves those at 1.33× DPR
|
||||
# (E31 S4e: cards fetched the 400 poster, 42 KB, and the 1280 art for a 212px picture). The browser
|
||||
# picks between a pair itself (`srcset`, the frontend's `plexSrcSet`).
|
||||
SIZES = {
|
||||
"thumb": (400, 600),
|
||||
"thumb_sm": (240, 360),
|
||||
"art": (1280, 720),
|
||||
"art_sm": (480, 270),
|
||||
}
|
||||
|
||||
|
||||
def picture(variant: str) -> str:
|
||||
"""Which stored picture a size variant is cut from: the poster (`thumb`) or the backdrop (`art`)."""
|
||||
return "art" if variant.startswith("art") else "thumb"
|
||||
|
||||
|
||||
def key_for(db: Session, rating_key: str, variant: str) -> str | None:
|
||||
|
||||
@@ -65,7 +65,7 @@ def image(rating_key: str, request: Request, variant: str = "thumb") -> Response
|
||||
brief session only to resolve the image key + read the Plex config, then RELEASES it before the
|
||||
fetch. Auth is the signed-session check (no DB user-load)."""
|
||||
_require_session(request)
|
||||
v = "art" if variant == "art" else "thumb"
|
||||
v = variant if variant in imgcache.SIZES else "thumb"
|
||||
w, h = imgcache.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
|
||||
@@ -77,7 +77,7 @@ def image(rating_key: str, request: Request, variant: str = "thumb") -> Response
|
||||
# Cold miss: short-lived session for the key + Plex client config, released before the slow fetch.
|
||||
try:
|
||||
with SessionLocal() as db:
|
||||
key = imgcache.key_for(db, str(rating_key), v)
|
||||
key = imgcache.key_for(db, str(rating_key), imgcache.picture(v))
|
||||
if not key:
|
||||
raise not_found("image")
|
||||
plex = PlexClient(db) # reads base URL + token now; the fetch below needs no DB
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""A card asks the Plex image proxy for a small cut; a detail page keeps the full one (E31 S4e).
|
||||
|
||||
Cards fetched the 400×600 poster for a ~144px picture and the 1280×720 backdrop for a ~212px shelf
|
||||
picture. The `_sm` variants are cut by Plex at a card's size and cached apart from the full ones; an
|
||||
unknown variant still gets the full poster. DB lane (the proxy opens its own short session).
|
||||
"""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.state as app_state
|
||||
from app.main import app
|
||||
from app.models import PlexItem, PlexLibrary
|
||||
from app.plex import imgcache
|
||||
from app.routes.plex import playback
|
||||
|
||||
|
||||
class _FakePlex:
|
||||
calls: list[tuple[str, int | None, int | None]] = []
|
||||
|
||||
def __init__(self, db) -> None:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc) -> None:
|
||||
return None
|
||||
|
||||
def image_bytes(self, key, width=None, height=None):
|
||||
_FakePlex.calls.append((key, width, height))
|
||||
return b"\xff\xd8jpeg", "image/jpeg"
|
||||
|
||||
|
||||
def _setup(db, make_user, monkeypatch, tmp_path) -> TestClient:
|
||||
user = make_user()
|
||||
lib = PlexLibrary(plex_key="90", title="Movies", kind="movie", enabled=True)
|
||||
db.add(lib)
|
||||
db.flush()
|
||||
db.add(
|
||||
PlexItem(
|
||||
rating_key="pm1",
|
||||
library_id=lib.id,
|
||||
kind="movie",
|
||||
title="A Movie",
|
||||
thumb_key="/library/metadata/pm1/thumb/1",
|
||||
art_key="/library/metadata/pm1/art/1",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
monkeypatch.setattr(app_state, "_configured_cache", True)
|
||||
monkeypatch.setattr(playback, "resolved_user_id", lambda request: (user.id, True))
|
||||
monkeypatch.setattr(playback, "PlexClient", _FakePlex)
|
||||
monkeypatch.setattr(imgcache, "CACHE_DIR", tmp_path)
|
||||
_FakePlex.calls = []
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_each_variant_is_cut_at_its_own_size(db, make_user, monkeypatch, tmp_path):
|
||||
client = _setup(db, make_user, monkeypatch, tmp_path)
|
||||
for variant in ("thumb", "thumb_sm", "art", "art_sm"):
|
||||
assert client.get(f"/api/plex/image/pm1?variant={variant}").status_code == 200
|
||||
assert _FakePlex.calls == [
|
||||
("/library/metadata/pm1/thumb/1", 400, 600),
|
||||
("/library/metadata/pm1/thumb/1", 240, 360),
|
||||
("/library/metadata/pm1/art/1", 1280, 720),
|
||||
("/library/metadata/pm1/art/1", 480, 270),
|
||||
]
|
||||
# Each cut is cached under its own name: asking again reaches Plex no more.
|
||||
for variant in ("thumb", "thumb_sm", "art", "art_sm"):
|
||||
client.get(f"/api/plex/image/pm1?variant={variant}")
|
||||
assert len(_FakePlex.calls) == 4
|
||||
|
||||
|
||||
def test_an_unknown_variant_gets_the_full_poster(db, make_user, monkeypatch, tmp_path):
|
||||
client = _setup(db, make_user, monkeypatch, tmp_path)
|
||||
assert client.get("/api/plex/image/pm1?variant=huge").status_code == 200
|
||||
assert _FakePlex.calls == [("/library/metadata/pm1/thumb/1", 400, 600)]
|
||||
Reference in New Issue
Block a user