Pak Jabit: full stack — storefront, Spree customisations, catalogue tooling

A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.

What is here:
  storefront/  the Next.js app
  backend/     ONLY the custom Spree code. Spree itself is a prebuilt image
               that mounts no app code, so every file here needs an explicit
               volume entry in docker-compose.yml or it is silently ignored.
  catalogue/   the scripts that loaded and maintain the products
  docs/        manifest, mock registry, napkins

Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.

The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-15 22:09:55 +08:00
co-authored by Claude Opus 5
commit a58a61b98c
98 changed files with 15524 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# Spree ADMIN API key — full write access to products, prices and orders.
# The scripts here read it from the environment and refuse to run without it;
# it used to be a literal in six files, which is why this exists.
#
# export SPREE_ADMIN_KEY=$(grep SPREE_ADMIN_KEY catalogue/.env | cut -d= -f2)
SPREE_ADMIN_KEY=sk_ntDFxFE1KwiDwqGzpCBGTJ8N
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Pull a full Pak Jabit backend backup to the house.
# - pg_dump (custom format) of spree_production
# - ActiveStorage volume, rsync'd mirror (~208MB, only deltas after the first run)
# - .env (SECRET_KEY_BASE lives here — without it every signed image URL and
# the admin API token die on restore)
#
# Usage: backup-pak-jabit.sh # normal run
# backup-pak-jabit.sh --verify # also list the dump's table counts
set -euo pipefail
HOST=ijat@35.247.170.3
KEY=/root/.ssh/google_compute_engine
SSH=(ssh -i "$KEY" -o ConnectTimeout=15 -o BatchMode=yes)
DEST=/root/Backups/pak-jabit
KEEP=14 # dated dumps to retain
STAMP=$(date +%Y%m%d-%H%M)
STORAGE_REMOTE=/var/lib/docker/volumes/pak-jabit_storage_data/_data/
mkdir -p "$DEST"/{db,storage}
chmod 700 "$DEST"
log() { printf '%s %s\n' "$(date +%H:%M:%S)" "$*"; }
# --- 1. database -------------------------------------------------------------
log "pg_dump → db/spree-$STAMP.dump"
"${SSH[@]}" "$HOST" \
'docker exec pak-jabit-postgres-1 pg_dump -U postgres -Fc spree_production' \
> "$DEST/db/spree-$STAMP.dump.part"
mv "$DEST/db/spree-$STAMP.dump.part" "$DEST/db/spree-$STAMP.dump"
# A truncated dump is the classic silent-success backup. Refuse to keep one.
if ! pg_restore -l "$DEST/db/spree-$STAMP.dump" >/dev/null 2>&1; then
rm -f "$DEST/db/spree-$STAMP.dump"
echo "FATAL: dump failed pg_restore -l — not a usable archive, deleted." >&2
exit 1
fi
log " ok $(du -h "$DEST/db/spree-$STAMP.dump" | cut -f1)"
# --- 2. ActiveStorage --------------------------------------------------------
log "rsync storage/"
rsync -a --delete --info=stats2 \
-e "ssh -i $KEY -o BatchMode=yes" \
--rsync-path='sudo rsync' \
"$HOST:$STORAGE_REMOTE" "$DEST/storage/" | tail -n 6
# --- 3. secrets & compose ----------------------------------------------------
log "env + compose"
"${SSH[@]}" "$HOST" 'cat ~/pak-jabit/.env' > "$DEST/env-$STAMP"
"${SSH[@]}" "$HOST" 'cat ~/pak-jabit/docker-compose.yml' > "$DEST/docker-compose.yml"
chmod 600 "$DEST"/env-*
ln -sfn "env-$STAMP" "$DEST/env.latest"
# --- 3b. custom app code -----------------------------------------------------
# The Spree image is prebuilt and mounts no app code, so every customisation
# (Image Studio, the API body-limit initializer, the patched routes.rb) exists
# ONLY as bind-mounted files on this VM. The database dump does not contain them
# and a rebuilt box would come back without them.
log "custom code"
rsync -a --delete \
-e "ssh -i $KEY -o BatchMode=yes" \
--include='config/' --include='config/routes.rb' \
--include='config/initializers/***' \
--include='app/' --include='app/controllers/***' --include='app/views/***' \
--exclude='*' \
"$HOST:pak-jabit/" "$DEST/custom/"
find "$DEST/custom" -type f | sed "s|$DEST/custom/| |"
# --- 4. rotate ---------------------------------------------------------------
ls -1t "$DEST"/db/spree-*.dump 2>/dev/null | tail -n +$((KEEP + 1)) | xargs -r rm -v
ls -1t "$DEST"/env-* 2>/dev/null | tail -n +$((KEEP + 1)) | xargs -r rm -v
if [[ ${1:-} == --verify ]]; then
log "dump contents:"
pg_restore -l "$DEST/db/spree-$STAMP.dump" | grep -c 'TABLE DATA' \
| xargs printf ' %s tables with data\n'
fi
log "done → $DEST ($(du -sh "$DEST" | cut -f1) total)"
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
Pak Jabit Store — Bulk Product Import via Spree Admin API v3
Creates products, uploads images, publishes to Online Store channel,
and sets stock — all through the official Admin API.
Usage:
python3 bulk_import.py [--dry-run] [--start N] [--limit N]
"""
import json
import os
import sys
import time
import requests
import argparse
from pathlib import Path
import os as _os
def _spree_admin_key() -> str:
"""The Spree admin API key. Required — never defaulted."""
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
if not key:
raise SystemExit(
"SPREE_ADMIN_KEY is not set.\n"
" export SPREE_ADMIN_KEY=<the admin API key>\n"
"It is deliberately not stored in this file — see README.md."
)
return key
# --- Config ---
API_BASE = "https://api.jux-lab.com/api/v3/admin"
API_KEY = _spree_admin_key()
HEADERS = {"X-Spree-Api-Key": API_KEY, "Content-Type": "application/json"}
CHANNEL_ID = "ch_UkLWZg9DAJ" # Online Store
MYR_TO_USD = 4.8
PUBLISHED_AT = "2026-09-15T00:00:00Z"
CATALOGUE_DIR = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue")
PRODUCTS_DIR = CATALOGUE_DIR / "products"
BATCH_FILES = ["products-batch2.jsonl", "products-batch3.jsonl"]
LOG_FILE = CATALOGUE_DIR / "import_log.jsonl"
def api(method, path, **kwargs):
url = f"{API_BASE}{path}"
headers = dict(HEADERS)
if "files" in kwargs:
headers.pop("Content-Type", None)
r = getattr(requests, method)(url, headers=headers, timeout=60, **kwargs)
try:
return r.json()
except Exception:
return {"error": {"code": r.status_code, "message": r.text[:200]}}
def find_product_folder(code, name):
"""Match JSONL entry to local image folder."""
for folder in sorted(PRODUCTS_DIR.iterdir()):
info_file = folder / "info.json"
if info_file.exists():
info = json.loads(info_file.read_text())
if info.get("source_code") == code:
return folder
return None
def create_product(name, brand, price_myr, slug=None):
price = round(float(price_myr) * MYR_TO_USD, 2)
desc = f"{brand} - {name}"
# A scalar "price" is silently dropped; Spree wants a prices array, and a
# product with no price row is excluded from the storefront's
# available(currency) scope no matter how it is published.
payload = {
"name": name,
"description": desc,
"prices": [{"amount": price, "currency": "USD"}],
"status": "active",
"available_on": "2026-09-15T00:00:00Z",
}
if slug:
payload["slug"] = slug
return api("post", "/products", json=payload)
def upload_image(product_id, variant_id, image_path):
with open(image_path, "rb") as f:
return api("post",
f"/products/{product_id}/variants/{variant_id}/media",
files={"attachment": (image_path.name, f, "image/jpeg")})
def publish_to_channel(product_id):
# Without published_at the publication row exists but is not live.
return api("post",
f"/channels/{CHANNEL_ID}/add_products",
json={"product_ids": [product_id], "published_at": PUBLISHED_AT})
def make_purchasable(product_id, variant_id):
return api("patch", f"/products/{product_id}/variants/{variant_id}",
json={"track_inventory": False})
def log_result(entry):
with open(LOG_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")
def get_existing_slugs():
slugs = set()
page = 1
while True:
data = api("get", f"/products?per_page=100&page={page}")
products = data.get("data", [])
for p in products:
slugs.add(p.get("slug", ""))
meta = data.get("meta", {})
if page >= meta.get("pages", 1):
break
page += 1
return slugs
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--start", type=int, default=0)
parser.add_argument("--limit", type=int, default=999)
args = parser.parse_args()
# Load all products from batch files
products = []
for batch_file in BATCH_FILES:
path = CATALOGUE_DIR / batch_file
if path.exists():
with open(path) as f:
for line in f:
products.append(json.loads(line.strip()))
print(f"Loaded {len(products)} products from {len(BATCH_FILES)} batch files")
# Get existing to skip duplicates
print("Fetching existing products...")
existing = get_existing_slugs()
print(f" {len(existing)} products already in store")
# Process
imported = 0
skipped = 0
failed = 0
products_slice = products[args.start:args.start + args.limit]
for i, prod in enumerate(products_slice):
idx = args.start + i
name = prod["name"]
brand = prod["brand"]
code = prod["code"]
price = prod["price"]
folder = find_product_folder(code, name)
if not folder:
print(f" [{idx+1}] SKIP (no folder): {brand} - {name}")
skipped += 1
continue
# Check slug collision
info = json.loads((folder / "info.json").read_text())
slug = folder.name
if slug in existing:
print(f" [{idx+1}] SKIP (exists): {slug}")
skipped += 1
continue
if args.dry_run:
images = list(folder.glob("image-*.jpg"))
print(f" [{idx+1}] DRY: {brand} - {name} ({len(images)} imgs, ${float(price)*MYR_TO_USD:.2f})")
continue
# 1. Create product
result = create_product(name, brand, price, slug)
if "error" in result:
print(f" [{idx+1}] FAIL create: {result['error']}")
log_result({"code": code, "action": "create", "error": result["error"]})
failed += 1
continue
product_id = result["id"]
variant_id = result.get("default_variant_id", "")
print(f" [{idx+1}] Created: {product_id} — {brand} - {name}")
# 2. Upload images
images = sorted(folder.glob("image-*.jpg"))
img_ok = 0
for img in images:
r = upload_image(product_id, variant_id, img)
if "error" not in r:
img_ok += 1
else:
print(f" WARN image {img.name}: {r['error']}")
time.sleep(0.3)
print(f" Images: {img_ok}/{len(images)}")
# 3. Publish to Online Store
pub = publish_to_channel(product_id)
if "error" in pub:
print(f" WARN publish: {pub['error']}")
# 4. Make purchasable
stk = make_purchasable(product_id, variant_id)
if "error" in stk:
print(f" WARN stock: {stk['error']}")
log_result({
"code": code, "product_id": product_id, "slug": slug,
"brand": brand, "name": name, "images": img_ok,
"price_usd": round(float(price) * MYR_TO_USD, 2)
})
imported += 1
existing.add(slug)
time.sleep(0.5)
print(f"\nDone! Imported: {imported}, Skipped: {skipped}, Failed: {failed}")
if not args.dry_run and imported > 0:
print(f"Log: {LOG_FILE}")
if __name__ == "__main__":
main()
+33
View File
@@ -0,0 +1,33 @@
import json, os, re, urllib.request, time
base = os.path.dirname(os.path.abspath(__file__))
out_root = os.path.join(base, "products")
os.makedirs(out_root, exist_ok=True)
def slugify(name):
s = re.sub(r'[^a-zA-Z0-9]+', '-', name).strip('-').lower()
return s[:60]
catalogue = []
with open(os.path.join(base, "products.jsonl")) as f:
for line in f:
line = line.strip()
if not line:
continue
catalogue.append(json.loads(line))
for i, p in enumerate(catalogue, 1):
dirname = f"{i:02d}-{slugify(p['brand'])}-{slugify(p['name'])}"
pdir = os.path.join(out_root, dirname)
os.makedirs(pdir, exist_ok=True)
with open(os.path.join(pdir, "info.json"), "w") as f:
json.dump({"name": p["name"], "brand": p["brand"], "price_myr": p["price"], "source_code": p["code"]}, f, indent=2)
for j, url in enumerate(p["imgs"], 1):
dest = os.path.join(pdir, f"image-{j}.jpg")
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=15) as r, open(dest, "wb") as out:
out.write(r.read())
except Exception as e:
print(f"FAIL {url}: {e}")
print(f"[{i}/{len(catalogue)}] {p['brand']} - {p['name']} -> {len(p['imgs'])} imgs")
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Download images for batch2 + batch3 products."""
import json, os, re, urllib.request, time, sys
base = os.path.dirname(os.path.abspath(__file__))
out_root = os.path.join(base, "products")
os.makedirs(out_root, exist_ok=True)
def slugify(name):
s = re.sub(r'[^a-zA-Z0-9]+', '-', name).strip('-').lower()
return s[:60]
def load_jsonl(path):
items = []
with open(path) as f:
for line in f:
line = line.strip()
if line:
items.append(json.loads(line))
return items
existing = load_jsonl(os.path.join(base, "products.jsonl"))
offset = len(existing)
batches = []
for fname in ["products-batch2.jsonl", "products-batch3.jsonl"]:
path = os.path.join(base, fname)
if os.path.exists(path):
batches.extend(load_jsonl(path))
print(f"Existing: {offset} products, new: {len(batches)} to download")
ok, fail = 0, 0
for i, p in enumerate(batches, 1):
idx = offset + i
dirname = f"{idx:03d}-{slugify(p['brand'])}-{slugify(p['name'])}"
pdir = os.path.join(out_root, dirname)
os.makedirs(pdir, exist_ok=True)
info = {"name": p["name"], "brand": p["brand"], "price_myr": p["price"], "source_code": p["code"]}
if "category" in p:
info["category"] = p["category"]
with open(os.path.join(pdir, "info.json"), "w") as f:
json.dump(info, f, indent=2)
img_ok = 0
for j, url in enumerate(p["imgs"], 1):
dest = os.path.join(pdir, f"image-{j}.jpg")
if os.path.exists(dest) and os.path.getsize(dest) > 1000:
img_ok += 1
continue
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"})
with urllib.request.urlopen(req, timeout=15) as r, open(dest, "wb") as out:
out.write(r.read())
img_ok += 1
except Exception as e:
print(f" FAIL {url}: {e}")
fail += 1
time.sleep(0.2)
ok += img_ok
print(f"[{i}/{len(batches)}] {p['brand']} - {p['name'][:40]} -> {img_ok}/{len(p['imgs'])} imgs")
print(f"\nDone: {ok} images downloaded, {fail} failed")
+57
View File
@@ -0,0 +1,57 @@
import json, time, requests
from pathlib import Path
import os as _os
def _spree_admin_key() -> str:
"""The Spree admin API key. Required — never defaulted."""
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
if not key:
raise SystemExit(
"SPREE_ADMIN_KEY is not set.\n"
" export SPREE_ADMIN_KEY=<the admin API key>\n"
"It is deliberately not stored in this file — see README.md."
)
return key
API = "https://api.jux-lab.com/api/v3/admin"
H = {"X-Spree-Api-Key": _spree_admin_key(), "Content-Type": "application/json"}
PRODUCTS = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products")
MYR_TO_USD = 4.8
prices = {}
for folder in PRODUCTS.iterdir():
info = folder / "info.json"
if info.exists():
prices[folder.name] = round(float(json.loads(info.read_text())["price_myr"]) * MYR_TO_USD, 2)
items, page = [], 1
while True:
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
items += d.get("data", [])
if page >= d.get("meta", {}).get("pages", 1):
break
page += 1
print(f"{len(items)} products, {len(prices)} source prices")
ok = fail = 0
for i, p in enumerate(items):
amt = prices.get(p["slug"])
if amt is None:
print(f" no source price: {p['slug'][:45]}")
fail += 1
continue
r = requests.patch(f"{API}/products/{p['id']}", headers=H,
json={"prices": [{"amount": amt, "currency": "USD"}]}, timeout=60).json()
if "error" in r:
fail += 1
print(f" FAIL {p['slug'][:40]}: {r['error'].get('message')}")
else:
ok += 1
if (i + 1) % 40 == 0:
print(f" {i+1}/{len(items)}")
time.sleep(0.12)
print(f"Done ok={ok} fail={fail}")
+48
View File
@@ -0,0 +1,48 @@
import json, sys, time, requests
import os as _os
def _spree_admin_key() -> str:
"""The Spree admin API key. Required — never defaulted."""
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
if not key:
raise SystemExit(
"SPREE_ADMIN_KEY is not set.\n"
" export SPREE_ADMIN_KEY=<the admin API key>\n"
"It is deliberately not stored in this file — see README.md."
)
return key
API = "https://api.jux-lab.com/api/v3/admin"
KEY = _spree_admin_key()
H = {"X-Spree-Api-Key": KEY, "Content-Type": "application/json"}
products, page = [], 1
while True:
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
products += d.get("data", [])
if page >= d.get("meta", {}).get("pages", 1):
break
page += 1
print(f"{len(products)} products")
ok = fail = 0
for i, p in enumerate(products):
vid = p.get("default_variant_id")
if not vid:
fail += 1
continue
r = requests.patch(f"{API}/products/{p['id']}/variants/{vid}", headers=H,
json={"count_on_hand": 100, "track_inventory": False}, timeout=60).json()
if "error" in r:
fail += 1
print(f" FAIL {p['slug'][:40]}: {r['error'].get('message')}")
else:
ok += 1
if (i + 1) % 25 == 0:
print(f" {i+1}/{len(products)}")
time.sleep(0.15)
print(f"Done. ok={ok} fail={fail}")
+164
View File
@@ -0,0 +1,164 @@
{"code": "hm-3377-7607064", "product_id": "prod_rsClYtECFI", "slug": "031-h-m-oversized-fit-scuba-zip-through-hoodie", "brand": "H&M", "name": "Oversized Fit Scuba zip-through hoodie", "images": 5, "price_usd": 719.52}
{"code": "urban-revivo-3653-8941654", "product_id": "prod_8DR0I7fEbw", "slug": "032-urban-revivo-casual-crew-neck-short-sleeve-tee", "brand": "Urban Revivo", "name": "Casual Crew Neck Short Sleeve Tee", "images": 6, "price_usd": 571.2}
{"code": "urban-revivo-4852-8181854", "product_id": "prod_aAtNvLSUAr", "slug": "033-urban-revivo-short-sleeve-shirts", "brand": "Urban Revivo", "name": "Short Sleeve Shirts", "images": 7, "price_usd": 557.28}
{"code": "nike-4039-6920264", "product_id": "prod_QiA48cIW7A", "slug": "034-nike-training-t-shirt", "brand": "Nike", "name": "Training T-Shirt", "images": 1, "price_usd": 706.8}
{"code": "hm-8811-9083444", "product_id": "prod_QDA48cIW7A", "slug": "035-h-m-regular-fit-track-pants-with-drymove", "brand": "H&M", "name": "Regular Fit Track pants with DryMove\u2122", "images": 8, "price_usd": 719.52}
{"code": "trendyol-6397-0057634", "product_id": "prod_8cR0I7fEbw", "slug": "036-trendyol-beige-oversize-wide-cut-long-sleeve-animal-embroidered-sweat", "brand": "Trendyol", "name": "Beige Oversize/Wide Cut Long Sleeve Animal Embroidered Sweatshirt TMNAW24SW00164", "images": 5, "price_usd": 269.04}
{"code": "trendyol-9722-5541154", "product_id": "prod_oqlrKdXjaP", "slug": "037-trendyol-horse-embroidered-sweatshirt", "brand": "Trendyol", "name": "Horse Embroidered Sweatshirt", "images": 1, "price_usd": 264.96}
{"code": "hm-1602-9539354", "product_id": "prod_48Olu6p4zZ", "slug": "038-h-m-relaxed-fit-resort-shirt", "brand": "H&M", "name": "Relaxed Fit Resort shirt", "images": 6, "price_usd": 479.52}
{"code": "trendyol-6524-2362464", "product_id": "prod_qp31JYFduR", "slug": "039-trendyol-anthracite-001-relaxed-mock-neck-plain-sweater-tmnaw26kz0001", "brand": "Trendyol", "name": "Anthracite*001 Relaxed Mock Neck Plain Sweater Tmnaw26Kz00013", "images": 5, "price_usd": 405.6}
{"code": "giordano-7623-8198654", "product_id": "prod_JgaEBgznCp", "slug": "040-giordano-men-s-cotton-short-sleeve-slim-tee-01026250", "brand": "Giordano", "name": "Men's Cotton Short Sleeve Slim Tee 01026250", "images": 2, "price_usd": 178.42}
{"code": "hm-4373-6317064", "product_id": "prod_tXyJwvFCuK", "slug": "041-h-m-ultra-baggy-jeans", "brand": "H&M", "name": "Ultra Baggy Jeans", "images": 5, "price_usd": 719.52}
{"code": "hm-0024-5243544", "product_id": "prod_NJzXvxTk8u", "slug": "042-h-m-relaxed-jeans", "brand": "H&M", "name": "Relaxed Jeans", "images": 4, "price_usd": 623.52}
{"code": "ysocool-brand-7905-5999931", "product_id": "prod_XLbb5HTmVa", "slug": "043-ysocool-men-compression-body-shaper-vests-top", "brand": "YSoCool", "name": "Men Compression Body Shaper Vests Top", "images": 3, "price_usd": 223.44}
{"code": "nike-8330-6036864", "product_id": "prod_k2KT9MVfb0", "slug": "044-nike-dri-fit-training-t-shirt", "brand": "Nike", "name": "Dri-FIT Training T-Shirt", "images": 1, "price_usd": 542.64}
{"code": "hm-8820-5290854", "product_id": "prod_Z5MUPd7wVj", "slug": "045-h-m-loose-fit-t-shirt", "brand": "H&M", "name": "Loose Fit T-shirt", "images": 4, "price_usd": 239.52}
{"code": "hm-6806-1878823", "product_id": "prod_STIszOMIvc", "slug": "046-h-m-slim-fit-t-shirt", "brand": "H&M", "name": "Slim Fit T-shirt", "images": 5, "price_usd": 119.52}
{"code": "hm-6462-8250054", "product_id": "prod_LYpHOn63Vw", "slug": "047-h-m-relaxed-fit-textured-resort-shirt", "brand": "H&M", "name": "Relaxed Fit Textured resort shirt", "images": 4, "price_usd": 479.52}
{"code": "trendyol-3134-8272734", "product_id": "prod_Kj5Q8YFGzd", "slug": "048-trendyol-black-regular-fit-100-viscose-loose-and-loose-summer-shirt-t", "brand": "Trendyol", "name": "Black Regular Fit 100% Viscose Loose and Loose Summer Shirt TMNSS25GO00048", "images": 1, "price_usd": 433.44}
{"code": "altinyildiz-8185-0860924", "product_id": "prod_hDBvQ85Ws9", "slug": "049-altinyildiz-basic-slim-fit-t-shirt", "brand": "Altinyildiz", "name": "Basic Slim Fit T-Shirt", "images": 1, "price_usd": 250.37}
{"code": "nike-0596-4602954", "product_id": "prod_IOvBlcSU2T", "slug": "050-nike-palm-tree-men-s-tee", "brand": "Nike", "name": "Palm Tree Men's Tee", "images": 1, "price_usd": 446.4}
{"code": "lacoste-6939-3474274", "product_id": "prod_K95Q8YFGzd", "slug": "051-lacoste-monogram-jacquard-t-shirt", "brand": "Lacoste", "name": "Monogram Jacquard T-shirt", "images": 4, "price_usd": 2587.2}
{"code": "hm-2842-2711854", "product_id": "prod_iIDfaj7G4u", "slug": "052-h-m-relaxed-fit-cotton-chinos", "brand": "H&M", "name": "Relaxed Fit Cotton chinos", "images": 2, "price_usd": 623.52}
{"code": "urban-revivo-8524-3567374", "product_id": "prod_6AgxZbenF4", "slug": "053-urban-revivo-classic-solid-color-thick-shirt-in-viscose-blend", "brand": "Urban Revivo", "name": "Classic Solid Color Thick Shirt in Viscose Blend", "images": 8, "price_usd": 907.44}
{"code": "hm-4144-4577954", "product_id": "prod_Aks1igzRC6", "slug": "054-h-m-relaxed-fit-oxford-shirt", "brand": "H&M", "name": "Relaxed Fit Oxford shirt", "images": 5, "price_usd": 623.52}
{"code": "trendyol-7335-7898364", "product_id": "prod_lmiGAYKIUz", "slug": "055-trendyol-black-regular-normal-cut-hooded-textured-sweatshirt-tmnaw26s", "brand": "Trendyol", "name": "Black Regular/Normal Cut Hooded Textured Sweatshirt Tmnaw26Sw00068", "images": 3, "price_usd": 379.34}
{"code": "trendyol-7769-9773614", "product_id": "prod_yar3EWPH85", "slug": "056-trendyol-striped-knit-polo-shirt", "brand": "Trendyol", "name": "Striped Knit Polo Shirt", "images": 4, "price_usd": 410.5}
{"code": "hm-4087-3660754", "product_id": "prod_zNcwP3ZbDr", "slug": "057-h-m-regular-fit-hole-knit-sweater-vest", "brand": "H&M", "name": "Regular Fit Hole-knit sweater vest", "images": 6, "price_usd": 671.52}
{"code": "hm-9694-6720944", "product_id": "prod_ccuasQi8rC", "slug": "058-h-m-easy-care-relaxed-fit-linen-blend-shorts", "brand": "H&M", "name": "Easy-care Relaxed Fit Linen-blend shorts", "images": 4, "price_usd": 719.52}
{"code": "hm-9884-9573454", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "trendyol-1466-7949634", "product_id": "prod_bnnZn4Y5Fq", "slug": "060-trendyol-brown-oversize-wide-cut-buttoned-polo-collar-thick-polar-fle", "brand": "Trendyol", "name": "Brown Oversize/Wide Cut Buttoned Polo Collar Thick Polar Fleece Sweatshirt TMNAW24SW00107", "images": 1, "price_usd": 396.14}
{"code": "ben-sherman-1585-5545954", "product_id": "prod_UkKLWZg9DA", "slug": "061-ben-sherman-short-sleeves-jacquard-polo-shirt", "brand": "Ben Sherman", "name": "Short Sleeves Jacquard Polo Shirt", "images": 1, "price_usd": 1500.24}
{"code": "hm-9884-9573454", "product_id": "prod_UkpLWZg9DA", "slug": "059-h-m-oversized-fit-t-shirt", "brand": "H&M", "name": "Oversized Fit T-shirt", "images": 6, "price_usd": 335.52}
{"code": "trendyol-1466-7949634", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "ben-sherman-1585-5545954", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-3289-5938864", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-3289-5938864", "product_id": "prod_gXZHJdmfrX", "slug": "062-h-m-relaxed-fit-sweatshirt", "brand": "H&M", "name": "Relaxed Fit Sweatshirt", "images": 2, "price_usd": 287.52}
{"code": "hm-7368-5259764", "product_id": "prod_Ej9hxLZ9ck", "slug": "063-h-m-regular-fit-easy-iron-shirt", "brand": "H&M", "name": "Regular Fit Easy-iron shirt", "images": 2, "price_usd": 383.52}
{"code": "hm-7368-5259764", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-0914-3034864", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-0914-3034864", "product_id": "prod_VZ3XmZF31w", "slug": "064-h-m-polo-shirt", "brand": "H&M", "name": "Polo shirt", "images": 2, "price_usd": 479.52}
{"code": "adidas-0957-2958274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "tommy-hilfiger-6431-9934964", "product_id": "prod_OtvJLhNcSb", "slug": "066-tommy-hilfiger-linear-logo-slim-crew-neck-t-shirt-tommy-jeans", "brand": "Tommy Hilfiger", "name": "Linear Logo Slim Crew Neck T-Shirt - Tommy Jeans", "images": 1, "price_usd": 725.04}
{"code": "adidas-0957-2958274", "product_id": "prod_uEf2YK1rnl", "slug": "065-adidas-liverpool-fc-26-27-third-jersey", "brand": "ADIDAS", "name": "Liverpool Fc 26/27 Third Jersey", "images": 4, "price_usd": 1579.2}
{"code": "tommy-hilfiger-6431-9934964", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "gap-2969-6099274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "gap-2969-6099274", "product_id": "prod_ARbs1igzRC", "slug": "067-gap-logo-t-shirt", "brand": "GAP", "name": "Logo T-Shirt", "images": 1, "price_usd": 479.95}
{"code": "gap-0710-4300374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "gap-0710-4300374", "product_id": "prod_p5gklVeMgb", "slug": "068-gap-logo-t-shirt", "brand": "GAP", "name": "Logo T-Shirt", "images": 1, "price_usd": 479.95}
{"code": "hm-0924-7104664", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "adidas-9961-7990474", "product_id": "prod_9URFqgRua4", "slug": "070-adidas-liverpool-fc-26-27-home-fan-jersey", "brand": "ADIDAS", "name": "Liverpool FC 26/27 Home Fan Jersey", "images": 1, "price_usd": 451.44}
{"code": "gap-3366-0689274", "product_id": "prod_xiBoCpeNMW", "slug": "071-gap-pique-polo-shirt", "brand": "GAP", "name": "Pique Polo Shirt", "images": 1, "price_usd": 647.14}
{"code": "hm-0924-7104664", "product_id": "prod_nWaqfPa3Cp", "slug": "069-h-m-loose-fit-interlock-t-shirt", "brand": "H&M", "name": "Loose Fit Interlock T-shirt", "images": 6, "price_usd": 239.52}
{"code": "adidas-9961-7990474", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "gap-3366-0689274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "adidas-6543-9526964", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "adidas-6543-9526964", "product_id": "prod_vohmzDoNU0", "slug": "072-adidas-mercedes-amg-petronas-formula-1-team-miami-tee", "brand": "ADIDAS", "name": "Mercedes - AMG Petronas Formula 1 Team Miami Tee", "images": 1, "price_usd": 679.44}
{"code": "giordano-8229-4161474", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "giordano-8229-4161474", "product_id": "prod_2h5Y5XM6fr", "slug": "073-giordano-giordano-by-poly-pac-20-24-28-fury-unbreakable-pp-expandable", "brand": "Giordano", "name": "Giordano by Poly Pac 20'' 24\" 28'' Fury UNBREAKABLE PP Expandable Travel Luggage - GA2620 - Black", "images": 6, "price_usd": 3835.2}
{"code": "adidas-9235-1679564", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "adidas-9235-1679564", "product_id": "prod_HbJVtlDrOm", "slug": "074-adidas-vl-court-base-shoes", "brand": "ADIDAS", "name": "VL Court Base Shoes", "images": 8, "price_usd": 745.92}
{"code": "hm-3158-4937164", "product_id": "prod_eH46l8wVqS", "slug": "075-h-m-relaxed-fit-zip-top-rib-knit-jumper", "brand": "H&M", "name": "Relaxed Fit Zip-top rib-knit jumper", "images": 1, "price_usd": 719.52}
{"code": "hm-3158-4937164", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-5561-3429464", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "mango-man-6546-3612374", "product_id": "prod_GpMUcnUYI5", "slug": "077-mango-man-fine-knit-cotton-blend-sweater", "brand": "MANGO Man", "name": "Fine Knit Cotton-Blend Sweater", "images": 1, "price_usd": 959.52}
{"code": "hm-5561-3429464", "product_id": "prod_se78jaGony", "slug": "076-h-m-loose-fit-scuba-zip-through-hoodie", "brand": "H&M", "name": "Loose Fit Scuba Zip-through hoodie", "images": 2, "price_usd": 719.52}
{"code": "mango-man-6546-3612374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "adidas-3065-8268274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "adidas-3065-8268274", "product_id": "prod_RgJeNhrwyV", "slug": "078-adidas-trefoil-cushion-crew-socks-3-pairs", "brand": "ADIDAS", "name": "Trefoil Cushion Crew Socks 3 Pairs", "images": 1, "price_usd": 314.64}
{"code": "mango-man-5487-0122374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "mango-man-5487-0122374", "product_id": "prod_LKWpHOn63V", "slug": "079-mango-man-fine-knit-sweater-with-zip", "brand": "MANGO Man", "name": "Fine-Knit Sweater With Zip", "images": 1, "price_usd": 1055.52}
{"code": "hm-6071-6565464", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "harley-davidson-6140-0807274", "product_id": "prod_CvFSrJgpzQ", "slug": "081-harley-davidson-h-d-tech-vented-short-sleeve-shirt", "brand": "Harley-Davidson", "name": "H-D\u00ae TECH Vented Short Sleeve Shirt", "images": 3, "price_usd": 2409.6}
{"code": "hm-6071-6565464", "product_id": "prod_PcANMcnNFW", "slug": "080-h-m-10-pack-mid-trunks-with-lycra", "brand": "H&M", "name": "10-pack mid trunks with Lycra\u00ae", "images": 5, "price_usd": 959.52}
{"code": "harley-davidson-6140-0807274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "harley-davidson-6555-0017274", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "mango-man-6179-9132374", "product_id": "prod_5sJQZnCRf8", "slug": "083-mango-man-fine-knit-cotton-blend-sweater", "brand": "MANGO Man", "name": "Fine Knit Cotton-Blend Sweater", "images": 1, "price_usd": 959.52}
{"code": "harley-davidson-6555-0017274", "product_id": "prod_YBVETxCGlb", "slug": "082-harley-davidson-harley-davidson-vintage-drop-h-ringer-tee", "brand": "Harley-Davidson", "name": "Harley-Davidson\u00ae Vintage Drop H Ringer Tee", "images": 3, "price_usd": 993.6}
{"code": "mango-man-6179-9132374", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-3272-3490754", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "hm-3272-3490754", "product_id": "prod_FLTGX8NaP3", "slug": "084-h-m-regular-jeans", "brand": "H&M", "name": "Regular Jeans", "images": 3, "price_usd": 479.52}
{"code": "levi-s-8540-2089554", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "levi-s-8540-2089554", "product_id": "prod_kQJKT9MVfb", "slug": "085-levi-s-levi-s-men-s-505-regular-jeans-00505-3166", "brand": "Levi's", "name": "Levi's\u00ae Men's 505\u2122 Regular Jeans 00505-3166", "images": 5, "price_usd": 879.36}
{"code": "volkswagen-4831-1155093", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "cotton-on-6837-1419164", "product_id": "prod_DzgPsx31Wa", "slug": "087-cotton-on-baggy-jeans", "brand": "Cotton On", "name": "Baggy Jeans", "images": 1, "price_usd": 822.86}
{"code": "volkswagen-4831-1155093", "product_id": "prod_3T2WpPTcf5", "slug": "086-volkswagen-slim-long-jeans", "brand": "Volkswagen", "name": "Slim Long Jeans", "images": 4, "price_usd": 763.2}
{"code": "cotton-on-6837-1419164", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "levi-s-4894-3133664", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "levi-s-4894-3133664", "product_id": "prod_ml8x3nOJap", "slug": "088-levi-s-levi-s-men-s-505-regular-jeans-00505-3276", "brand": "Levi's", "name": "Levi's\u00ae Men's 505\u2122 Regular Jeans 00505-3276", "images": 3, "price_usd": 879.36}
{"code": "volkswagen-4840-0055093", "product_id": "prod_5sSQZnCRf8", "slug": "089-volkswagen-straight-long-jeans", "brand": "Volkswagen", "name": "Straight Long Jeans", "images": 2, "price_usd": 763.2}
{"code": "volkswagen-4840-0055093", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "connor-3964-1987644", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "connor-3964-1987644", "product_id": "prod_M3YnyloHRU", "slug": "090-connor-toronto-straight-jean", "brand": "Connor", "name": "Toronto Straight Jean", "images": 3, "price_usd": 690.77}
{"code": "levi-s-8200-6615651", "product_id": "prod_p5CklVeMgb", "slug": "091-levi-s-511-slim-fit-jeans-04511-1907", "brand": "Levi's", "name": "511 Slim Fit Jeans 04511-1907", "images": 2, "price_usd": 1071.36}
{"code": "levi-s-8200-6615651", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "fidelio-1184-7387944", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "fidelio-1184-7387944", "product_id": "prod_sel8jaGony", "slug": "092-fidelio-601-slim-fit-washed-denim-jeans", "brand": "FIDELIO", "name": "601 Slim Fit Washed Denim Jeans", "images": 3, "price_usd": 523.2}
{"code": "levi-s-9015-6656924", "action": "create", "error": {"code": "validation_error", "message": "Slug has already been taken", "details": {"slug": ["has already been taken"]}}}
{"code": "drum-3906-4664754", "product_id": "prod_86PR0I7fEb", "slug": "094-drum-drum-casual-skinny-to-slim-fit-jeans-dark-blue", "brand": "DRUM", "name": "DRUM Casual Skinny to Slim Fit Jeans- Dark Blue", "images": 2, "price_usd": 479.52}
{"code": "connor-3962-2987644", "product_id": "prod_arstNvLSUA", "slug": "095-connor-arran-straight-jean", "brand": "Connor", "name": "Arran Straight Jean", "images": 5, "price_usd": 690.77}
{"code": "trendyol-3864-8297064", "product_id": "prod_Q8DA48cIW7", "slug": "096-trendyol-essential-fit-jeans-denim-pants", "brand": "Trendyol", "name": "Essential Fit Jeans Denim Pants", "images": 1, "price_usd": 520.46}
{"code": "electro-denim-lab-7932-5806954", "product_id": "prod_Q8PA48cIW7", "slug": "097-electro-denim-lab-dub-relaxed-fit-jeans", "brand": "Electro Denim Lab", "name": "Dub Relaxed Fit Jeans", "images": 5, "price_usd": 374.4}
{"code": "drum-5753-5393434", "product_id": "prod_86uR0I7fEb", "slug": "098-drum-drum-digital-print-denim-straight-cut-jeans-brown", "brand": "DRUM", "name": "DRUM Digital Print Denim Straight Cut Jeans- Brown", "images": 4, "price_usd": 671.52}
{"code": "trendyol-4181-4687064", "product_id": "prod_oD3lrKdXja", "slug": "099-trendyol-slim-fit-stretch-fabric-denim-pants-jeans", "brand": "Trendyol", "name": "Slim Fit Stretch Fabric Denim Pants Jeans", "images": 1, "price_usd": 702.24}
{"code": "urban-revivo-4820-1281854", "product_id": "prod_4aROlu6p4z", "slug": "100-urban-revivo-denim-shorts", "brand": "Urban Revivo", "name": "Denim Shorts", "images": 6, "price_usd": 431.57}
{"code": "hm-2256-5380854", "product_id": "prod_q0k31JYFdu", "slug": "101-h-m-2-pack-regular-fit-shorts", "brand": "H&M", "name": "2-pack Regular Fit Shorts", "images": 2, "price_usd": 479.52}
{"code": "hm-5374-3237954", "product_id": "prod_JyHaEBgznC", "slug": "102-h-m-regular-fit-linen-blend-shorts", "brand": "H&M", "name": "Regular Fit Linen-blend shorts", "images": 2, "price_usd": 479.52}
{"code": "cotton-on-4000-9099341", "product_id": "prod_tCbyJwvFCu", "slug": "103-cotton-on-easy-shorts", "brand": "Cotton On", "name": "Easy Shorts", "images": 1, "price_usd": 588.24}
{"code": "connor-3932-5487644", "product_id": "prod_NuazXvxTk8", "slug": "104-connor-darwin-chino-stretch-short", "brand": "Connor", "name": "Darwin Chino Stretch Short", "images": 3, "price_usd": 561.17}
{"code": "fidelio-4233-8849254", "product_id": "prod_XMpbb5HTmV", "slug": "105-fidelio-chino-super-shorts", "brand": "FIDELIO", "name": "Chino Super Shorts", "images": 5, "price_usd": 427.2}
{"code": "cotton-on-5986-9744864", "product_id": "prod_kQYKT9MVfb", "slug": "106-cotton-on-easy-active-shorts", "brand": "Cotton On", "name": "Easy Active Shorts", "images": 1, "price_usd": 428.54}
{"code": "cotton-on-6085-0724713", "product_id": "prod_ZqQMUPd7wV", "slug": "107-cotton-on-stretch-swim-shorts", "brand": "Cotton On", "name": "Stretch Swim Shorts", "images": 7, "price_usd": 588.24}
{"code": "under-armour-0166-9009864", "product_id": "prod_Sm1IszOMIv", "slug": "108-under-armour-challenger-core-men-s-7-5-shorts", "brand": "Under Armour", "name": "Challenger Core Men's 7.5\" Shorts", "images": 1, "price_usd": 360.24}
{"code": "cotton-on-1627-2412493", "product_id": "prod_LKEpHOn63V", "slug": "109-cotton-on-everyday-fleece-shorts", "brand": "Cotton On", "name": "Everyday Fleece Shorts", "images": 2, "price_usd": 433.44}
{"code": "connor-3936-8387644", "product_id": "prod_KAw5Q8YFGz", "slug": "110-connor-jethro-cargo-short", "brand": "Connor", "name": "Jethro Cargo Short", "images": 1, "price_usd": 561.17}
{"code": "nike-9182-7836064", "product_id": "prod_hwPBvQ85Ws", "slug": "111-nike-miler-dri-fit-5-brief-lined-running-shorts", "brand": "Nike", "name": "Miler Dri-FIT 5\" Brief-Lined Running Shorts", "images": 5, "price_usd": 615.6}
{"code": "electro-denim-lab-8284-8755973", "product_id": "prod_IxJvBlcSU2", "slug": "112-electro-denim-lab-tailored-shorts", "brand": "Electro Denim Lab", "name": "Tailored Shorts", "images": 4, "price_usd": 345.6}
{"code": "electro-denim-lab-5883-6755973", "product_id": "prod_KAF5Q8YFGz", "slug": "113-electro-denim-lab-tailored-shorts", "brand": "Electro Denim Lab", "name": "Tailored Shorts", "images": 3, "price_usd": 345.6}
{"code": "under-armour-0103-4898864", "product_id": "prod_iNvDfaj7G4", "slug": "114-under-armour-vanish-woven-2-0-men-s-8-shorts", "brand": "Under Armour", "name": "Vanish Woven 2.0 Men's 8\" Shorts", "images": 2, "price_usd": 668.64}
{"code": "under-armour-9965-8909864", "product_id": "prod_6VsgxZbenF", "slug": "115-under-armour-vanish-woven-2-0-men-s-6-shorts", "brand": "Under Armour", "name": "Vanish Woven 2.0 Men's 6\" Shorts", "images": 2, "price_usd": 668.64}
{"code": "hm-4916-5208854", "product_id": "prod_ARKs1igzRC", "slug": "116-h-m-oversized-fit-printed-boxy-vest-top", "brand": "H&M", "name": "Oversized Fit Printed boxy vest top", "images": 2, "price_usd": 383.52}
{"code": "hm-3300-2208854", "product_id": "prod_lPxiGAYKIU", "slug": "117-h-m-loose-fit-printed-vest-top", "brand": "H&M", "name": "Loose Fit Printed vest top", "images": 2, "price_usd": 143.52}
{"code": "hm-0791-0454064", "product_id": "prod_yStr3EWPH8", "slug": "118-h-m-regular-fit-cotton-vest-top", "brand": "H&M", "name": "Regular Fit Cotton vest top", "images": 3, "price_usd": 239.52}
{"code": "jack-jones-5952-7458954", "product_id": "prod_z2zcwP3ZbD", "slug": "119-jack-jones-charge-bomber-jacket", "brand": "Jack & Jones", "name": "Charge Bomber Jacket", "images": 1, "price_usd": 673.63}
{"code": "jordan-9145-3516754", "product_id": "prod_cE2uasQi8r", "slug": "120-jordan-brooklyn-tricot-track-jacket", "brand": "Jordan", "name": "Brooklyn Tricot Track Jacket", "images": 4, "price_usd": 861.12}
{"code": "under-armour-5681-1265724", "product_id": "prod_UKkLWZg9DA", "slug": "121-under-armour-meridian-zip-jacket", "brand": "Under Armour", "name": "Meridian \u00bc Zip Jacket", "images": 1, "price_usd": 861.6}
{"code": "desigual-1923-6483534", "product_id": "prod_bqqZn4Y5Fq", "slug": "122-desigual-contrast-collar-jacket", "brand": "Desigual", "name": "Contrast collar jacket", "images": 3, "price_usd": 2373.6}
{"code": "mango-man-1932-0456174", "product_id": "prod_UK5LWZg9DA", "slug": "123-mango-man-denim-overshirt-with-pockets", "brand": "MANGO Man", "name": "Denim Overshirt With Pockets", "images": 7, "price_usd": 1727.52}
{"code": "nike-3433-7819904", "product_id": "prod_gbMHJdmfrX", "slug": "124-nike-stride-repel-uv-running-jacket", "brand": "Nike", "name": "Stride Repel UV Running Jacket", "images": 1, "price_usd": 1773.84}
{"code": "fitleasure-6107-0154414", "product_id": "prod_EwFhxLZ9ck", "slug": "125-fitleasure-men-s-navy-training-track-jacket", "brand": "Fitleasure", "name": "Men's Navy Training Track Jacket", "images": 3, "price_usd": 331.2}
{"code": "mango-man-7595-9855154", "product_id": "prod_VMWXmZF31w", "slug": "126-mango-man-wool-overshirt-with-buttons", "brand": "MANGO Man", "name": "Wool Overshirt with Buttons", "images": 1, "price_usd": 1919.52}
{"code": "fitleasure-7254-8558544", "product_id": "prod_uh92YK1rnl", "slug": "127-fitleasure-ultraflex-men-s-premium-grey-athleisure-blazer", "brand": "Fitleasure", "name": "UltraFlex Men\u2019s Premium Grey Athleisure Blazer", "images": 6, "price_usd": 448.8}
{"code": "tommy-hilfiger-3102-7968464", "product_id": "prod_OymJLhNcSb", "slug": "128-tommy-hilfiger-washed-flag-embroidery-denim-chore-jacket-th-mainline", "brand": "Tommy Hilfiger", "name": "Washed Flag Embroidery Denim Chore Jacket - Th Mainline", "images": 1, "price_usd": 2292.48}
{"code": "adidas-6064-9829864", "product_id": "prod_AeZs1igzRC", "slug": "129-adidas-lifestyle-barreda-shoes-men-white-jp5928", "brand": "ADIDAS", "name": "Lifestyle Barreda Shoes Men White JP5928", "images": 1, "price_usd": 803.52}
{"code": "adidas-7467-6165014", "product_id": "prod_pQHklVeMgb", "slug": "130-adidas-runfalcon-5-running-shoes", "brand": "ADIDAS", "name": "Runfalcon 5 Running Shoes", "images": 1, "price_usd": 717.12}
{"code": "adidas-3835-1386964", "product_id": "prod_n0tqfPa3Cp", "slug": "131-adidas-lifestyle-vl-court-3-0-shoes-men-white-jr8635", "brand": "ADIDAS", "name": "Lifestyle VL Court 3.0 Shoes Men White JR8635", "images": 4, "price_usd": 947.52}
{"code": "adidas-3902-2344624", "product_id": "prod_9LeFqgRua4", "slug": "132-adidas-adizero-evo-sl-shoes", "brand": "ADIDAS", "name": "Adizero EVO SL Shoes", "images": 3, "price_usd": 3211.2}
{"code": "new-balance-7276-4452864", "product_id": "prod_xDjoCpeNMW", "slug": "133-new-balance-fuelcell-rebel-v5-running-shoes", "brand": "New Balance", "name": "FuelCell Rebel v5 Running Shoes", "images": 1, "price_usd": 2180.64}
{"code": "nike-8718-6589064", "product_id": "prod_vlBmzDoNU0", "slug": "134-nike-air-monarch-iv-workout-shoes", "brand": "Nike", "name": "Air Monarch IV Workout Shoes", "images": 2, "price_usd": 825.6}
{"code": "under-armour-5894-7391454", "product_id": "prod_2BQY5XM6fr", "slug": "135-under-armour-halo-runner-se-men-s-running-shoes", "brand": "Under Armour", "name": "Halo Runner SE Men's Running Shoes", "images": 5, "price_usd": 1725.12}
{"code": "new-balance-0654-0936864", "product_id": "prod_HZaVtlDrOm", "slug": "136-new-balance-603-lifestyle-shoes", "brand": "New Balance", "name": "603 Lifestyle Shoes", "images": 1, "price_usd": 1163.81}
{"code": "hm-1277-2755944", "product_id": "prod_eVO6l8wVqS", "slug": "137-h-m-canvas-espadrilles", "brand": "H&M", "name": "Canvas espadrilles", "images": 3, "price_usd": 383.52}
{"code": "adidas-7643-4564454", "product_id": "prod_s6d8jaGony", "slug": "138-adidas-handball-spezial-shoes", "brand": "ADIDAS", "name": "Handball Spezial Shoes", "images": 1, "price_usd": 1437.12}
{"code": "puma-7967-0821193", "product_id": "prod_GknUcnUYI5", "slug": "139-puma-puma-shuffle", "brand": "PUMA", "name": "Puma Shuffle", "images": 3, "price_usd": 971.04}
{"code": "veja-7352-9893832", "product_id": "prod_RHaeNhrwyV", "slug": "140-veja-campo-chromefree-sneakers", "brand": "Veja", "name": "Campo Chromefree Sneakers", "images": 1, "price_usd": 3600.0}
{"code": "new-balance-2615-5260964", "product_id": "prod_L50pHOn63V", "slug": "141-new-balance-530-lifestyle-shoes", "brand": "New Balance", "name": "530 Lifestyle Shoes", "images": 1, "price_usd": 1569.46}
{"code": "tommy-hilfiger-6391-3924164", "product_id": "prod_PusNMcnNFW", "slug": "142-tommy-hilfiger-the-greenwich-edge-leather-sneakers-tommy-jeans", "brand": "Tommy Hilfiger", "name": "The Greenwich Edge Leather Sneakers - Tommy Jeans", "images": 2, "price_usd": 1615.68}
{"code": "veja-6434-3897744", "product_id": "prod_CmGSrJgpzQ", "slug": "143-veja-campo-chromefree-sneakers", "brand": "Veja", "name": "Campo Chromefree Sneakers", "images": 2, "price_usd": 3600.0}
{"code": "veja-7346-2897744", "product_id": "prod_YjXETxCGlb", "slug": "144-veja-volley-o-t-leather-sneakers", "brand": "Veja", "name": "Volley O.T. Leather Sneakers", "images": 1, "price_usd": 3480.0}
{"code": "new-balance-8119-1352864", "product_id": "prod_58aQZnCRf8", "slug": "145-new-balance-2010-lifestyle-shoes", "brand": "New Balance", "name": "2010 Lifestyle Shoes", "images": 1, "price_usd": 2128.32}
{"code": "new-balance-8362-1136064", "product_id": "prod_Fp1GX8NaP3", "slug": "146-new-balance-9060-lifestyle-shoes", "brand": "New Balance", "name": "9060 Lifestyle Shoes", "images": 5, "price_usd": 2106.38}
{"code": "hm-3543-9075264", "product_id": "prod_kAaKT9MVfb", "slug": "147-h-m-chelsea-boots", "brand": "H&M", "name": "Chelsea boots", "images": 3, "price_usd": 719.52}
{"code": "dr-cardin-3393-7528864", "product_id": "prod_31YWpPTcf5", "slug": "148-dr-cardin-dr-cardin-men-pillow-foam-faux-leather-chelsea-boot-shoe-tbp", "brand": "Dr. Cardin", "name": "Dr Cardin Men Pillow Foam Faux Leather Chelsea Boot Shoe TBP-6855", "images": 7, "price_usd": 420.34}
{"code": "hunter-boots-3275-8311344", "product_id": "prod_DcHPsx31Wa", "slug": "149-hunter-boots-men-s-play-short-boots", "brand": "Hunter Boots", "name": "MEN'S PLAY SHORT BOOTS", "images": 6, "price_usd": 2731.2}
{"code": "mango-man-1327-3466174", "product_id": "prod_miRx3nOJap", "slug": "150-mango-man-suede-leather-ankle-boots", "brand": "MANGO Man", "name": "Suede Leather Ankle Boots", "images": 1, "price_usd": 2303.52}
{"code": "clarks-5776-2553954", "product_id": "prod_58IQZnCRf8", "slug": "151-clarks-mens-wallabee-shoes-blue-green-stripe", "brand": "CLARKS", "name": "Mens Wallabee Shoes - Blue Green Stripe", "images": 2, "price_usd": 3643.2}
{"code": "clarks-2605-0299864", "product_id": "prod_MWEnyloHRU", "slug": "152-clarks-mens-paulson-up-boots-black-leather", "brand": "CLARKS", "name": "Mens Paulson Up Boots - Black Leather", "images": 6, "price_usd": 2395.2}
{"code": "hm-5557-0718954", "product_id": "prod_pQSklVeMgb", "slug": "153-h-m-regular-fit-merino-wool-jumper", "brand": "H&M", "name": "Regular Fit Merino wool jumper", "images": 5, "price_usd": 959.52}
{"code": "hm-3731-3617164", "product_id": "prod_s6i8jaGony", "slug": "154-h-m-regular-fit-rib-knit-cotton-jumper", "brand": "H&M", "name": "Regular Fit Rib-knit cotton jumper", "images": 4, "price_usd": 671.52}
{"code": "harley-davidson-0026-6212544", "product_id": "prod_rvRClYtECF", "slug": "155-harley-davidson-h-d-realtree-apg-pullover-hoodie", "brand": "Harley-Davidson", "name": "H-D\u00ae Realtree\u00ae APG Pullover Hoodie", "images": 4, "price_usd": 1881.6}
{"code": "gap-6704-2923414", "product_id": "prod_8gNR0I7fEb", "slug": "156-gap-heritage-logo-zip-hoodie", "brand": "GAP", "name": "Heritage Logo Zip Hoodie", "images": 1, "price_usd": 1071.6}
{"code": "gap-1231-2925864", "product_id": "prod_aC8tNvLSUA", "slug": "157-gap-arch-sherpa-zip-hoodie", "brand": "GAP", "name": "Arch Sherpa Zip Hoodie", "images": 2, "price_usd": 840.67}
{"code": "cotton-on-6809-2319164", "product_id": "prod_QRPA48cIW7", "slug": "158-cotton-on-zip-up-hoodie", "brand": "Cotton On", "name": "Zip Up Hoodie", "images": 1, "price_usd": 573.12}
{"code": "cotton-on-6979-5652664", "product_id": "prod_QRNA48cIW7", "slug": "159-cotton-on-box-fit-hoodie", "brand": "Cotton On", "name": "Box Fit Hoodie", "images": 1, "price_usd": 357.6}
{"code": "gap-1516-1025214", "product_id": "prod_8g2R0I7fEb", "slug": "160-gap-french-heritage-logo-zip-hoodie", "brand": "GAP", "name": "French Heritage Logo Zip Hoodie", "images": 1, "price_usd": 735.5}
+42
View File
@@ -0,0 +1,42 @@
require 'json'
require 'open-uri'
store = Spree::Store.default
taxonomy = Spree::Taxonomy.find_or_create_by!(name: "Categories", store: store)
shipping_category = Spree::ShippingCategory.first_or_create!(name: "Default")
products_file = "/home/juxtapo/products.jsonl"
File.readlines(products_file).each_with_index do |line, idx|
data = JSON.parse(line)
taxon = taxonomy.taxons.find_or_create_by!(name: data["brand"]) do |t|
t.parent = taxonomy.root
end
product = Spree::Product.create!(
name: data["name"],
price: data["price"].to_f,
description: "#{data["brand"]} - #{data["name"]}",
available_on: Time.current,
shipping_category: shipping_category,
status: "active",
stores: [store]
)
product.taxons << taxon
# attach first image
if data["imgs"] && data["imgs"].first
begin
img_url = data["imgs"].first
downloaded = URI.open(img_url)
product.images.create!(attachment: { io: downloaded, filename: "#{data["code"]}.jpg" })
puts "✅ #{idx+1}/30: #{data["name"]} (#{data["brand"]}) - RM#{data["price"]}"
rescue => e
puts "⚠️ #{idx+1}/30: #{data["name"]} - image failed: #{e.message}"
end
end
end
puts "Done! Total products: #{Spree::Product.count}"
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""
Pak Jabit — Product Image Pipeline
PhotoRoom API: remove background + studio-quality packshot
Usage:
python3 photoroom_pipeline.py --input products/01-gshock-*/image-1.jpg
python3 photoroom_pipeline.py --input-dir products/ --all
python3 photoroom_pipeline.py --input products/01-gshock-*/image-1.jpg --dry-run
Env:
PHOTOROOM_API_KEY — sandbox_ prefix for testing (watermarked), real key for production
"""
import argparse, glob, json, os, sys, time
from pathlib import Path
import urllib.request, urllib.error
API_KEY = os.environ.get("PHOTOROOM_API_KEY")
EDIT_URL = "https://image-api.photoroom.com/v2/edit"
SEGMENT_URL = "https://sdk.photoroom.com/v1/segment"
def check_account():
req = urllib.request.Request(
"https://image-api.photoroom.com/v2/account",
headers={"x-api-key": API_KEY},
)
with urllib.request.urlopen(req, timeout=15) as r:
data = json.loads(r.read())
print(f"Plan: {data['plan']} | Credits: {data['images']['available']}/{data['images']['subscription']}")
return data
def remove_background(image_path: str, output_path: str):
boundary = "----PakJabitBoundary"
filename = os.path.basename(image_path)
with open(image_path, "rb") as f:
image_data = f.read()
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="image_file"; filename="{filename}"\r\n'
f"Content-Type: image/jpeg\r\n\r\n"
).encode() + image_data + (
f"\r\n--{boundary}\r\n"
f'Content-Disposition: form-data; name="format"\r\n\r\npng\r\n'
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="size"\r\n\r\nhd\r\n'
f"--{boundary}--\r\n"
).encode()
req = urllib.request.Request(SEGMENT_URL, data=body, method="POST", headers={
"x-api-key": API_KEY,
"Content-Type": f"multipart/form-data; boundary={boundary}",
})
with urllib.request.urlopen(req, timeout=60) as r:
with open(output_path, "wb") as out:
out.write(r.read())
return output_path
def studio_packshot(image_path: str, output_path: str, bg_color: str = "FFFFFF"):
boundary = "----PakJabitBoundary"
filename = os.path.basename(image_path)
with open(image_path, "rb") as f:
image_data = f.read()
fields = [
("background.color", bg_color),
("padding", "0.1"),
("shadow.mode", "ai.soft"),
("outputSize", "1200x1600"),
("export.format", "jpeg"),
("removeBackground", "true"),
]
body = b""
for name, value in fields:
body += (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'
).encode()
body += (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="imageFile"; filename="{filename}"\r\n'
f"Content-Type: image/jpeg\r\n\r\n"
).encode() + image_data + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(EDIT_URL, data=body, method="POST", headers={
"x-api-key": API_KEY,
"Content-Type": f"multipart/form-data; boundary={boundary}",
})
with urllib.request.urlopen(req, timeout=90) as r:
with open(output_path, "wb") as out:
out.write(r.read())
return output_path
def process_image(image_path: str, output_dir: str, mode: str = "packshot"):
stem = Path(image_path).stem
parent = Path(image_path).parent.name
out_subdir = os.path.join(output_dir, parent)
os.makedirs(out_subdir, exist_ok=True)
if mode == "remove-bg":
out_path = os.path.join(out_subdir, f"{stem}-nobg.png")
remove_background(image_path, out_path)
elif mode == "packshot":
out_path = os.path.join(out_subdir, f"{stem}-studio.jpg")
studio_packshot(image_path, out_path)
else:
raise ValueError(f"Unknown mode: {mode}")
print(f" ✅ {os.path.basename(image_path)} → {os.path.basename(out_path)}")
return out_path
def main():
parser = argparse.ArgumentParser(description="PhotoRoom product image pipeline")
parser.add_argument("--input", nargs="+", help="Image file(s) or glob pattern")
parser.add_argument("--input-dir", help="Process all images in product subdirs")
parser.add_argument("--all", action="store_true", help="Process all image-*.jpg in input-dir")
parser.add_argument("--first-only", action="store_true", help="Only process image-1.jpg per product")
parser.add_argument("--output-dir", default="products-enhanced", help="Output directory")
parser.add_argument("--mode", choices=["remove-bg", "packshot"], default="packshot")
parser.add_argument("--dry-run", action="store_true", help="List files without processing")
parser.add_argument("--account", action="store_true", help="Check account credits")
parser.add_argument("--delay", type=float, default=1.0, help="Delay between API calls (seconds)")
args = parser.parse_args()
if not API_KEY:
print("❌ Set PHOTOROOM_API_KEY env var first")
print(" Sandbox key (free, watermarked): starts with sandbox_")
print(" Get it from: https://app.photoroom.com/api-dashboard")
sys.exit(1)
if args.account:
check_account()
return
images = []
if args.input:
for pattern in args.input:
images.extend(glob.glob(pattern))
elif args.input_dir and args.all:
pattern = "image-1.jpg" if args.first_only else "image-*.jpg"
images = sorted(glob.glob(os.path.join(args.input_dir, "**", pattern), recursive=True))
else:
parser.print_help()
return
if not images:
print("No images found")
return
print(f"📸 {len(images)} images | mode: {args.mode} | output: {args.output_dir}")
if args.dry_run:
for img in images:
print(f" would process: {img}")
return
check_account()
print()
ok, fail = 0, 0
for i, img in enumerate(images, 1):
print(f"[{i}/{len(images)}] {img}")
try:
process_image(img, args.output_dir, args.mode)
ok += 1
except urllib.error.HTTPError as e:
print(f" ❌ HTTP {e.code}: {e.read().decode()[:200]}")
fail += 1
if e.code == 429:
print(" ⏳ Rate limited, waiting 30s...")
time.sleep(30)
except Exception as e:
print(f" ❌ {e}")
fail += 1
if i < len(images):
time.sleep(args.delay)
print(f"\n✅ Done: {ok} ok, {fail} failed")
if __name__ == "__main__":
main()
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Pak Jabit — Photoroom return leg.
The missing half of photoroom_pipeline.py: takes a source image, runs it through
Photoroom, and pushes the result back onto the Spree product as a media asset.
source jpg -> Photoroom /v2/edit -> Spree POST /products/:id/media -> verify
Verification is not optional here. Spree's media#create has a url path that
enqueues a background job and answers `head :accepted` — a 202 with no body and
no way to know if the image ever landed. This uses the multipart path, which
returns 201 plus the serialized asset, and then re-reads the product's media to
confirm the asset id is really there.
Usage:
photoroom_to_spree.py --slug 19-electro-denim-lab-chino-pants --live
photoroom_to_spree.py --slug <slug> --position 1 # make it the thumbnail
photoroom_to_spree.py --slug <slug> --dry-run
Keys come from /root/.photoroom/credentials. Sandbox is the default and
watermarks every image; --live uses the paid allowance (10/month) and is the
only way to get a clean result onto a live storefront.
"""
import argparse
import json
import mimetypes
import pathlib
import sys
import urllib.error
import urllib.request
import uuid
import os as _os
def _spree_admin_key() -> str:
"""The Spree admin API key. Required — never defaulted."""
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
if not key:
raise SystemExit(
"SPREE_ADMIN_KEY is not set.\n"
" export SPREE_ADMIN_KEY=<the admin API key>\n"
"It is deliberately not stored in this file — see README.md."
)
return key
SPREE_API = "https://api.jux-lab.com/api/v3/admin"
SPREE_KEY = _spree_admin_key()
PHOTOROOM_EDIT = "https://image-api.photoroom.com/v2/edit"
CREDS = pathlib.Path("/root/.photoroom/credentials")
SOURCE_ROOT = pathlib.Path(
"/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products"
)
def fatal(msg):
print(f"FATAL: {msg}", file=sys.stderr)
sys.exit(1)
def load_key(live: bool) -> str:
if not CREDS.exists():
fatal(f"{CREDS} missing — no API key to use")
want = "PHOTOROOM_LIVE_KEY" if live else "PHOTOROOM_SANDBOX_KEY"
for line in CREDS.read_text().splitlines():
if line.startswith(want + "="):
return line.split("=", 1)[1].strip()
fatal(f"{want} not found in {CREDS}")
def multipart(fields: dict, file_field: str, filename: str, blob: bytes, content_type: str):
boundary = uuid.uuid4().hex
parts = []
for k, v in fields.items():
parts.append(
f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode()
)
parts.append(
f'--{boundary}\r\nContent-Disposition: form-data; name="{file_field}"; '
f'filename="{filename}"\r\nContent-Type: {content_type}\r\n\r\n'.encode()
+ blob
+ b"\r\n"
)
parts.append(f"--{boundary}--\r\n".encode())
return b"".join(parts), f"multipart/form-data; boundary={boundary}"
def spree_get(path):
req = urllib.request.Request(
f"{SPREE_API}{path}", headers={"X-Spree-Api-Key": SPREE_KEY}
)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())
def find_product(slug):
data = spree_get(f"/products?filter[slug_eq]={slug}&per_page=50")
for p in data.get("data", []):
if p.get("slug") == slug or p.get("attributes", {}).get("slug") == slug:
return p
# The filter key varies by Spree build; fall back to a full scan rather than
# reporting "not found" for a product that is actually there.
page = 1
while True:
d = spree_get(f"/products?per_page=100&page={page}")
for p in d.get("data", []):
if p.get("slug") == slug:
return p
if page >= d.get("meta", {}).get("pages", 1):
return None
page += 1
def photoroom(img_path: pathlib.Path, key: str, size: str, bg: str) -> bytes:
body, ctype = multipart(
{"background.color": bg, "outputSize": size, "padding": "0.05"},
"imageFile",
img_path.name,
img_path.read_bytes(),
mimetypes.guess_type(img_path.name)[0] or "image/jpeg",
)
req = urllib.request.Request(
PHOTOROOM_EDIT, data=body, headers={"x-api-key": key, "Content-Type": ctype}
)
try:
with urllib.request.urlopen(req, timeout=180) as r:
data = r.read()
if not data:
fatal("Photoroom returned 200 with an empty body")
if not r.headers.get("content-type", "").startswith("image/"):
fatal(f"Photoroom returned non-image: {r.headers.get('content-type')}")
return data
except urllib.error.HTTPError as e:
fatal(f"Photoroom HTTP {e.code}: {e.read().decode()[:300]}")
def upload_to_spree(product_id: str, blob: bytes, filename: str, position):
# position is always sent. Spree defaults a new asset to position 1, which
# silently makes it the product thumbnail — a watermarked sandbox image went
# live on the storefront that way. Callers pass the append position unless
# they explicitly want the front.
fields = {"type": "Spree::Image", "position": str(position)}
body, ctype = multipart(fields, "attachment", filename, blob, "image/png")
req = urllib.request.Request(
f"{SPREE_API}/products/{product_id}/media",
data=body,
headers={"X-Spree-Api-Key": SPREE_KEY, "Content-Type": ctype},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=180) as r:
return r.status, json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
fatal(f"Spree HTTP {e.code}: {e.read().decode()[:400]}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--slug", required=True)
ap.add_argument("--image", help="source file (default: products/<slug>/image-1.jpg)")
ap.add_argument("--live", action="store_true", help="use the paid key (10/month, no watermark)")
ap.add_argument("--position", type=int,
help="1 makes it the product thumbnail. Default: appended last.")
ap.add_argument("--size", default="1000x1000")
ap.add_argument("--bg", default="FFFFFF")
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
src = pathlib.Path(args.image) if args.image else SOURCE_ROOT / args.slug / "image-1.jpg"
if not src.exists():
fatal(f"source image not found: {src}")
product = find_product(args.slug)
if not product:
fatal(f"no Spree product with slug {args.slug}")
pid = product["id"]
name = product.get("name", "?")
before = spree_get(f"/products/{pid}/media").get("data", [])
print(f"product : {name} ({pid})")
print(f"source : {src} ({src.stat().st_size:,} bytes)")
print(f"media : {len(before)} existing")
print(f"key : {'LIVE (consumes paid quota)' if args.live else 'sandbox (watermarked)'}")
if args.dry_run:
print("\n--dry-run: nothing sent.")
return
key = load_key(args.live)
position = args.position if args.position is not None else len(before) + 1
print(f"\nprocessing... (will land at position {position}"
f"{' — THUMBNAIL' if position == 1 else ''})")
out = photoroom(src, key, args.size, args.bg)
print(f" photoroom ok: {len(out):,} bytes png")
status, resp = upload_to_spree(pid, out, f"{args.slug}-photoroom.png", position)
asset_id = resp.get("data", resp).get("id")
print(f" spree upload: HTTP {status} asset={asset_id} position={position}")
# Re-read rather than trusting the response. A 201 is not proof the asset is
# attached and serving.
after = spree_get(f"/products/{pid}/media").get("data", [])
ids = [m.get("id") for m in after]
if asset_id and asset_id in ids:
url = next((m.get("original_url") or m.get("url") for m in after if m.get("id") == asset_id), None)
print(f"\ncheck ok: media {len(before)} -> {len(after)}, asset present")
if url:
print(f"url: {url}")
else:
print(f"\nCHECK FAILED: asset {asset_id} not in product media after upload", file=sys.stderr)
print(f" media ids: {ids}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+53
View File
@@ -0,0 +1,53 @@
{"brand": "H&M", "code": "hm-3377-7607064", "name": "Oversized Fit Scuba zip-through hoodie", "price": "149.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-3377-7607064-1.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-2.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-3.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-4.jpg", "https://static-my.zacdn.com/p/hm-3377-7607064-5.jpg"]}
{"brand": "Urban Revivo", "code": "urban-revivo-3653-8941654", "name": "Casual Crew Neck Short Sleeve Tee", "price": "119.00", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-3653-8941654-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-3653-8941654-6.jpg"]}
{"brand": "Urban Revivo", "code": "urban-revivo-4852-8181854", "name": "Short Sleeve Shirts", "price": "116.10", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-4852-8181854-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-6.jpg", "https://static-my.zacdn.com/p/urban-revivo-4852-8181854-7.jpg"]}
{"brand": "Nike", "code": "nike-4039-6920264", "name": "Training T-Shirt", "price": "147.25", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/nike-4039-6920264-1.jpg"]}
{"brand": "H&M", "code": "hm-8811-9083444", "name": "Regular Fit Track pants with DryMove™", "price": "149.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-8811-9083444-1.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-2.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-3.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-4.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-5.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-6.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-7.jpg", "https://static-my.zacdn.com/p/hm-8811-9083444-8.jpg"]}
{"brand": "Trendyol", "code": "trendyol-6397-0057634", "name": "Beige Oversize/Wide Cut Long Sleeve Animal Embroidered Sweatshirt TMNAW24SW00164", "price": "56.05", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-6397-0057634-1.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-2.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-3.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-4.jpg", "https://static-my.zacdn.com/p/trendyol-6397-0057634-5.jpg"]}
{"brand": "Trendyol", "code": "trendyol-9722-5541154", "name": "Horse Embroidered Sweatshirt", "price": "55.20", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-9722-5541154-1.jpg"]}
{"brand": "H&M", "code": "hm-1602-9539354", "name": "Relaxed Fit Resort shirt", "price": "99.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-1602-9539354-1.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-2.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-3.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-4.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-5.jpg", "https://static-my.zacdn.com/p/hm-1602-9539354-6.jpg"]}
{"brand": "Trendyol", "code": "trendyol-6524-2362464", "name": "Anthracite*001 Relaxed Mock Neck Plain Sweater Tmnaw26Kz00013", "price": "84.50", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-6524-2362464-1.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-2.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-3.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-4.jpg", "https://static-my.zacdn.com/p/trendyol-6524-2362464-5.jpg"]}
{"brand": "Giordano", "code": "giordano-7623-8198654", "name": "Men's Cotton Short Sleeve Slim Tee 01026250", "price": "37.17", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/giordano-7623-8198654-1.jpg", "https://static-my.zacdn.com/p/giordano-7623-8198654-2.jpg"]}
{"brand": "H&M", "code": "hm-4373-6317064", "name": "Ultra Baggy Jeans", "price": "149.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-4373-6317064-1.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-2.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-3.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-4.jpg", "https://static-my.zacdn.com/p/hm-4373-6317064-5.jpg"]}
{"brand": "H&M", "code": "hm-0024-5243544", "name": "Relaxed Jeans", "price": "129.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-0024-5243544-1.jpg", "https://static-my.zacdn.com/p/hm-0024-5243544-2.jpg", "https://static-my.zacdn.com/p/hm-0024-5243544-3.jpg", "https://static-my.zacdn.com/p/hm-0024-5243544-4.jpg"]}
{"brand": "YSoCool", "code": "ysocool-brand-7905-5999931", "name": "Men Compression Body Shaper Vests Top", "price": "46.55", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/ysocool-brand-7905-5999931-1.jpg", "https://static-my.zacdn.com/p/ysocool-brand-7905-5999931-2.jpg", "https://static-my.zacdn.com/p/ysocool-brand-7905-5999931-3.jpg"]}
{"brand": "Nike", "code": "nike-8330-6036864", "name": "Dri-FIT Training T-Shirt", "price": "113.05", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/nike-8330-6036864-1.jpg"]}
{"brand": "H&M", "code": "hm-8820-5290854", "name": "Loose Fit T-shirt", "price": "49.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-8820-5290854-1.jpg", "https://static-my.zacdn.com/p/hm-8820-5290854-2.jpg", "https://static-my.zacdn.com/p/hm-8820-5290854-3.jpg", "https://static-my.zacdn.com/p/hm-8820-5290854-4.jpg"]}
{"brand": "H&M", "code": "hm-6806-1878823", "name": "Slim Fit T-shirt", "price": "24.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-6806-1878823-1.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-2.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-3.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-4.jpg", "https://static-my.zacdn.com/p/hm-6806-1878823-5.jpg"]}
{"brand": "H&M", "code": "hm-6462-8250054", "name": "Relaxed Fit Textured resort shirt", "price": "99.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-6462-8250054-1.jpg", "https://static-my.zacdn.com/p/hm-6462-8250054-2.jpg", "https://static-my.zacdn.com/p/hm-6462-8250054-3.jpg", "https://static-my.zacdn.com/p/hm-6462-8250054-4.jpg"]}
{"brand": "Trendyol", "code": "trendyol-3134-8272734", "name": "Black Regular Fit 100% Viscose Loose and Loose Summer Shirt TMNSS25GO00048", "price": "90.30", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-3134-8272734-1.jpg"]}
{"brand": "Altinyildiz", "code": "altinyildiz-8185-0860924", "name": "Basic Slim Fit T-Shirt", "price": "52.16", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/altinyildiz-8185-0860924-1.jpg"]}
{"brand": "Nike", "code": "nike-0596-4602954", "name": "Palm Tree Men's Tee", "price": "93.00", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/nike-0596-4602954-1.jpg"]}
{"brand": "Lacoste", "code": "lacoste-6939-3474274", "name": "Monogram Jacquard T-shirt", "price": "539.00", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/lacoste-6939-3474274-1.jpg", "https://static-my.zacdn.com/p/lacoste-6939-3474274-2.jpg", "https://static-my.zacdn.com/p/lacoste-6939-3474274-3.jpg", "https://static-my.zacdn.com/p/lacoste-6939-3474274-4.jpg"]}
{"brand": "H&M", "code": "hm-2842-2711854", "name": "Relaxed Fit Cotton chinos", "price": "129.90", "category": "t-shirts", "imgs": ["https://static-my.zacdn.com/p/hm-2842-2711854-1.jpg", "https://static-my.zacdn.com/p/hm-2842-2711854-2.jpg"]}
{"brand": "Urban Revivo", "code": "urban-revivo-8524-3567374", "name": "Classic Solid Color Thick Shirt in Viscose Blend", "price": "189.05", "category": "shirts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-8524-3567374-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-6.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-7.jpg", "https://static-my.zacdn.com/p/urban-revivo-8524-3567374-8.jpg"]}
{"brand": "H&M", "code": "hm-4144-4577954", "name": "Relaxed Fit Oxford shirt", "price": "129.90", "category": "shirts", "imgs": ["https://static-my.zacdn.com/p/hm-4144-4577954-1.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-2.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-3.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-4.jpg", "https://static-my.zacdn.com/p/hm-4144-4577954-5.jpg"]}
{"brand": "Trendyol", "code": "trendyol-7335-7898364", "name": "Black Regular/Normal Cut Hooded Textured Sweatshirt Tmnaw26Sw00068", "price": "79.03", "category": "shirts", "imgs": ["https://static-my.zacdn.com/p/trendyol-7335-7898364-1.jpg", "https://static-my.zacdn.com/p/trendyol-7335-7898364-2.jpg", "https://static-my.zacdn.com/p/trendyol-7335-7898364-3.jpg"]}
{"brand": "Trendyol", "code": "trendyol-7769-9773614", "name": "Striped Knit Polo Shirt", "price": "85.52", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/trendyol-7769-9773614-1.jpg", "https://static-my.zacdn.com/p/trendyol-7769-9773614-2.jpg", "https://static-my.zacdn.com/p/trendyol-7769-9773614-3.jpg", "https://static-my.zacdn.com/p/trendyol-7769-9773614-4.jpg"]}
{"brand": "H&M", "code": "hm-4087-3660754", "name": "Regular Fit Hole-knit sweater vest", "price": "139.90", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/hm-4087-3660754-1.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-2.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-3.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-4.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-5.jpg", "https://static-my.zacdn.com/p/hm-4087-3660754-6.jpg"]}
{"brand": "H&M", "code": "hm-9694-6720944", "name": "Easy-care Relaxed Fit Linen-blend shorts", "price": "149.90", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/hm-9694-6720944-1.jpg", "https://static-my.zacdn.com/p/hm-9694-6720944-2.jpg", "https://static-my.zacdn.com/p/hm-9694-6720944-3.jpg", "https://static-my.zacdn.com/p/hm-9694-6720944-4.jpg"]}
{"brand": "H&M", "code": "hm-9884-9573454", "name": "Oversized Fit T-shirt", "price": "69.90", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/hm-9884-9573454-1.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-2.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-3.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-4.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-5.jpg", "https://static-my.zacdn.com/p/hm-9884-9573454-6.jpg"]}
{"brand": "Trendyol", "code": "trendyol-1466-7949634", "name": "Brown Oversize/Wide Cut Buttoned Polo Collar Thick Polar Fleece Sweatshirt TMNAW24SW00107", "price": "82.53", "category": "pants", "imgs": ["https://static-my.zacdn.com/p/trendyol-1466-7949634-1.jpg"]}
{"brand": "Ben Sherman", "code": "ben-sherman-1585-5545954", "name": "Short Sleeves Jacquard Polo Shirt", "price": "312.55", "category": "accessories", "imgs": ["https://static-my.zacdn.com/p/ben-sherman-1585-5545954-1.jpg"]}
{"brand": "H&M", "code": "hm-3289-5938864", "name": "Relaxed Fit Sweatshirt", "price": "59.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-3289-5938864-1.jpg", "https://static-my.zacdn.com/p/hm-3289-5938864-2.jpg"]}
{"brand": "H&M", "code": "hm-7368-5259764", "name": "Regular Fit Easy-iron shirt", "price": "79.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-7368-5259764-1.jpg", "https://static-my.zacdn.com/p/hm-7368-5259764-2.jpg"]}
{"brand": "H&M", "code": "hm-0914-3034864", "name": "Polo shirt", "price": "99.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-0914-3034864-1.jpg", "https://static-my.zacdn.com/p/hm-0914-3034864-2.jpg"]}
{"brand": "ADIDAS", "code": "adidas-0957-2958274", "name": "Liverpool Fc 26/27 Third Jersey", "price": "329.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-0957-2958274-1.jpg", "https://static-my.zacdn.com/p/adidas-0957-2958274-2.jpg", "https://static-my.zacdn.com/p/adidas-0957-2958274-3.jpg", "https://static-my.zacdn.com/p/adidas-0957-2958274-4.jpg"]}
{"brand": "Tommy Hilfiger", "code": "tommy-hilfiger-6431-9934964", "name": "Linear Logo Slim Crew Neck T-Shirt - Tommy Jeans", "price": "151.05", "category": "new", "imgs": ["https://static-my.zacdn.com/p/tommy-hilfiger-6431-9934964-1.jpg"]}
{"brand": "GAP", "code": "gap-2969-6099274", "name": "Logo T-Shirt", "price": "99.99", "category": "new", "imgs": ["https://static-my.zacdn.com/p/gap-2969-6099274-1.jpg"]}
{"brand": "GAP", "code": "gap-0710-4300374", "name": "Logo T-Shirt", "price": "99.99", "category": "new", "imgs": ["https://static-my.zacdn.com/p/gap-0710-4300374-1.jpg"]}
{"brand": "H&M", "code": "hm-0924-7104664", "name": "Loose Fit Interlock T-shirt", "price": "49.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-0924-7104664-1.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-2.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-3.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-4.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-5.jpg", "https://static-my.zacdn.com/p/hm-0924-7104664-6.jpg"]}
{"brand": "ADIDAS", "code": "adidas-9961-7990474", "name": "Liverpool FC 26/27 Home Fan Jersey", "price": "94.05", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-9961-7990474-1.jpg"]}
{"brand": "GAP", "code": "gap-3366-0689274", "name": "Pique Polo Shirt", "price": "134.82", "category": "new", "imgs": ["https://static-my.zacdn.com/p/gap-3366-0689274-1.jpg"]}
{"brand": "ADIDAS", "code": "adidas-6543-9526964", "name": "Mercedes - AMG Petronas Formula 1 Team Miami Tee", "price": "141.55", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-6543-9526964-1.jpg"]}
{"brand": "Giordano", "code": "giordano-8229-4161474", "name": "Giordano by Poly Pac 20'' 24\" 28'' Fury UNBREAKABLE PP Expandable Travel Luggage - GA2620 - Black", "price": "799.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/giordano-8229-4161474-1.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-2.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-3.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-4.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-5.jpg", "https://static-my.zacdn.com/p/giordano-8229-4161474-6.jpg"]}
{"brand": "ADIDAS", "code": "adidas-9235-1679564", "name": "VL Court Base Shoes", "price": "155.40", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-9235-1679564-1.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-2.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-3.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-4.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-5.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-6.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-7.jpg", "https://static-my.zacdn.com/p/adidas-9235-1679564-8.jpg"]}
{"brand": "H&M", "code": "hm-3158-4937164", "name": "Relaxed Fit Zip-top rib-knit jumper", "price": "149.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-3158-4937164-1.jpg"]}
{"brand": "H&M", "code": "hm-5561-3429464", "name": "Loose Fit Scuba Zip-through hoodie", "price": "149.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-5561-3429464-1.jpg", "https://static-my.zacdn.com/p/hm-5561-3429464-2.jpg"]}
{"brand": "MANGO Man", "code": "mango-man-6546-3612374", "name": "Fine Knit Cotton-Blend Sweater", "price": "199.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/mango-man-6546-3612374-1.jpg"]}
{"brand": "ADIDAS", "code": "adidas-3065-8268274", "name": "Trefoil Cushion Crew Socks 3 Pairs", "price": "65.55", "category": "new", "imgs": ["https://static-my.zacdn.com/p/adidas-3065-8268274-1.jpg"]}
{"brand": "MANGO Man", "code": "mango-man-5487-0122374", "name": "Fine-Knit Sweater With Zip", "price": "219.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/mango-man-5487-0122374-1.jpg"]}
{"brand": "H&M", "code": "hm-6071-6565464", "name": "10-pack mid trunks with Lycra®", "price": "199.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/hm-6071-6565464-1.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-2.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-3.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-4.jpg", "https://static-my.zacdn.com/p/hm-6071-6565464-5.jpg"]}
{"brand": "Harley-Davidson", "code": "harley-davidson-6140-0807274", "name": "H-D® TECH Vented Short Sleeve Shirt", "price": "502.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/harley-davidson-6140-0807274-1.jpg", "https://static-my.zacdn.com/p/harley-davidson-6140-0807274-2.jpg", "https://static-my.zacdn.com/p/harley-davidson-6140-0807274-3.jpg"]}
{"brand": "Harley-Davidson", "code": "harley-davidson-6555-0017274", "name": "Harley-Davidson® Vintage Drop H Ringer Tee", "price": "207.00", "category": "new", "imgs": ["https://static-my.zacdn.com/p/harley-davidson-6555-0017274-1.jpg", "https://static-my.zacdn.com/p/harley-davidson-6555-0017274-2.jpg", "https://static-my.zacdn.com/p/harley-davidson-6555-0017274-3.jpg"]}
{"brand": "MANGO Man", "code": "mango-man-6179-9132374", "name": "Fine Knit Cotton-Blend Sweater", "price": "199.90", "category": "new", "imgs": ["https://static-my.zacdn.com/p/mango-man-6179-9132374-1.jpg"]}
+77
View File
@@ -0,0 +1,77 @@
{"brand": "H&M", "code": "hm-3272-3490754", "name": "Regular Jeans", "price": "99.90", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/hm-3272-3490754-1.jpg", "https://static-my.zacdn.com/p/hm-3272-3490754-2.jpg", "https://static-my.zacdn.com/p/hm-3272-3490754-3.jpg"]}
{"brand": "Levi's", "code": "levi-s-8540-2089554", "name": "Levi's® Men's 505™ Regular Jeans 00505-3166", "price": "183.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-8540-2089554-1.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-2.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-3.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-4.jpg", "https://static-my.zacdn.com/p/levi-s-8540-2089554-5.jpg"]}
{"brand": "Volkswagen", "code": "volkswagen-4831-1155093", "name": "Slim Long Jeans", "price": "159.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/volkswagen-4831-1155093-1.jpg", "https://static-my.zacdn.com/p/volkswagen-4831-1155093-2.jpg", "https://static-my.zacdn.com/p/volkswagen-4831-1155093-3.jpg", "https://static-my.zacdn.com/p/volkswagen-4831-1155093-4.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-6837-1419164", "name": "Baggy Jeans", "price": "171.43", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6837-1419164-1.jpg"]}
{"brand": "Levi's", "code": "levi-s-4894-3133664", "name": "Levi's® Men's 505™ Regular Jeans 00505-3276", "price": "183.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-4894-3133664-1.jpg", "https://static-my.zacdn.com/p/levi-s-4894-3133664-2.jpg", "https://static-my.zacdn.com/p/levi-s-4894-3133664-3.jpg"]}
{"brand": "Volkswagen", "code": "volkswagen-4840-0055093", "name": "Straight Long Jeans", "price": "159.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/volkswagen-4840-0055093-1.jpg", "https://static-my.zacdn.com/p/volkswagen-4840-0055093-2.jpg"]}
{"brand": "Connor", "code": "connor-3964-1987644", "name": "Toronto Straight Jean", "price": "143.91", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/connor-3964-1987644-1.jpg", "https://static-my.zacdn.com/p/connor-3964-1987644-2.jpg", "https://static-my.zacdn.com/p/connor-3964-1987644-3.jpg"]}
{"brand": "Levi's", "code": "levi-s-8200-6615651", "name": "511 Slim Fit Jeans 04511-1907", "price": "223.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-8200-6615651-1.jpg", "https://static-my.zacdn.com/p/levi-s-8200-6615651-2.jpg"]}
{"brand": "FIDELIO", "code": "fidelio-1184-7387944", "name": "601 Slim Fit Washed Denim Jeans", "price": "109.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/fidelio-1184-7387944-1.jpg", "https://static-my.zacdn.com/p/fidelio-1184-7387944-2.jpg", "https://static-my.zacdn.com/p/fidelio-1184-7387944-3.jpg"]}
{"brand": "Levi's", "code": "levi-s-9015-6656924", "name": "Men's 511™ Slim Jeans 04511-6225", "price": "239.20", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/levi-s-9015-6656924-1.jpg", "https://static-my.zacdn.com/p/levi-s-9015-6656924-2.jpg"]}
{"brand": "DRUM", "code": "drum-3906-4664754", "name": "DRUM Casual Skinny to Slim Fit Jeans- Dark Blue", "price": "99.90", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/drum-3906-4664754-1.jpg", "https://static-my.zacdn.com/p/drum-3906-4664754-2.jpg"]}
{"brand": "Connor", "code": "connor-3962-2987644", "name": "Arran Straight Jean", "price": "143.91", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/connor-3962-2987644-1.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-2.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-3.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-4.jpg", "https://static-my.zacdn.com/p/connor-3962-2987644-5.jpg"]}
{"brand": "Trendyol", "code": "trendyol-3864-8297064", "name": "Essential Fit Jeans Denim Pants", "price": "108.43", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/trendyol-3864-8297064-1.jpg"]}
{"brand": "Electro Denim Lab", "code": "electro-denim-lab-7932-5806954", "name": "Dub Relaxed Fit Jeans", "price": "78.00", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-1.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-2.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-3.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-4.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-7932-5806954-5.jpg"]}
{"brand": "DRUM", "code": "drum-5753-5393434", "name": "DRUM Digital Print Denim Straight Cut Jeans- Brown", "price": "139.90", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/drum-5753-5393434-1.jpg", "https://static-my.zacdn.com/p/drum-5753-5393434-2.jpg", "https://static-my.zacdn.com/p/drum-5753-5393434-3.jpg", "https://static-my.zacdn.com/p/drum-5753-5393434-4.jpg"]}
{"brand": "Trendyol", "code": "trendyol-4181-4687064", "name": "Slim Fit Stretch Fabric Denim Pants Jeans", "price": "146.30", "category": "jeans", "imgs": ["https://static-my.zacdn.com/p/trendyol-4181-4687064-1.jpg"]}
{"brand": "Urban Revivo", "code": "urban-revivo-4820-1281854", "name": "Denim Shorts", "price": "89.91", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/urban-revivo-4820-1281854-1.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-2.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-3.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-4.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-5.jpg", "https://static-my.zacdn.com/p/urban-revivo-4820-1281854-6.jpg"]}
{"brand": "H&M", "code": "hm-2256-5380854", "name": "2-pack Regular Fit Shorts", "price": "99.90", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/hm-2256-5380854-1.jpg", "https://static-my.zacdn.com/p/hm-2256-5380854-2.jpg"]}
{"brand": "H&M", "code": "hm-5374-3237954", "name": "Regular Fit Linen-blend shorts", "price": "99.90", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/hm-5374-3237954-1.jpg", "https://static-my.zacdn.com/p/hm-5374-3237954-2.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-4000-9099341", "name": "Easy Shorts", "price": "122.55", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-4000-9099341-1.jpg"]}
{"brand": "Connor", "code": "connor-3932-5487644", "name": "Darwin Chino Stretch Short", "price": "116.91", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/connor-3932-5487644-1.jpg", "https://static-my.zacdn.com/p/connor-3932-5487644-2.jpg", "https://static-my.zacdn.com/p/connor-3932-5487644-3.jpg"]}
{"brand": "FIDELIO", "code": "fidelio-4233-8849254", "name": "Chino Super Shorts", "price": "89.00", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/fidelio-4233-8849254-1.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-2.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-3.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-4.jpg", "https://static-my.zacdn.com/p/fidelio-4233-8849254-5.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-5986-9744864", "name": "Easy Active Shorts", "price": "89.28", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-5986-9744864-1.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-6085-0724713", "name": "Stretch Swim Shorts", "price": "122.55", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6085-0724713-1.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-2.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-3.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-4.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-5.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-6.jpg", "https://static-my.zacdn.com/p/cotton-on-6085-0724713-7.jpg"]}
{"brand": "Under Armour", "code": "under-armour-0166-9009864", "name": "Challenger Core Men's 7.5\" Shorts", "price": "75.05", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/under-armour-0166-9009864-1.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-1627-2412493", "name": "Everyday Fleece Shorts", "price": "90.30", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/cotton-on-1627-2412493-1.jpg", "https://static-my.zacdn.com/p/cotton-on-1627-2412493-2.jpg"]}
{"brand": "Connor", "code": "connor-3936-8387644", "name": "Jethro Cargo Short", "price": "116.91", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/connor-3936-8387644-1.jpg"]}
{"brand": "Nike", "code": "nike-9182-7836064", "name": "Miler Dri-FIT 5\" Brief-Lined Running Shorts", "price": "128.25", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/nike-9182-7836064-1.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-2.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-3.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-4.jpg", "https://static-my.zacdn.com/p/nike-9182-7836064-5.jpg"]}
{"brand": "Electro Denim Lab", "code": "electro-denim-lab-8284-8755973", "name": "Tailored Shorts", "price": "72.00", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-1.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-2.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-3.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-8284-8755973-4.jpg"]}
{"brand": "Electro Denim Lab", "code": "electro-denim-lab-5883-6755973", "name": "Tailored Shorts", "price": "72.00", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/electro-denim-lab-5883-6755973-1.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-5883-6755973-2.jpg", "https://static-my.zacdn.com/p/electro-denim-lab-5883-6755973-3.jpg"]}
{"brand": "Under Armour", "code": "under-armour-0103-4898864", "name": "Vanish Woven 2.0 Men's 8\" Shorts", "price": "139.30", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/under-armour-0103-4898864-1.jpg", "https://static-my.zacdn.com/p/under-armour-0103-4898864-2.jpg"]}
{"brand": "Under Armour", "code": "under-armour-9965-8909864", "name": "Vanish Woven 2.0 Men's 6\" Shorts", "price": "139.30", "category": "shorts", "imgs": ["https://static-my.zacdn.com/p/under-armour-9965-8909864-1.jpg", "https://static-my.zacdn.com/p/under-armour-9965-8909864-2.jpg"]}
{"brand": "H&M", "code": "hm-4916-5208854", "name": "Oversized Fit Printed boxy vest top", "price": "79.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/hm-4916-5208854-1.jpg", "https://static-my.zacdn.com/p/hm-4916-5208854-2.jpg"]}
{"brand": "H&M", "code": "hm-3300-2208854", "name": "Loose Fit Printed vest top", "price": "29.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/hm-3300-2208854-1.jpg", "https://static-my.zacdn.com/p/hm-3300-2208854-2.jpg"]}
{"brand": "H&M", "code": "hm-0791-0454064", "name": "Regular Fit Cotton vest top", "price": "49.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/hm-0791-0454064-1.jpg", "https://static-my.zacdn.com/p/hm-0791-0454064-2.jpg", "https://static-my.zacdn.com/p/hm-0791-0454064-3.jpg"]}
{"brand": "Jack & Jones", "code": "jack-jones-5952-7458954", "name": "Charge Bomber Jacket", "price": "140.34", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/jack-jones-5952-7458954-1.jpg"]}
{"brand": "Jordan", "code": "jordan-9145-3516754", "name": "Brooklyn Tricot Track Jacket", "price": "179.40", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/jordan-9145-3516754-1.jpg", "https://static-my.zacdn.com/p/jordan-9145-3516754-2.jpg", "https://static-my.zacdn.com/p/jordan-9145-3516754-3.jpg", "https://static-my.zacdn.com/p/jordan-9145-3516754-4.jpg"]}
{"brand": "Under Armour", "code": "under-armour-5681-1265724", "name": "Meridian ¼ Zip Jacket", "price": "179.50", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/under-armour-5681-1265724-1.jpg"]}
{"brand": "Desigual", "code": "desigual-1923-6483534", "name": "Contrast collar jacket", "price": "494.50", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/desigual-1923-6483534-1.jpg", "https://static-my.zacdn.com/p/desigual-1923-6483534-2.jpg", "https://static-my.zacdn.com/p/desigual-1923-6483534-3.jpg"]}
{"brand": "MANGO Man", "code": "mango-man-1932-0456174", "name": "Denim Overshirt With Pockets", "price": "359.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/mango-man-1932-0456174-1.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-2.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-3.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-4.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-5.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-6.jpg", "https://static-my.zacdn.com/p/mango-man-1932-0456174-7.jpg"]}
{"brand": "Nike", "code": "nike-3433-7819904", "name": "Stride Repel UV Running Jacket", "price": "369.55", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/nike-3433-7819904-1.jpg"]}
{"brand": "Fitleasure", "code": "fitleasure-6107-0154414", "name": "Men's Navy Training Track Jacket", "price": "69.00", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/fitleasure-6107-0154414-1.jpg", "https://static-my.zacdn.com/p/fitleasure-6107-0154414-2.jpg", "https://static-my.zacdn.com/p/fitleasure-6107-0154414-3.jpg"]}
{"brand": "MANGO Man", "code": "mango-man-7595-9855154", "name": "Wool Overshirt with Buttons", "price": "399.90", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/mango-man-7595-9855154-1.jpg"]}
{"brand": "Fitleasure", "code": "fitleasure-7254-8558544", "name": "UltraFlex Men’s Premium Grey Athleisure Blazer", "price": "93.50", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/fitleasure-7254-8558544-1.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-2.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-3.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-4.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-5.jpg", "https://static-my.zacdn.com/p/fitleasure-7254-8558544-6.jpg"]}
{"brand": "Tommy Hilfiger", "code": "tommy-hilfiger-3102-7968464", "name": "Washed Flag Embroidery Denim Chore Jacket - Th Mainline", "price": "477.60", "category": "jackets-coats", "imgs": ["https://static-my.zacdn.com/p/tommy-hilfiger-3102-7968464-1.jpg"]}
{"brand": "ADIDAS", "code": "adidas-6064-9829864", "name": "Lifestyle Barreda Shoes Men White JP5928", "price": "167.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-6064-9829864-1.jpg"]}
{"brand": "ADIDAS", "code": "adidas-7467-6165014", "name": "Runfalcon 5 Running Shoes", "price": "149.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-7467-6165014-1.jpg"]}
{"brand": "ADIDAS", "code": "adidas-3835-1386964", "name": "Lifestyle VL Court 3.0 Shoes Men White JR8635", "price": "197.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-3835-1386964-1.jpg", "https://static-my.zacdn.com/p/adidas-3835-1386964-2.jpg", "https://static-my.zacdn.com/p/adidas-3835-1386964-3.jpg", "https://static-my.zacdn.com/p/adidas-3835-1386964-4.jpg"]}
{"brand": "ADIDAS", "code": "adidas-3902-2344624", "name": "Adizero EVO SL Shoes", "price": "669.00", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/adidas-3902-2344624-1.jpg", "https://static-my.zacdn.com/p/adidas-3902-2344624-2.jpg", "https://static-my.zacdn.com/p/adidas-3902-2344624-3.jpg"]}
{"brand": "New Balance", "code": "new-balance-7276-4452864", "name": "FuelCell Rebel v5 Running Shoes", "price": "454.30", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/new-balance-7276-4452864-1.jpg"]}
{"brand": "Nike", "code": "nike-8718-6589064", "name": "Air Monarch IV Workout Shoes", "price": "172.00", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/nike-8718-6589064-1.jpg", "https://static-my.zacdn.com/p/nike-8718-6589064-2.jpg"]}
{"brand": "Under Armour", "code": "under-armour-5894-7391454", "name": "Halo Runner SE Men's Running Shoes", "price": "359.40", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/under-armour-5894-7391454-1.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-2.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-3.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-4.jpg", "https://static-my.zacdn.com/p/under-armour-5894-7391454-5.jpg"]}
{"brand": "New Balance", "code": "new-balance-0654-0936864", "name": "603 Lifestyle Shoes", "price": "242.46", "category": "shoes-sports", "imgs": ["https://static-my.zacdn.com/p/new-balance-0654-0936864-1.jpg"]}
{"brand": "H&M", "code": "hm-1277-2755944", "name": "Canvas espadrilles", "price": "79.90", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/hm-1277-2755944-1.jpg", "https://static-my.zacdn.com/p/hm-1277-2755944-2.jpg", "https://static-my.zacdn.com/p/hm-1277-2755944-3.jpg"]}
{"brand": "ADIDAS", "code": "adidas-7643-4564454", "name": "Handball Spezial Shoes", "price": "299.40", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/adidas-7643-4564454-1.jpg"]}
{"brand": "PUMA", "code": "puma-7967-0821193", "name": "Puma Shuffle", "price": "202.30", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/puma-7967-0821193-1.jpg", "https://static-my.zacdn.com/p/puma-7967-0821193-2.jpg", "https://static-my.zacdn.com/p/puma-7967-0821193-3.jpg"]}
{"brand": "Veja", "code": "veja-7352-9893832", "name": "Campo Chromefree Sneakers", "price": "750.00", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/veja-7352-9893832-1.jpg"]}
{"brand": "New Balance", "code": "new-balance-2615-5260964", "name": "530 Lifestyle Shoes", "price": "326.97", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/new-balance-2615-5260964-1.jpg"]}
{"brand": "Tommy Hilfiger", "code": "tommy-hilfiger-6391-3924164", "name": "The Greenwich Edge Leather Sneakers - Tommy Jeans", "price": "336.60", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/tommy-hilfiger-6391-3924164-1.jpg", "https://static-my.zacdn.com/p/tommy-hilfiger-6391-3924164-2.jpg"]}
{"brand": "Veja", "code": "veja-6434-3897744", "name": "Campo Chromefree Sneakers", "price": "750.00", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/veja-6434-3897744-1.jpg", "https://static-my.zacdn.com/p/veja-6434-3897744-2.jpg"]}
{"brand": "Veja", "code": "veja-7346-2897744", "name": "Volley O.T. Leather Sneakers", "price": "725.00", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/veja-7346-2897744-1.jpg"]}
{"brand": "New Balance", "code": "new-balance-8119-1352864", "name": "2010 Lifestyle Shoes", "price": "443.40", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/new-balance-8119-1352864-1.jpg"]}
{"brand": "New Balance", "code": "new-balance-8362-1136064", "name": "9060 Lifestyle Shoes", "price": "438.83", "category": "shoes-sneakers", "imgs": ["https://static-my.zacdn.com/p/new-balance-8362-1136064-1.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-2.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-3.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-4.jpg", "https://static-my.zacdn.com/p/new-balance-8362-1136064-5.jpg"]}
{"brand": "H&M", "code": "hm-3543-9075264", "name": "Chelsea boots", "price": "149.90", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/hm-3543-9075264-1.jpg", "https://static-my.zacdn.com/p/hm-3543-9075264-2.jpg", "https://static-my.zacdn.com/p/hm-3543-9075264-3.jpg"]}
{"brand": "Dr. Cardin", "code": "dr-cardin-3393-7528864", "name": "Dr Cardin Men Pillow Foam Faux Leather Chelsea Boot Shoe TBP-6855", "price": "87.57", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/dr-cardin-3393-7528864-1.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-2.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-3.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-4.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-5.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-6.jpg", "https://static-my.zacdn.com/p/dr-cardin-3393-7528864-7.jpg"]}
{"brand": "Hunter Boots", "code": "hunter-boots-3275-8311344", "name": "MEN'S PLAY SHORT BOOTS", "price": "569.00", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/hunter-boots-3275-8311344-1.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-2.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-3.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-4.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-5.jpg", "https://static-my.zacdn.com/p/hunter-boots-3275-8311344-6.jpg"]}
{"brand": "MANGO Man", "code": "mango-man-1327-3466174", "name": "Suede Leather Ankle Boots", "price": "479.90", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/mango-man-1327-3466174-1.jpg"]}
{"brand": "CLARKS", "code": "clarks-5776-2553954", "name": "Mens Wallabee Shoes - Blue Green Stripe", "price": "759.00", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/clarks-5776-2553954-1.jpg", "https://static-my.zacdn.com/p/clarks-5776-2553954-2.jpg"]}
{"brand": "CLARKS", "code": "clarks-2605-0299864", "name": "Mens Paulson Up Boots - Black Leather", "price": "499.00", "category": "shoes-boots", "imgs": ["https://static-my.zacdn.com/p/clarks-2605-0299864-1.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-2.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-3.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-4.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-5.jpg", "https://static-my.zacdn.com/p/clarks-2605-0299864-6.jpg"]}
{"brand": "H&M", "code": "hm-5557-0718954", "name": "Regular Fit Merino wool jumper", "price": "199.90", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/hm-5557-0718954-1.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-2.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-3.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-4.jpg", "https://static-my.zacdn.com/p/hm-5557-0718954-5.jpg"]}
{"brand": "H&M", "code": "hm-3731-3617164", "name": "Regular Fit Rib-knit cotton jumper", "price": "139.90", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/hm-3731-3617164-1.jpg", "https://static-my.zacdn.com/p/hm-3731-3617164-2.jpg", "https://static-my.zacdn.com/p/hm-3731-3617164-3.jpg", "https://static-my.zacdn.com/p/hm-3731-3617164-4.jpg"]}
{"brand": "Harley-Davidson", "code": "harley-davidson-0026-6212544", "name": "H-D® Realtree® APG Pullover Hoodie", "price": "392.00", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/harley-davidson-0026-6212544-1.jpg", "https://static-my.zacdn.com/p/harley-davidson-0026-6212544-2.jpg", "https://static-my.zacdn.com/p/harley-davidson-0026-6212544-3.jpg", "https://static-my.zacdn.com/p/harley-davidson-0026-6212544-4.jpg"]}
{"brand": "GAP", "code": "gap-6704-2923414", "name": "Heritage Logo Zip Hoodie", "price": "223.25", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/gap-6704-2923414-1.jpg"]}
{"brand": "GAP", "code": "gap-1231-2925864", "name": "Arch Sherpa Zip Hoodie", "price": "175.14", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/gap-1231-2925864-1.jpg", "https://static-my.zacdn.com/p/gap-1231-2925864-2.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-6809-2319164", "name": "Zip Up Hoodie", "price": "119.40", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6809-2319164-1.jpg"]}
{"brand": "Cotton On", "code": "cotton-on-6979-5652664", "name": "Box Fit Hoodie", "price": "74.50", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/cotton-on-6979-5652664-1.jpg"]}
{"brand": "GAP", "code": "gap-1516-1025214", "name": "French Heritage Logo Zip Hoodie", "price": "153.23", "category": "hoodies", "imgs": ["https://static-my.zacdn.com/p/gap-1516-1025214-1.jpg"]}
+30
View File
@@ -0,0 +1,30 @@
{"brand":"G-Shock","code":"g-shock-9407-8856754","name":"DW-5600STT-1 Stranger Things Men Digital Watch Black Resin Band","price":"536.90","imgs":["https://static-my.zacdn.com/p/g-shock-9407-8856754-1.jpg","https://static-my.zacdn.com/p/g-shock-9407-8856754-2.jpg","https://static-my.zacdn.com/p/g-shock-9407-8856754-3.jpg","https://static-my.zacdn.com/p/g-shock-9407-8856754-4.jpg"]}
{"brand":"H&M","code":"hm-4512-4118154","name":"Relaxed Fit Printed T-shirt","price":"29.90","imgs":["https://static-my.zacdn.com/p/hm-4512-4118154-1.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-2.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-3.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-4.jpg","https://static-my.zacdn.com/p/hm-4512-4118154-5.jpg"]}
{"brand":"H&M","code":"hm-4434-6513734","name":"Regular Jeans","price":"99.90","imgs":["https://static-my.zacdn.com/p/hm-4434-6513734-1.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-2.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-3.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-4.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-5.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-6.jpg","https://static-my.zacdn.com/p/hm-4434-6513734-7.jpg"]}
{"brand":"Lacoste","code":"lacoste-2548-0143293","name":"Cotton T-shirt with Breast Pocket","price":"184.50","imgs":["https://static-my.zacdn.com/p/lacoste-2548-0143293-1.jpg","https://static-my.zacdn.com/p/lacoste-2548-0143293-2.jpg"]}
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-9447-9378554","name":"Heritage Logo Washed Baseball Cap - Tommy Jeans","price":"116.45","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-9447-9378554-1.jpg"]}
{"brand":"Cotton On","code":"cotton-on-4997-4515844","name":"Baggy Worker Pants","price":"114.45","imgs":["https://static-my.zacdn.com/p/cotton-on-4997-4515844-1.jpg"]}
{"brand":"GAP","code":"gap-0235-5300064","name":"Arch Logo T-Shirt","price":"60.95","imgs":["https://static-my.zacdn.com/p/gap-0235-5300064-1.jpg"]}
{"brand":"Giordano","code":"giordano-4368-3595764","name":"Men's Cotton Short Sleeve Print Tee 13096224","price":"28.03","imgs":["https://static-my.zacdn.com/p/giordano-4368-3595764-1.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-2.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-3.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-4.jpg","https://static-my.zacdn.com/p/giordano-4368-3595764-5.jpg"]}
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-1700-4474664","name":"Square Buckle Leather Belt - TH Mainline","price":"164.34","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-1700-4474664-1.jpg"]}
{"brand":"Superdry","code":"superdry-9309-2538964","name":"Vintage Script T-Shirt","price":"80.73","imgs":["https://static-my.zacdn.com/p/superdry-9309-2538964-1.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-2.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-3.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-4.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-5.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-6.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-7.jpg","https://static-my.zacdn.com/p/superdry-9309-2538964-8.jpg"]}
{"brand":"Under Armour","code":"under-armour-4667-9098804","name":"Tech™ Short Sleeve T-Shirt","price":"44.24","imgs":["https://static-my.zacdn.com/p/under-armour-4667-9098804-1.jpg"]}
{"brand":"Under Armour","code":"under-armour-8098-5270454","name":"Tech™ Polo Shirt","price":"114.56","imgs":["https://static-my.zacdn.com/p/under-armour-8098-5270454-1.jpg"]}
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-1774-1674664","name":"Six-Panel Baseball Cap - Th Mainline","price":"143.34","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-1774-1674664-1.jpg"]}
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-2343-0274664","name":"1985 Regular Polo Shirt - Th Mainline","price":"238.14","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-2343-0274664-1.jpg"]}
{"brand":"Cotton On","code":"cotton-on-1718-5169564","name":"Box Fit Text T-Shirt","price":"78.33","imgs":["https://static-my.zacdn.com/p/cotton-on-1718-5169564-1.jpg"]}
{"brand":"Giordano","code":"giordano-5996-5838764","name":"Men's Cotton Short Sleeve Print Tee 13096299","price":"28.03","imgs":["https://static-my.zacdn.com/p/giordano-5996-5838764-1.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-2.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-3.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-4.jpg","https://static-my.zacdn.com/p/giordano-5996-5838764-5.jpg"]}
{"brand":"GAP","code":"gap-7655-7195541","name":"Gap Original Arch Crew","price":"143.36","imgs":["https://static-my.zacdn.com/p/gap-7655-7195541-1.jpg"]}
{"brand":"Under Armour","code":"under-armour-3483-5826724","name":"Performance Tech™ Unisex 3-Pack Crew Socks","price":"52.43","imgs":["https://static-my.zacdn.com/p/under-armour-3483-5826724-1.jpg","https://static-my.zacdn.com/p/under-armour-3483-5826724-2.jpg","https://static-my.zacdn.com/p/under-armour-3483-5826724-3.jpg"]}
{"brand":"Electro Denim Lab","code":"electro-denim-lab-8086-0906954","name":"Chino Pants","price":"65.00","imgs":["https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-1.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-2.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-3.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-4.jpg","https://static-my.zacdn.com/p/electro-denim-lab-8086-0906954-5.jpg"]}
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-2147-5964664","name":"Flag Raglan Sleeves Relaxed Jersey T-Shirt - Th Mainline","price":"179.94","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-2147-5964664-1.jpg"]}
{"brand":"Cotton On","code":"cotton-on-6238-0809164","name":"3-Pack Essential Crew Socks","price":"33.05","imgs":["https://static-my.zacdn.com/p/cotton-on-6238-0809164-1.jpg"]}
{"brand":"Cotton On","code":"cotton-on-4057-8012493","name":"Everyday Fleece Shorts","price":"60.95","imgs":["https://static-my.zacdn.com/p/cotton-on-4057-8012493-1.jpg"]}
{"brand":"Cotton On","code":"cotton-on-6382-0063854","name":"Box Fit Graphic Hoodie","price":"142.03","imgs":["https://static-my.zacdn.com/p/cotton-on-6382-0063854-1.jpg"]}
{"brand":"Trendyol","code":"trendyol-3907-9299634","name":"Grimelanj Regular Fit Plaid Knitted Shorts Pajama Set TMNSS25PT00034","price":"79.03","imgs":["https://static-my.zacdn.com/p/trendyol-3907-9299634-1.jpg"]}
{"brand":"Camel Active","code":"camel-active-5848-1383954","name":"C BY CAMEL ACTIVE OUTDOOR HIKING TRAVEL BACKPACK - 51250081","price":"269.00","imgs":["https://static-my.zacdn.com/p/camel-active-5848-1383954-1.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-2.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-3.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-4.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-5.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-6.jpg","https://static-my.zacdn.com/p/camel-active-5848-1383954-7.jpg"]}
{"brand":"Lacoste","code":"lacoste-8093-9302554","name":"Cotton T-shirt","price":"258.30","imgs":["https://static-my.zacdn.com/p/lacoste-8093-9302554-1.jpg"]}
{"brand":"Under Armour","code":"under-armour-6462-2343264","name":"Under Armour UA Tech™ Vent Men's Short Sleeve","price":"83.44","imgs":["https://static-my.zacdn.com/p/under-armour-6462-2343264-1.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-2.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-3.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-4.jpg","https://static-my.zacdn.com/p/under-armour-6462-2343264-5.jpg"]}
{"brand":"Tommy Hilfiger","code":"tommy-hilfiger-1355-5874664","name":"Pool Slide Sandals - TH Mainline","price":"191.94","imgs":["https://static-my.zacdn.com/p/tommy-hilfiger-1355-5874664-1.jpg"]}
{"brand":"Calvin Klein","code":"calvin-klein-3281-1558554","name":"Low Rise Trunks 3 Pack - Calvin Klein Underwear","price":"180.45","imgs":["https://static-my.zacdn.com/p/calvin-klein-3281-1558554-1.jpg"]}
{"brand":"Swiss Polo","code":"swiss-polo-9699-7969744","name":"Men's Backpack - Black","price":"103.55","imgs":["https://static-my.zacdn.com/p/swiss-polo-9699-7969744-1.jpg","https://static-my.zacdn.com/p/swiss-polo-9699-7969744-2.jpg"]}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Replace product 129's square thumbnail with the reframed 762x1100 version.
The source was a 2400x2400 square. The storefront grid renders at aspect-3/4
with object-cover, so a 1:1 image loses 12.5% off each side — enough to cut the
heel and the toe off the shoe. Every other shoe in the catalogue is 762x1100
with the subject at ~93% width, so this reframes 129 to match its siblings.
Uploads the new asset at position 1, verifies it is really there, then removes
the square original. --apply is required; the default is a dry run.
"""
import argparse
import json
import pathlib
import sys
import urllib.error
import urllib.request
import uuid
import os as _os
def _spree_admin_key() -> str:
"""The Spree admin API key. Required — never defaulted."""
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
if not key:
raise SystemExit(
"SPREE_ADMIN_KEY is not set.\n"
" export SPREE_ADMIN_KEY=<the admin API key>\n"
"It is deliberately not stored in this file — see README.md."
)
return key
SPREE_API = "https://api.jux-lab.com/api/v3/admin"
SPREE_KEY = _spree_admin_key()
SLUG = "129-adidas-lifestyle-barreda-shoes-men-white-jp5928"
NEW_IMAGE = pathlib.Path(
"/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products/"
"129-adidas-lifestyle-barreda-shoes-men-white-jp5928/image-1-reframed.jpg"
)
def fatal(msg):
print(f"FATAL: {msg}", file=sys.stderr)
sys.exit(1)
def multipart(fields, file_field, filename, blob, content_type):
boundary = uuid.uuid4().hex
parts = []
for k, v in fields.items():
parts.append(
f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode()
)
parts.append(
f'--{boundary}\r\nContent-Disposition: form-data; name="{file_field}"; '
f'filename="{filename}"\r\nContent-Type: {content_type}\r\n\r\n'.encode()
+ blob
+ b"\r\n"
)
parts.append(f"--{boundary}--\r\n".encode())
return b"".join(parts), f"multipart/form-data; boundary={boundary}"
def api(path, method="GET", data=None, ctype=None):
headers = {"X-Spree-Api-Key": SPREE_KEY}
if ctype:
headers["Content-Type"] = ctype
req = urllib.request.Request(
f"{SPREE_API}{path}", data=data, headers=headers, method=method
)
try:
with urllib.request.urlopen(req, timeout=180) as r:
raw = r.read()
return r.status, (json.loads(raw) if raw else {})
except urllib.error.HTTPError as e:
fatal(f"{method} {path} -> HTTP {e.code}: {e.read().decode()[:400]}")
# This Spree build serves flat objects, not JSON:API — the fields are on the
# object itself, not under "attributes" — and it pages on `limit`, ignoring
# `per_page` entirely.
def find_product(slug):
page = 1
while True:
_, d = api(f"/products?limit=100&page={page}")
for p in d.get("data", []):
if p.get("slug") == slug:
return p
if page >= d.get("meta", {}).get("pages", 1):
return None
page += 1
def media_list(pid):
_, d = api(f"/products/{pid}/media?limit=100")
return d.get("data", [])
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true", help="actually write to Spree")
args = ap.parse_args()
if not NEW_IMAGE.exists():
fatal(f"{NEW_IMAGE} missing")
p = find_product(SLUG)
if not p:
fatal(f"product {SLUG} not found")
pid = p["id"]
print(f"product {pid} {p.get('name')}")
before = media_list(pid)
print(f"media before: {len(before)}")
for m in before:
print(f" id={m['id']} position={m.get('position')} {str(m.get('original_url',''))[:80]}")
if not args.apply:
print("\nDRY RUN — re-run with --apply to upload and remove the square original.")
return
old_ids = [m["id"] for m in before]
body, ctype = multipart(
{"type": "Spree::Image", "position": "1"},
"attachment",
NEW_IMAGE.name,
NEW_IMAGE.read_bytes(),
"image/jpeg",
)
status, resp = api(f"/products/{pid}/media", "POST", body, ctype)
new_id = (resp.get("data") or resp).get("id")
print(f"\nupload -> HTTP {status}, new asset id={new_id}")
if status == 202 or not new_id:
fatal("Spree answered without a serialized asset — cannot confirm the image landed")
# Re-read rather than trusting the create response: media#create has a path
# that enqueues a background job and answers 202 with no body.
after = media_list(pid)
if new_id not in [m["id"] for m in after]:
fatal(f"asset {new_id} is not in the product media after upload")
print(f"verified: asset {new_id} present, {len(after)} media on product")
for oid in old_ids:
st, _ = api(f"/products/{pid}/media/{oid}", "DELETE")
print(f"removed old asset {oid} -> HTTP {st}")
final = media_list(pid)
print(f"\nmedia after: {len(final)}")
for m in final:
print(f" id={m['id']} position={m.get('position')} {str(m.get('original_url',''))[:80]}")
if __name__ == "__main__":
main()
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Reprice all products from the source MYR catalogue.
Replaces the prices written by fix_prices.py, which multiplied by 4.8 instead of
dividing — putting every product on the store at ~23x its real price.
Writes BOTH currency rows the store supports:
ZAR (store default) = price_myr * MYR_TO_ZAR
USD (still in supported_currencies) = price_myr * MYR_TO_USD
The USD row is corrected in the same pass on purpose: leaving the old 23x rows
behind means anyone switching the store currency to USD sees the broken prices
again, with nothing to warn them.
Rates are fetched live and printed. No hardcoded rate, no fallback rate — if the
fetch fails this exits rather than pricing a live store off a guess.
Usage:
reprice_zar.py --dry-run # show the changes, write nothing (do this first)
reprice_zar.py # apply, then read back and check every row
"""
import json
import sys
import time
from pathlib import Path
import requests
import os as _os
def _spree_admin_key() -> str:
"""The Spree admin API key. Required — never defaulted."""
key = _os.environ.get("SPREE_ADMIN_KEY", "").strip()
if not key:
raise SystemExit(
"SPREE_ADMIN_KEY is not set.\n"
" export SPREE_ADMIN_KEY=<the admin API key>\n"
"It is deliberately not stored in this file — see README.md."
)
return key
API = "https://api.jux-lab.com/api/v3/admin"
H = {"X-Spree-Api-Key": _spree_admin_key(), "Content-Type": "application/json"}
PRODUCTS = Path("/root/Opus/Projects/PeakyBuildr/scraped-catalogue/products")
DRY = "--dry-run" in sys.argv
def fatal(msg):
print(f"\nFATAL: {msg}", file=sys.stderr)
sys.exit(1)
# --- rates -------------------------------------------------------------------
def get_rates():
"""Live MYR base rates. Loud failure beats a stale constant — see fix_prices.py."""
try:
d = requests.get("https://open.er-api.com/v6/latest/MYR", timeout=20).json()
except Exception as e:
fatal(f"rate fetch failed: {e}")
r = d.get("rates") or {}
zar, usd = r.get("ZAR"), r.get("USD")
if not zar or not usd:
fatal(f"rate response missing ZAR/USD: {d}")
print(f"rates 1 MYR = {zar} ZAR = {usd} USD ({d.get('time_last_update_utc')})")
return float(zar), float(usd)
# --- source catalogue --------------------------------------------------------
def load_source():
prices = {}
for folder in sorted(PRODUCTS.iterdir()):
info = folder / "info.json"
if not info.exists():
continue
myr = json.loads(info.read_text()).get("price_myr")
if myr is None:
fatal(f"{folder.name}/info.json has no price_myr")
prices[folder.name] = float(myr)
if not prices:
fatal(f"no source prices found under {PRODUCTS}")
print(f"source {len(prices)} products, MYR {min(prices.values()):.2f}-{max(prices.values()):.2f}")
return prices
def fetch_products():
items, page = [], 1
while True:
d = requests.get(f"{API}/products?per_page=100&page={page}", headers=H, timeout=60).json()
items += d.get("data", [])
if page >= d.get("meta", {}).get("pages", 1):
break
page += 1
return items
def main():
zar_rate, usd_rate = get_rates()
src = load_source()
items = fetch_products()
print(f"store {len(items)} products")
planned, missing = {}, []
for p in items:
slug = p["slug"]
myr = src.get(slug)
if myr is None:
missing.append(slug)
continue
planned[p["id"]] = {
"slug": slug,
"myr": myr,
"ZAR": round(myr * zar_rate, 2),
"USD": round(myr * usd_rate, 2),
}
if missing:
# Not fatal: these keep their current prices. But they must be named, not
# silently skipped — a product left at 23x is the whole bug again.
print(f"\n{len(missing)} products have NO source price and will be LEFT UNCHANGED:")
for s in missing:
print(f" ! {s}")
sample = list(planned.values())[:5]
print(f"\n{len(planned)} to reprice. Sample:")
for s in sample:
print(f" MYR {s['myr']:>7.2f} -> ZAR {s['ZAR']:>8.2f} / USD {s['USD']:>7.2f} {s['slug'][:44]}")
zars = [v["ZAR"] for v in planned.values()]
print(f" new ZAR range: {min(zars):.2f} - {max(zars):.2f}")
if DRY:
print("\n--dry-run: nothing written.")
return
# --- apply ---------------------------------------------------------------
print()
ok = fail = 0
for n, (pid, s) in enumerate(planned.items(), 1):
body = {"prices": [
{"amount": s["ZAR"], "currency": "ZAR"},
{"amount": s["USD"], "currency": "USD"},
]}
try:
r = requests.patch(f"{API}/products/{pid}", headers=H, json=body, timeout=60).json()
except Exception as e:
fail += 1
print(f" ERROR {s['slug'][:40]}: {e}")
continue
if "error" in r:
fail += 1
print(f" FAIL {s['slug'][:40]}: {r['error'].get('message')}")
else:
ok += 1
if n % 40 == 0:
print(f" {n}/{len(planned)}")
time.sleep(0.12)
print(f"patched ok={ok} fail={fail}")
# --- read back -----------------------------------------------------------
# The store returns 200 for a price write that never created a row in the
# store currency. Trusting that 200 is what hid 130 products for an hour.
print("\nreading back...")
time.sleep(2)
bad = []
for p in fetch_products():
want = planned.get(p["id"])
if not want:
continue
a = p.get("attributes", p)
got = a.get("price")
cur = a.get("currency")
if got is None or abs(float(got) - want["ZAR"]) > 0.01 or cur != "ZAR":
bad.append((want["slug"], want["ZAR"], got, cur))
if bad:
print(f"\n{len(bad)} products did NOT take the ZAR price:")
for slug, want, got, cur in bad[:15]:
print(f" ! {slug[:40]} want ZAR {want} got {got} {cur}")
sys.exit(1)
print(f"check ok: all {len(planned)} products report the expected ZAR price.")
if __name__ == "__main__":
main()