RECORDGOD/app/virtual.py
type-two 9b8ecdb84d feat: deploy-ready — env-aware Discogs enrichment + Dockerfile
- app/mirror.py: centralised enrichment, DISCOGS_MIRROR_KIND selects full (ultra `release`)
  vs vps (`discogs_release`); virtual.py + wowplatter_v1.py use it.
- Dockerfile + .dockerignore for the VPS container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 13:08:49 +10:00

84 lines
3.8 KiB
Python

from fastapi import APIRouter, Depends
from sqlalchemy import text
from .db import get_db
from .mirror import enrich as mirror_enrich
# The virtual store API — PUBLIC read (it's the customer-facing storefront). Replaces
# WowPlatter's payload.php: same scene shape, but from Postgres instead of a full WP bootstrap.
router = APIRouter(prefix="/virtual", tags=["virtual-store"])
async def _all(db, table, where=""):
rows = await db.execute(text(f"SELECT * FROM {table} {where}"))
return [dict(r) for r in rows.mappings()]
def _attach(recs, meta):
for r in recs:
m = meta.get(r["release_id"], {})
r["title"], r["artist"], r["thumb"] = m.get("title"), m.get("artist"), m.get("thumb")
return recs
@router.get("/scene")
async def scene(db=Depends(get_db)):
"""Everything the renderer needs at boot. Records are SLIM — one front cover per crate;
the full per-crate list is lazy-loaded on dig via /virtual/crate/{id}/records."""
spaces = await _all(db, "virtual_space", "WHERE visible='y'")
active = spaces[0] if len(spaces) == 1 else next(
(s for s in spaces if s.get("is_default") == "y"), spaces[0] if spaces else None)
recs = [dict(r) for r in (await db.execute(text("""
SELECT DISTINCT ON (crate_id) crate_id, slot_number, release_id, sku, price, condition
FROM inventory
WHERE in_stock AND crate_id IS NOT NULL AND release_id IS NOT NULL
ORDER BY crate_id, slot_number NULLS LAST"""))).mappings()]
recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs}))
return {
"space": active, "spaces": spaces,
"racks": await _all(db, "virtual_rack", "WHERE visible='y'"),
"rack_types": await _all(db, "virtual_rack_type"),
"rack_type_levels": await _all(db, "virtual_rack_type_level"),
"crates": await _all(db, "virtual_crate", "WHERE visible='y'"),
"crate_types": await _all(db, "virtual_crate_type"),
"cameras": await _all(db, "virtual_camera"),
"lights": await _all(db, "virtual_light"),
"decals": await _all(db, "virtual_decal", "WHERE visible <> 'n'"),
"portals": await _all(db, "virtual_portal"),
"records": recs,
}
@router.get("/crate/{crate_id}/records")
async def crate_records(crate_id: int, db=Depends(get_db)):
"""Full contents of one crate — loaded when the shopper starts digging it."""
recs = [dict(r) for r in (await db.execute(text("""
SELECT release_id, slot_number, sku, price, condition
FROM inventory WHERE crate_id = :c AND in_stock AND release_id IS NOT NULL
ORDER BY slot_number NULLS LAST"""), {"c": crate_id})).mappings()]
recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs}))
return {"ok": True, "crate_id": crate_id, "records": recs}
@router.get("/locate")
async def locate(release_id: int, db=Depends(get_db)):
"""Locate-stock: where in the physical store is this record? Returns the crate + position
so the UI can fly the camera there and flash the bin."""
row = (await db.execute(text("""
SELECT i.crate_id, i.slot_number, i.sku,
c.name, c.label_text, c.pos_x, c.pos_y, c.pos_z
FROM inventory i LEFT JOIN virtual_crate c ON c.id = i.crate_id
WHERE i.release_id = :r AND i.in_stock AND i.crate_id IS NOT NULL
ORDER BY i.slot_number NULLS LAST LIMIT 1"""), {"r": release_id})).mappings().first()
if not row:
return {"ok": True, "located": False, "release_id": release_id}
d = dict(row)
return {
"ok": True, "located": True, "release_id": release_id,
"crate_id": d["crate_id"], "slot_number": d["slot_number"], "sku": d["sku"],
"crate": {"name": d["name"], "label_text": d["label_text"],
"pos": [float(d["pos_x"] or 0), float(d["pos_y"] or 0), float(d["pos_z"] or 0)]},
}