fix(images): /img/r serves local copy else 302 to discogs webp (server-side fetch is CF-blocked); drop unused pillow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ddaa6ecedf
commit
0b679ba079
@ -1,24 +1,21 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
from PIL import Image
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from .db import engine
|
from .db import engine
|
||||||
|
|
||||||
# Local WebP cache for Discogs release covers. RecordGod + DealGod share a VPS, so there's no point
|
# Release-cover images for RecordGod. The stable URL the UI uses is /img/r/<release_id>:
|
||||||
# in a remote image service or a second copy — RecordGod compresses + serves covers itself.
|
# • if a LOCAL WebP copy exists (DISC_IMAGE_DIR), serve it — self-hosted, tiny, store-owned;
|
||||||
# Storage path is CONFIGURABLE (DISC_IMAGE_DIR env): a default now, per-store storage override later
|
# • else 302 → the Discogs thumb. Discogs already serves a ~150px q40 WebP (tiny), and a BROWSER can
|
||||||
# when shops host their own. Covers are tiny (q72, longest side <=600px). On-demand: converted on first
|
# load it even though server-side fetch is Cloudflare-403'd — so we redirect rather than proxy.
|
||||||
# request, then served from disk. See RECORDGOD_NAVIGATOR_PLAN / recordgod-engine.
|
# 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"))
|
IMAGE_DIR = Path(os.getenv("DISC_IMAGE_DIR", "/app/disc_images"))
|
||||||
WEBP_QUALITY = 72
|
|
||||||
MAX_DIM = 600
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -30,24 +27,13 @@ def _path(release_id: int) -> Path:
|
|||||||
|
|
||||||
@router.get("/img/r/{release_id}")
|
@router.get("/img/r/{release_id}")
|
||||||
async def release_image(release_id: int):
|
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)
|
p = _path(release_id)
|
||||||
if not p.exists():
|
if p.exists():
|
||||||
async with engine.connect() as conn:
|
return FileResponse(p, media_type="image/webp",
|
||||||
row = (await conn.execute(text("SELECT thumb FROM disc_release WHERE id = :r"),
|
headers={"Cache-Control": "public, max-age=2592000"})
|
||||||
{"r": release_id})).first()
|
async with engine.connect() as conn:
|
||||||
src = row[0] if row else None
|
row = (await conn.execute(text("SELECT thumb FROM disc_release WHERE id = :r"),
|
||||||
if not src:
|
{"r": release_id})).first()
|
||||||
raise HTTPException(404, "no source image")
|
if row and row[0]:
|
||||||
try:
|
return RedirectResponse(row[0], status_code=302)
|
||||||
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c:
|
raise HTTPException(404, "no image")
|
||||||
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"})
|
|
||||||
|
|||||||
@ -3,5 +3,4 @@ uvicorn[standard]
|
|||||||
sqlalchemy[asyncio]
|
sqlalchemy[asyncio]
|
||||||
asyncpg
|
asyncpg
|
||||||
httpx
|
httpx
|
||||||
pillow
|
|
||||||
cryptography
|
cryptography
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user