RECORDGOD/app/disc_images.py
type-two 2a41731fd3 feat(intake): ScanGod bridge — POST /admin/intake/scan (photo→staged product) + reply
DealGod's ScanGod POSTs camera-captured + bench-measured items; RecordGod resolves barcode→release_id
(local disc_release_identifier 'Barcode' → Discogs fallback), enriches, stores MEASURED weight_g
(overrides the 280g default → real AusPost quotes), dims_mm + scan_id → inventory.attributes,
est_market_value, base64 condition photos → DISC_IMAGE_DIR/items/<sku>/ served at /img/item/<sku>/<n>.
Lands staged (non-destructive review queue). Per-store-token auth (require_token). Smoke-tested live
(barcode→release 782254, 312g measured, photo served). Contract answered in SCANGOD_BRIDGE_REPLY.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:16:18 +10:00

53 lines
2.2 KiB
Python

import hashlib
import os
import re
from pathlib import Path
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, RedirectResponse
from sqlalchemy import text
from .db import engine
# Release-cover images for RecordGod. The stable URL the UI uses is /img/r/<release_id>:
# • 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"))
router = APIRouter()
def _path(release_id: int) -> Path:
h = hashlib.sha256(str(release_id).encode()).hexdigest()
return IMAGE_DIR / h[:2] / f"{h}.webp"
@router.get("/img/r/{release_id}")
async def release_image(release_id: int):
p = _path(release_id)
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")
@router.get("/img/item/{sku}/{n}")
async def item_image(sku: str, n: int):
"""Per-copy condition photos captured by ScanGod (DISC_IMAGE_DIR/items/<sku>/<n>.jpg)."""
if not re.fullmatch(r"[A-Za-z0-9_-]+", sku) or n < 0 or n > 50: # no path traversal
raise HTTPException(404, "no image")
p = IMAGE_DIR / "items" / sku / f"{n}.jpg"
if p.exists():
return FileResponse(p, media_type="image/jpeg",
headers={"Cache-Control": "public, max-age=2592000"})
raise HTTPException(404, "no image")