diff --git a/app/disc_images.py b/app/disc_images.py index 7ae45f8..01baec6 100644 --- a/app/disc_images.py +++ b/app/disc_images.py @@ -1,24 +1,21 @@ import hashlib -import io import os from pathlib import Path -import httpx from fastapi import APIRouter, HTTPException -from fastapi.responses import FileResponse -from PIL import Image +from fastapi.responses import FileResponse, RedirectResponse from sqlalchemy import text from .db import engine -# Local WebP cache for Discogs release covers. RecordGod + DealGod share a VPS, so there's no point -# in a remote image service or a second copy — RecordGod compresses + serves covers itself. -# Storage path is CONFIGURABLE (DISC_IMAGE_DIR env): a default now, per-store storage override later -# when shops host their own. Covers are tiny (q72, longest side <=600px). On-demand: converted on first -# request, then served from disk. See RECORDGOD_NAVIGATOR_PLAN / recordgod-engine. +# Release-cover images for RecordGod. The stable URL the UI uses is /img/r/: +# • if a LOCAL WebP copy exists (DISC_IMAGE_DIR), serve it — self-hosted, tiny, store-owned; +# • else 302 → the Discogs thumb. Discogs already serves a ~150px q40 WebP (tiny), and a BROWSER can +# load it even though server-side fetch is Cloudflare-403'd — so we redirect rather than proxy. +# DISC_IMAGE_DIR is CONFIGURABLE (default now; per-store storage override later when shops host their own). +# Local copies get populated by a backfill from WowPlatter's already-downloaded images / the harvester — +# NOT by a server-side i.discogs fetch (that's blocked). Until then, the redirect gives working tiny WebP. IMAGE_DIR = Path(os.getenv("DISC_IMAGE_DIR", "/app/disc_images")) -WEBP_QUALITY = 72 -MAX_DIM = 600 router = APIRouter() @@ -30,24 +27,13 @@ def _path(release_id: int) -> Path: @router.get("/img/r/{release_id}") async def release_image(release_id: int): - """Tiny WebP of a release cover — convert from disc_release.thumb on first hit, then serve from disk.""" p = _path(release_id) - if not p.exists(): - async with engine.connect() as conn: - row = (await conn.execute(text("SELECT thumb FROM disc_release WHERE id = :r"), - {"r": release_id})).first() - src = row[0] if row else None - if not src: - raise HTTPException(404, "no source image") - try: - async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c: - r = await c.get(src, headers={"User-Agent": "Mozilla/5.0 (RecordGod)"}) - r.raise_for_status() - img = Image.open(io.BytesIO(r.content)).convert("RGB") - img.thumbnail((MAX_DIM, MAX_DIM)) - p.parent.mkdir(parents=True, exist_ok=True) - img.save(p, "WEBP", quality=WEBP_QUALITY, method=6) - except Exception: - raise HTTPException(502, "image fetch/convert failed") - return FileResponse(p, media_type="image/webp", - headers={"Cache-Control": "public, max-age=2592000"}) + if p.exists(): + return FileResponse(p, media_type="image/webp", + headers={"Cache-Control": "public, max-age=2592000"}) + async with engine.connect() as conn: + row = (await conn.execute(text("SELECT thumb FROM disc_release WHERE id = :r"), + {"r": release_id})).first() + if row and row[0]: + return RedirectResponse(row[0], status_code=302) + raise HTTPException(404, "no image") diff --git a/requirements.txt b/requirements.txt index 1c7f24d..20a610f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,4 @@ uvicorn[standard] sqlalchemy[asyncio] asyncpg httpx -pillow cryptography