feat(inventory): CRUD backend (add/edit/delete/bulk) — first cream from the WowPlatter audit; + migration audit doc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0b679ba079
commit
4ece321b76
192
WOWPLATTER_MIGRATION_AUDIT.md
Normal file
192
WOWPLATTER_MIGRATION_AUDIT.md
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# WowPlatter → RecordGod — feature migration audit
|
||||||
|
|
||||||
|
Cycle through every WowPlatter wp-admin page, assess **cream vs bloat**, and spec what to migrate.
|
||||||
|
Legend: **🟢 CREAM** (migrate) · **🔴 BLOAT** (drop) · **🟡 UNSURE** (regroup with John) · **✅ DONE** (already in RecordGod).
|
||||||
|
Menu = 2 roots (WowPlatter / SOUND) + 28 submenu pages. Source: `admin/partials/*`, `admin/class-*-ajax-handler.php`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STOCK group
|
||||||
|
|
||||||
|
### 1. Dashboard — `dashboard.php` · ✅ DONE (parity to extend)
|
||||||
|
Purpose: at-a-glance — Top Performers (last 30 days), stock value, counts, quick actions.
|
||||||
|
RecordGod: `/admin` dashboard already has counts/stock-value/sales/quick-actions. **Migrate the "Top Performers (30d)"
|
||||||
|
+ best-sellers widget** onto it (from `sale_items` we already have). Verdict: 🟢 (small add to the existing dash).
|
||||||
|
|
||||||
|
### 2. Search — `search.php` · ✅ DONE / in progress
|
||||||
|
Scanner / Returns / Reorganize / Collections / Stock Finder + Store→Rack→Crate→Item navigator.
|
||||||
|
RecordGod: navigator (store→rack→crate→locate), Collections (CRUD + editor + fallback locator), Reorganize **backend**
|
||||||
|
(`/nav/reorg/scan,apply,compress`) all built. **Pending UIs: Scanner (scan-to-slot), Returns, Reorganize walkthrough.**
|
||||||
|
Verdict: 🟢 core — finish the 3 pending UIs.
|
||||||
|
|
||||||
|
### 3. Inventory — `inventory/{main,list,form,bulk,meta-catalog,discogs-marketplace,ebay-marketplace}.php` · 🟢 CREAM (vital, not yet built)
|
||||||
|
The actual stock manager. **list.php** = Current Stock table (search, edit, delete, bulk actions). **form.php** = add/edit
|
||||||
|
one item. **bulk.php** = 4 tabs: IMPORT / DISCOGS / SYNC / BULK ACTIONS (import missing release data+images, push to Woo).
|
||||||
|
ajax: `add_inventory_item`, `delete_inventory_item`, `sync_inventory_to_woo`, `create_woo_product_from_inventory`,
|
||||||
|
`import_missing_data`, `inventory_changes_log`. RecordGod has the `inventory` TABLE + intake API but **no list/edit/bulk UI**.
|
||||||
|
→ **Migrate: the inventory list (search/filter/edit/delete/bulk) + single-item add/edit form.** Skip eBay-marketplace tab
|
||||||
|
(🔴, not used). Woo-sync stays (commerce). Verdict: 🟢 — THE core back-office screen still missing.
|
||||||
|
|
||||||
|
### 4. Import — `import/{main,other,reference-conversion}.php` · 🟢 CREAM
|
||||||
|
Bulk-get-stock-in: paste release IDs / catnos / barcodes → match to Discogs → create inventory. **reference-conversion** =
|
||||||
|
turn catalog numbers / identifiers into release_ids (`convert_references`, `convert_discogs_references`). ajax:
|
||||||
|
`execute_discogs_import`, `execute_discogs_selected_import`, `check_import_progress`, `cancel_import`, `convert_*_references`.
|
||||||
|
RecordGod: has intake API + the disc_release mirror (so matching is local now). → **Migrate: the bulk import UI
|
||||||
|
(paste list → resolve against disc_release/identifier → review → create inventory).** Verdict: 🟢.
|
||||||
|
|
||||||
|
### 5. Print — `print.php` + `label-designer.php` · 🟢 CREAM
|
||||||
|
Tabs: Preview / Settings / Label Designer. Thermal/price-label printing with a template designer. ajax:
|
||||||
|
`ajax_{save,get,delete}_label_template`, `get_label_templates_by_type`. RecordGod: PriceGod does label printing via the
|
||||||
|
rfid-daemon; RecordGod itself has no label UI. → **Migrate: label templates + print (artist/title/price/QR/SKU), reuse the
|
||||||
|
PriceGod/daemon print path.** Designer is nice-to-have. Verdict: 🟢 (templates + print core; 🟡 the full visual designer).
|
||||||
|
|
||||||
|
### 6. Reports — `reports/audio.php` (+ sales/stock reports) · 🟡 UNSURE (depth)
|
||||||
|
The Reports menu surfaces sales/stock/audio analytics. RecordGod has a basic `/admin/reports` (revenue, by-month, top items).
|
||||||
|
→ **Question for John: how deep do reports need to go?** Basic sales+stock reports = 🟢; the audio/Beatport/MusicBrainz
|
||||||
|
report tabs lean enrichment-niche. Verdict: 🟡 — confirm which report tabs are cream.
|
||||||
|
|
||||||
|
### 7. Analyse — `analyse.php` · 🟡 UNSURE
|
||||||
|
"Analyse Inventory" — analysis + data-cleaning tools (find dupes, missing data, anomalies). Could be genuinely useful
|
||||||
|
(stock hygiene) or overlap with Import's "missing data". → **Question: is this a distinct tool you use, or covered by
|
||||||
|
Import + Inventory bulk?** Verdict: 🟡.
|
||||||
|
|
||||||
|
### 8. Wiki — `wiki.php` · 🟡 UNSURE
|
||||||
|
"Wiki Lookup & Editor" — per-release wiki/notes lookup + editing (wiki_extract enrichment). Niche enrichment; DealGod/
|
||||||
|
discogs already carry notes. → **Question: do you actually use the wiki editor, or is the release `notes`/`description`
|
||||||
|
field enough?** Verdict: 🟡 (lean 🔴).
|
||||||
|
|
||||||
|
### 9. Other — (misc tools) · 🟡 UNSURE
|
||||||
|
Catch-all "Other" menu — need to confirm what's parked here (likely odds-and-ends tools). Verdict: 🟡 — confirm contents.
|
||||||
|
|
||||||
|
### 10. Virtual Store — `virtual/*` · ✅ DONE
|
||||||
|
3D store ported to RecordGod (raw Three.js, coordinate convention fixed, 3 rooms render). Editing tools (rack/crate
|
||||||
|
placement) still WP-side — 🟡 do we need an in-RecordGod scene editor, or keep placing via the navigator? Verdict: ✅ core done.
|
||||||
|
|
||||||
|
### 11. Gig Guide — `gig-guide.php` · ✅ DONE (lives in DealGod)
|
||||||
|
GigGod is already in DealGod (`live_gig` + `/api/gigs/structured` + `/touring`); WowPlatter/RecordGod consume via API.
|
||||||
|
Verdict: ✅ — no migration, consume the DealGod API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SALES group
|
||||||
|
|
||||||
|
### 12. Sales — `sales/*` · ✅ DONE
|
||||||
|
POS (ring-up/discounts/payment-plans/locate/past-sales/promotions/import/settings) built in RecordGod `/pos`.
|
||||||
|
Pending: Email-receipt + Square tabs. Verdict: ✅ core done.
|
||||||
|
|
||||||
|
### 13. Customers — `customers.php` · 🟢 CREAM
|
||||||
|
Full CRM: tabs Overview / Sales / Loyalty / Wantlist / Mailing / Messages / Info per customer. RecordGod has a thin
|
||||||
|
`customer` table + POS picker only. → **Migrate: the customer record page (history, wantlist, contact, mailing prefs).**
|
||||||
|
Verdict: 🟢 (Loyalty/Messages tabs depend on those features below).
|
||||||
|
|
||||||
|
### 14. Wantlist — `wantlist.php` · 🟡 UNSURE (lean 🟢)
|
||||||
|
Customer wantlists — "who wants what" → buy signals + notify-on-arrival. Genuinely useful for a record store (match
|
||||||
|
incoming stock to wants). → **Question: priority? It pairs with Customers + intake.** Verdict: 🟡→🟢.
|
||||||
|
|
||||||
|
### 15. Inbox — (customer messages) · 🟡 UNSURE
|
||||||
|
Customer message inbox (ties to Messaging/Beeper). → **Question: do you run customer messaging through here, or external
|
||||||
|
(email/socials)?** Verdict: 🟡.
|
||||||
|
|
||||||
|
### 16. Loyalty — `loyalty-{users,reports,settings}.php` · 🟡 UNSURE
|
||||||
|
Points/tiers/rewards program (`loyalty_ledger`, `loyalty_tiers`, `loyalty_settings`). Real feature but is a loyalty program
|
||||||
|
cream for the shop right now? → **Question: are you running loyalty, or is it aspirational?** Verdict: 🟡.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FRONTEND group
|
||||||
|
|
||||||
|
### 17. Profile Editor — `profile-editor.php` · 🟡 UNSURE
|
||||||
|
Customer-facing profile editing (frontend account). Storefront concern; RecordGod's storefront is nascent. Verdict: 🟡.
|
||||||
|
|
||||||
|
### 18. Pages — `pages.php` · 🟢 CREAM (partly done)
|
||||||
|
Storefront page builder: tabs General / Sales / Header / Welcome / FAQ / SEO / URL-structure / Placeholder. RecordGod has
|
||||||
|
the `/builder` (theme + brand import). → **Migrate the content bits (welcome/FAQ/SEO/header copy) into the builder.**
|
||||||
|
Verdict: 🟢 (fold into the existing builder).
|
||||||
|
|
||||||
|
### 19. Audio — `audio.php` · 🟡 UNSURE (split)
|
||||||
|
Tabs: MusicBrainz / Streaming & Harvester / Beatport BPM-Key / Reports. = audio enrichment + track previews + Apple Music
|
||||||
|
harvester. **Streaming/preview audio on the storefront = potentially 🟢 cream** (sell records by letting people hear them);
|
||||||
|
**the MusicBrainz/Beatport enrichment = DealGod/discogs territory (🟡).** → **Question: do you want audio previews on the
|
||||||
|
RecordGod storefront?** Verdict: 🟡 — split preview (cream) from enrichment (DealGod).
|
||||||
|
|
||||||
|
### 20. Audio Reports — `reports/audio.php` · 🔴 BLOAT (lean)
|
||||||
|
Reporting on the audio-harvest process. Internal tooling for the enrichment pipeline — not a shop feature. Verdict: 🔴/🟡.
|
||||||
|
|
||||||
|
### 21. Messaging — `messaging-settings.php` + `channels/` · 🟡 UNSURE
|
||||||
|
Customer messaging config + channels (Beeper bridge = WhatsApp/etc). Powerful but heavy. → **Question: channel messaging =
|
||||||
|
keep or external?** Verdict: 🟡 (lean 🔴 for v1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## INTEGRATIONS & SYSTEM
|
||||||
|
|
||||||
|
### 22. Shipping — `shipping/{main,rates,labels}.php` · 🟢 CREAM (if you ship)
|
||||||
|
AusPost rates + label creation (`auspost_create_labels`), zones/flat-rates. Fulfillment for mail-order. → **Migrate if
|
||||||
|
RecordGod handles fulfillment; else Woo handles it.** Verdict: 🟡→🟢 — confirm RecordGod owns shipping vs Woo.
|
||||||
|
|
||||||
|
### 23. AI — `ai-tabs/{main,agent,chatbot,connect,prompts}.php` · 🔴 BLOAT (defer)
|
||||||
|
AI chatbot/agent + prompt config. Side quest, not the seller loop (PriceGod plan already cut AI). Verdict: 🔴 defer.
|
||||||
|
|
||||||
|
### 24. Beatport — `beatport-match.php` · 🟡 UNSURE
|
||||||
|
BPM/Key matcher (Search / Work Queue) via browser extension. DJ-store enrichment — useful for YOUR dance catalog, niche
|
||||||
|
generally. Overlaps DealGod enrichment. → **Question: BPM/key valuable enough to carry?** Verdict: 🟡.
|
||||||
|
|
||||||
|
### 25. Blagginate — `blagginate.php` · 🔴 BLOAT
|
||||||
|
YouTube-cookie tracklist/metadata processor. PriceGod plan explicitly cut it. Verdict: 🔴 drop.
|
||||||
|
|
||||||
|
### 26. Ads — `ads-admin-display.php` + `ads/` · 🔴 BLOAT (lean)
|
||||||
|
Advertising management. Niche, not core to stock/sell. → **Question: is this used?** Verdict: 🔴/🟡.
|
||||||
|
|
||||||
|
### 27. Connect — `connect/{main,auspost,dealgod,discogs,ebay,facebook,google,listenbrainz,mail,recaptcha,serp,square}.php` · 🟢 CREAM
|
||||||
|
THE integration hub — store all third-party credentials + test connections. RecordGod already has `/settings/secrets`
|
||||||
|
(Fernet vault) + connection tests for **discogs/woo/dealgod**. → **Migrate the rest: square, mail(SMTP), auspost,
|
||||||
|
facebook, google** as needed. eBay/listenbrainz/serp/recaptcha = 🟡 (only if used). Verdict: 🟢 — extend the vault/settings.
|
||||||
|
|
||||||
|
### 28. System — `system.php` + `tools/{debug,logging,shortcodes,wpcli}.php` + `settings/{endpoints,general,performance}.php` · 🟡 UNSURE
|
||||||
|
Admin plumbing: debug, logs, shortcodes, WP-CLI, endpoint registry, perf. Most is WP-specific (🔴 — RecordGod has its own
|
||||||
|
infra). **General settings (currency/tax/store info) = 🟢** (some already in POS settings). Verdict: 🟡 — cherry-pick
|
||||||
|
general settings; drop WP plumbing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SUMMARY
|
||||||
|
|
||||||
|
**🟢 CREAM — migrate (priority order):**
|
||||||
|
1. **Inventory list/edit/bulk** (the missing core stock screen)
|
||||||
|
2. **Import** (bulk stock-in, now matchable against the local disc_release mirror)
|
||||||
|
3. **Print / labels** (price labels + templates)
|
||||||
|
4. **Customers** CRM record page
|
||||||
|
5. **Connect** — extend the vault with square/mail/auspost/etc.
|
||||||
|
6. **Pages** content → fold into `/builder`
|
||||||
|
7. **Dashboard** Top-Performers widget
|
||||||
|
8. Finish **Search** (Scanner/Returns/Reorganize UIs), **Sales** (email/Square)
|
||||||
|
|
||||||
|
**🔴 BLOAT — drop:** Blagginate, AI chatbot, Ads (likely), Audio Reports, WP system/tools plumbing.
|
||||||
|
|
||||||
|
**✅ DONE:** Search (mostly), Sales, Virtual Store, Gig Guide (DealGod).
|
||||||
|
|
||||||
|
**RESOLVED — regroup with John (2026-06-22):**
|
||||||
|
- **Reports / Analyse** → 🟢 FULL reports + a standalone **Analyse** tool (dupes/missing-data/anomalies).
|
||||||
|
- **Wantlist** → 🟢 migrate **with intake-match + notify-on-arrival** (match incoming stock to customer wants).
|
||||||
|
- **Loyalty** → 🟢 migrate (data + UI) **but keep INACTIVE** — "needs a lot of work", don't activate the program yet.
|
||||||
|
- **Wiki** → 🔴 DROP — superseded by local enwiki/Discogs/MusicBrainz lookups (built before the local data centre existed).
|
||||||
|
- **Audio** → 🟢 migrate, but **INTERNALIZE**: replace API-key enrichment with local **MusicBrainz** (full MB on Ultra)
|
||||||
|
+ enwiki + Discogs lookups; KEEP the **Apple Music ID** feature (saves id as JSON to **preserve country**) + storefront
|
||||||
|
**previews** + **Beatport**. ⭐ **Needs its own careful page-by-page review** — what each piece does, what's API vs
|
||||||
|
internalisable. (ajax surface: batch_musicbrainz_*, discogs_to_musicbrainz_lookup, force_process_apple_id, apple_repair_*,
|
||||||
|
bp_search_beatport, batch_harvest_*, audio_player.)
|
||||||
|
- **Beatport** BPM/Key → 🟢 migrate (valuable for the dance catalog).
|
||||||
|
- **Messaging / Inbox** → 🔴 DROP in-app (Beeper/channels) — keep customer messaging external (email/socials).
|
||||||
|
- **Profile Editor** → 🟡 LATER — storefront not far enough along.
|
||||||
|
- **Shipping** → 🟢 migrate **own AusPost rates (table-based, NO API, no paid WP plugin)** — rates imported from the AusPost
|
||||||
|
guide PDF. ⚠️ **Rates +5% on 2026-07-01** (~9 days). Need a low-effort **rate-update path** (download new PDF → Gemini
|
||||||
|
extracts the tables → load), run ~yearly.
|
||||||
|
- **Ads** → 🟢 migrate — **post-ads-to-Meta API** + **Discogs marketplace management**. Not used yet but wanted in record-store software.
|
||||||
|
- **"Other"** menu (`wowplatter-other` / `display_other_page`) → confirm contents during execution (catch-all).
|
||||||
|
- **Virtual scene editor** → 🟢 BUILD the full **3D rack/crate placement editor inside RecordGod**.
|
||||||
|
|
||||||
|
**FINAL EXECUTION ORDER (cream):** 1) Inventory list/edit/bulk · 2) Import · 3) Print/labels · 4) Customers CRM ·
|
||||||
|
5) Connect (extend vault) + Shipping (AusPost rates, before Jul 1) · 6) Wantlist · 7) Reports+Analyse · 8) finish Search
|
||||||
|
(Scanner/Returns/Reorganize) + Sales (email/Square) · 9) Audio internalization review+build · 10) Beatport · 11) Ads
|
||||||
|
(Meta + Discogs marketplace) · 12) Loyalty (inactive) · 13) 3D scene editor · 14) Pages→builder · 15) Dashboard widget.
|
||||||
|
DROP: Wiki, Messaging/Inbox, Blagginate, AI chatbot, Audio-Reports, WP system plumbing. LATER: Profile Editor.
|
||||||
@ -1,5 +1,8 @@
|
|||||||
|
import secrets
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from . import vault
|
from . import vault
|
||||||
@ -63,6 +66,109 @@ async def inventory(q: str = Query(""), kind: str = Query(""), crate_id: int | N
|
|||||||
return {"ok": True, "items": items, "page": page, "per": per, "total": total}
|
return {"ok": True, "items": items, "page": page, "per": per, "total": total}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Inventory CRUD (the WowPlatter Inventory screen, RecordGod-side) ───────────────────────────
|
||||||
|
class InvItemIn(BaseModel):
|
||||||
|
sku: str | None = None
|
||||||
|
kind: str = "vinyl"
|
||||||
|
release_id: int | None = None
|
||||||
|
identifier: str | None = None
|
||||||
|
title: str | None = None
|
||||||
|
price: float | None = None
|
||||||
|
qty: int = 1
|
||||||
|
condition: str | None = None
|
||||||
|
sleeve_cond: str | None = None
|
||||||
|
brand: str | None = None
|
||||||
|
crate_id: int | None = None
|
||||||
|
slot_number: int | None = None
|
||||||
|
notes: str | None = None
|
||||||
|
in_stock: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class InvEditIn(BaseModel):
|
||||||
|
price: float | None = None
|
||||||
|
condition: str | None = None
|
||||||
|
sleeve_cond: str | None = None
|
||||||
|
title: str | None = None
|
||||||
|
crate_id: int | None = None
|
||||||
|
slot_number: int | None = None
|
||||||
|
notes: str | None = None
|
||||||
|
in_stock: bool | None = None
|
||||||
|
target_price: float | None = None
|
||||||
|
kind: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class BulkIn(BaseModel):
|
||||||
|
skus: list[str]
|
||||||
|
action: str # set_price | adjust_pct | set_crate | mark_sold | restock | delete
|
||||||
|
value: float | int | str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/inventory/add")
|
||||||
|
async def inv_add(body: InvItemIn, ident=Depends(require_token), db=Depends(get_db)):
|
||||||
|
sku = (body.sku or "").strip() or ("RG" + secrets.token_hex(4).upper())
|
||||||
|
title = body.title
|
||||||
|
if body.release_id and not title: # pull the cached title if intaking by release
|
||||||
|
r = (await db.execute(text("SELECT title FROM disc_cache WHERE release_id=:r"),
|
||||||
|
{"r": body.release_id})).first()
|
||||||
|
title = r[0] if r else None
|
||||||
|
await db.execute(text("""
|
||||||
|
INSERT INTO inventory (sku, store_id, kind, release_id, identifier, title, price, qty, in_stock,
|
||||||
|
condition, sleeve_cond, brand, crate_id, slot_number, notes, status,
|
||||||
|
created_at, updated_at)
|
||||||
|
VALUES (:sku, :s, :kind, :rid, :ident, :title, :price, :qty, :in_stock, :cond, :sleeve, :brand,
|
||||||
|
:crate, :slot, :notes, :status, now(), now())
|
||||||
|
ON CONFLICT (sku) DO NOTHING
|
||||||
|
"""), {"sku": sku, "s": ident["store_id"], "kind": body.kind, "rid": body.release_id,
|
||||||
|
"ident": body.identifier, "title": title, "price": body.price, "qty": body.qty,
|
||||||
|
"in_stock": body.in_stock, "cond": body.condition, "sleeve": body.sleeve_cond,
|
||||||
|
"brand": body.brand, "crate": body.crate_id, "slot": body.slot_number, "notes": body.notes,
|
||||||
|
"status": "publish" if body.in_stock else "sold"})
|
||||||
|
await db.commit()
|
||||||
|
return {"ok": True, "sku": sku}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/inventory/{sku}/edit")
|
||||||
|
async def inv_edit(sku: str, body: InvEditIn, ident=Depends(require_token), db=Depends(get_db)):
|
||||||
|
fields = {k: v for k, v in body.model_dump().items() if v is not None}
|
||||||
|
if not fields:
|
||||||
|
return {"ok": True, "unchanged": True}
|
||||||
|
sets = ", ".join(f"{k} = :{k}" for k in fields) + ", updated_at = now()"
|
||||||
|
r = await db.execute(text(f"UPDATE inventory SET {sets} WHERE sku = :sku AND store_id = :s"),
|
||||||
|
{**fields, "sku": sku, "s": ident["store_id"]})
|
||||||
|
await db.commit()
|
||||||
|
if not r.rowcount:
|
||||||
|
raise HTTPException(404, "sku not found")
|
||||||
|
return {"ok": True, "updated": r.rowcount}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/inventory/{sku}/delete")
|
||||||
|
async def inv_delete(sku: str, ident=Depends(require_token), db=Depends(get_db)):
|
||||||
|
r = await db.execute(text("DELETE FROM inventory WHERE sku = :sku AND store_id = :s"),
|
||||||
|
{"sku": sku, "s": ident["store_id"]})
|
||||||
|
await db.commit()
|
||||||
|
return {"ok": True, "deleted": r.rowcount}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/inventory/bulk")
|
||||||
|
async def inv_bulk(body: BulkIn, ident=Depends(require_token), db=Depends(get_db)):
|
||||||
|
if not body.skus:
|
||||||
|
raise HTTPException(400, "no skus")
|
||||||
|
p = {"skus": body.skus, "s": ident["store_id"], "v": body.value}
|
||||||
|
sql = {
|
||||||
|
"set_price": "UPDATE inventory SET price=:v, updated_at=now() WHERE sku=ANY(:skus) AND store_id=:s",
|
||||||
|
"adjust_pct": "UPDATE inventory SET price=round(price*(1+:v/100.0),2), updated_at=now() WHERE sku=ANY(:skus) AND store_id=:s",
|
||||||
|
"set_crate": "UPDATE inventory SET crate_id=:v, updated_at=now() WHERE sku=ANY(:skus) AND store_id=:s",
|
||||||
|
"mark_sold": "UPDATE inventory SET in_stock=false, status='sold', sold_date=now(), updated_at=now() WHERE sku=ANY(:skus) AND store_id=:s",
|
||||||
|
"restock": "UPDATE inventory SET in_stock=true, status='publish', sold_date=NULL, updated_at=now() WHERE sku=ANY(:skus) AND store_id=:s",
|
||||||
|
"delete": "DELETE FROM inventory WHERE sku=ANY(:skus) AND store_id=:s",
|
||||||
|
}.get(body.action)
|
||||||
|
if not sql:
|
||||||
|
raise HTTPException(400, f"unknown action {body.action}")
|
||||||
|
r = await db.execute(text(sql), p)
|
||||||
|
await db.commit()
|
||||||
|
return {"ok": True, "affected": r.rowcount}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/crates")
|
@router.get("/crates")
|
||||||
async def crates(ident=Depends(require_token), db=Depends(get_db)):
|
async def crates(ident=Depends(require_token), db=Depends(get_db)):
|
||||||
rows = await db.execute(text("""
|
rows = await db.execute(text("""
|
||||||
|
|||||||
62
disc_images_backfill.py
Normal file
62
disc_images_backfill.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy WowPlatter's already-downloaded release-cover WebPs into RecordGod's hashed image tree.
|
||||||
|
|
||||||
|
WowPlatter downloaded 60k+ covers to wp-content/uploads/wowplatter/release/<NN>/release-<id>.webp
|
||||||
|
(all WebP, ~150px, tiny). RecordGod serves /img/r/<release_id> from DISC_IMAGE_DIR/<h[:2]>/<h>.webp
|
||||||
|
where h = sha256(release_id) — so we stage them under that scheme, then rsync to the VPS image dir.
|
||||||
|
|
||||||
|
python disc_images_backfill.py # build /tmp/rg_disc_images
|
||||||
|
rsync -a /tmp/rg_disc_images/ root@<vps>:/opt/recordgod/disc_images/
|
||||||
|
|
||||||
|
Server-side i.discogs fetch is Cloudflare-403'd, so these local copies are how RecordGod self-hosts.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
WP = "/opt/homebrew/var/www/monsterrobot.localsite"
|
||||||
|
UPLOADS = WP + "/wp-content/uploads/"
|
||||||
|
STAGE = pathlib.Path("/tmp/rg_disc_images")
|
||||||
|
|
||||||
|
|
||||||
|
def creds():
|
||||||
|
t = pathlib.Path(WP + "/wp-config.php").read_text()
|
||||||
|
g = lambda k: re.search(rf"'{k}',\s*'([^']*)'", t).group(1)
|
||||||
|
return g("DB_NAME"), g("DB_USER"), g("DB_PASSWORD")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
name, user, pw = creds()
|
||||||
|
c = pymysql.connect(unix_socket="/opt/homebrew/var/mysql/mysql.sock", user=user, password=pw,
|
||||||
|
database=name).cursor()
|
||||||
|
# one primary cover per release (fall back to any image if no primary)
|
||||||
|
c.execute("""SELECT release_id, local_url FROM wp_rmp_disc_release_image
|
||||||
|
WHERE local_url IS NOT NULL ORDER BY release_id, (type='primary') DESC""")
|
||||||
|
seen, copied, missing = set(), 0, 0
|
||||||
|
for release_id, local_url in c.fetchall():
|
||||||
|
if release_id in seen:
|
||||||
|
continue
|
||||||
|
seen.add(release_id)
|
||||||
|
m = re.search(r"/wp-content/uploads/(.+)$", local_url)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
src = pathlib.Path(UPLOADS + m.group(1))
|
||||||
|
if not src.exists():
|
||||||
|
missing += 1
|
||||||
|
continue
|
||||||
|
h = hashlib.sha256(str(release_id).encode()).hexdigest()
|
||||||
|
dst = STAGE / h[:2] / f"{h}.webp"
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
copied += 1
|
||||||
|
if copied % 5000 == 0:
|
||||||
|
print(f" {copied}…", file=sys.stderr)
|
||||||
|
print(f"staged {copied} covers to {STAGE} ({missing} files missing on disk)", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user