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>
53 lines
2.2 KiB
Python
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")
|