← back to Wallco Ai
refresh data/designs.json snapshot — 30 designs (was 11): 8 damask · 9 floral · 8 geometric · 5 mixed; semantic title generator by category+hue
a6af87133ea70e41b759831a669f38dadc291908 · 2026-05-11 16:52:05 -0700 · Steve
Files touched
M data/designs.jsonA scripts/launch_spoon_chrome.shM scripts/pull_spoonflower_account.pyA scripts/refresh_designs_snapshot.pyM scripts/spoonflower_lib.py
Diff
commit a6af87133ea70e41b759831a669f38dadc291908
Author: Steve <steve@designerwallcoverings.com>
Date: Mon May 11 16:52:05 2026 -0700
refresh data/designs.json snapshot — 30 designs (was 11): 8 damask · 9 floral · 8 geometric · 5 mixed; semantic title generator by category+hue
---
data/designs.json | 373 +++++++++++++++++++++++++++++++-----
scripts/launch_spoon_chrome.sh | 46 +++++
scripts/pull_spoonflower_account.py | 142 +++++++++-----
scripts/refresh_designs_snapshot.py | 86 +++++++++
scripts/spoonflower_lib.py | 117 ++++++++++-
5 files changed, 664 insertions(+), 100 deletions(-)
diff --git a/data/designs.json b/data/designs.json
index 385a35d..3f63e9b 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -4,164 +4,449 @@
"kind": "seamless_tile",
"category": "floral",
"dominant_hex": "#9c9172",
- "saturation": 0.269,
- "title": "Amber Botanical No.11",
+ "saturation": 0.175,
+ "title": "Honey Bouquet No.11",
"handle": "wallco-0011",
"image_url": "/designs/img/1778541019744_893789819.png",
"filename": "1778541019744_893789819.png",
"generator": "Replicate SDXL",
"seed": 893789819,
- "is_published": true,
- "created_at": "2026-05-11T23:10:00Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:10:27.199721+00:00"
},
{
"id": 12,
"kind": "seamless_tile",
"category": "floral",
"dominant_hex": "#a09279",
- "saturation": 0.244,
- "title": "Amber Botanical No.12",
+ "saturation": 0.17,
+ "title": "Honey Blossom No.12",
"handle": "wallco-0012",
"image_url": "/designs/img/1778541051623_1771215304.png",
"filename": "1778541051623_1771215304.png",
"generator": "Replicate SDXL",
"seed": 1771215304,
- "is_published": true,
- "created_at": "2026-05-11T23:10:15Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:10:58.996561+00:00"
},
{
"id": 13,
"kind": "seamless_tile",
"category": "floral",
"dominant_hex": "#87774c",
- "saturation": 0.437,
- "title": "Umber Botanical No.13",
+ "saturation": 0.28,
+ "title": "Honey Garden No.13",
"handle": "wallco-0013",
"image_url": "/designs/img/1778541058999_209403190.png",
"filename": "1778541058999_209403190.png",
"generator": "Replicate SDXL",
"seed": 209403190,
- "is_published": true,
- "created_at": "2026-05-11T23:10:30Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:11:06.301179+00:00"
},
{
"id": 14,
"kind": "seamless_tile",
"category": "floral",
"dominant_hex": "#0c162b",
- "saturation": 0.721,
- "title": "Noir Botanical No.14",
+ "saturation": 0.564,
+ "title": "Sapphire Floret No.14",
"handle": "wallco-0014",
"image_url": "/designs/img/1778541066304_1186496331.png",
"filename": "1778541066304_1186496331.png",
"generator": "Replicate SDXL",
"seed": 1186496331,
- "is_published": true,
- "created_at": "2026-05-11T23:10:45Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:11:13.611884+00:00"
},
{
"id": 15,
"kind": "seamless_tile",
"category": "floral",
"dominant_hex": "#b49d95",
- "saturation": 0.172,
- "title": "Sienna Botanical No.15",
+ "saturation": 0.171,
+ "title": "Amber Botanical No.15",
"handle": "wallco-0015",
"image_url": "/designs/img/1778541073614_864976504.png",
"filename": "1778541073614_864976504.png",
"generator": "Replicate SDXL",
"seed": 864976504,
- "is_published": true,
- "created_at": "2026-05-11T23:11:00Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:11:21.243547+00:00"
},
{
"id": 16,
"kind": "seamless_tile",
"category": "geometric",
"dominant_hex": "#283527",
- "saturation": 0.264,
- "title": "Sage Lattice No.16",
+ "saturation": 0.152,
+ "title": "Sage Tessellation No.16",
"handle": "wallco-0016",
"image_url": "/designs/img/1778541111658_918473109.png",
"filename": "1778541111658_918473109.png",
"generator": "Replicate SDXL",
"seed": 918473109,
- "is_published": true,
- "created_at": "2026-05-11T23:11:15Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:11:58.987722+00:00"
},
{
"id": 17,
"kind": "seamless_tile",
"category": "geometric",
"dominant_hex": "#f0e8db",
- "saturation": 0.088,
- "title": "Stone Lattice No.17",
+ "saturation": 0.412,
+ "title": "Honey Geometry No.17",
"handle": "wallco-0017",
"image_url": "/designs/img/1778541118993_368707680.png",
"filename": "1778541118993_368707680.png",
"generator": "Replicate SDXL",
"seed": 368707680,
- "is_published": true,
- "created_at": "2026-05-11T23:11:30Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:12:06.628664+00:00"
},
{
"id": 18,
"kind": "seamless_tile",
"category": "geometric",
"dominant_hex": "#bb7d4e",
- "saturation": 0.583,
- "title": "Amber Lattice No.18",
+ "saturation": 0.445,
+ "title": "Amber Meridian No.18",
"handle": "wallco-0018",
"image_url": "/designs/img/1778541126631_1206353655.png",
"filename": "1778541126631_1206353655.png",
"generator": "Replicate SDXL",
"seed": 1206353655,
- "is_published": true,
- "created_at": "2026-05-11T23:11:45Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:12:14.021917+00:00"
},
{
"id": 19,
"kind": "seamless_tile",
"category": "damask",
"dominant_hex": "#bfab91",
- "saturation": 0.241,
- "title": "Amber Baroque No.19",
+ "saturation": 0.264,
+ "title": "Amber Salon No.19",
"handle": "wallco-0019",
"image_url": "/designs/img/1778541171660_1800259551.png",
"filename": "1778541171660_1800259551.png",
"generator": "Replicate SDXL",
"seed": 1800259551,
- "is_published": true,
- "created_at": "2026-05-11T23:12:00Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:12:59.057562+00:00"
},
{
"id": 20,
"kind": "seamless_tile",
"category": "damask",
"dominant_hex": "#787675",
- "saturation": 0.025,
- "title": "Graphite Baroque No.20",
+ "saturation": 0.013,
+ "title": "Slate Baroque No.20",
"handle": "wallco-0020",
"image_url": "/designs/img/1778541179060_1139384775.png",
"filename": "1778541179060_1139384775.png",
"generator": "Replicate SDXL",
"seed": 1139384775,
- "is_published": true,
- "created_at": "2026-05-11T23:12:15Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:13:06.407821+00:00"
},
{
"id": 21,
"kind": "seamless_tile",
"category": "damask",
"dominant_hex": "#cba15c",
- "saturation": 0.547,
- "title": "Amber Baroque No.21",
+ "saturation": 0.516,
+ "title": "Honey Damask No.21",
"handle": "wallco-0021",
"image_url": "/designs/img/1778541186410_1284647138.png",
"filename": "1778541186410_1284647138.png",
"generator": "Replicate SDXL",
"seed": 1284647138,
- "is_published": true,
- "created_at": "2026-05-11T23:12:30Z"
+ "is_published": false,
+ "created_at": "2026-05-11T23:13:13.762466+00:00"
+ },
+ {
+ "id": 22,
+ "kind": "seamless_tile",
+ "category": "floral",
+ "dominant_hex": "#a29671",
+ "saturation": 0.209,
+ "title": "Honey Blossom No.22",
+ "handle": "wallco-0022",
+ "image_url": "/designs/img/1778543135179_1552462120.png",
+ "filename": "1778543135179_1552462120.png",
+ "generator": "Replicate SDXL",
+ "seed": 1552462120,
+ "is_published": false,
+ "created_at": "2026-05-11T23:45:45.997753+00:00"
+ },
+ {
+ "id": 23,
+ "kind": "seamless_tile",
+ "category": "floral",
+ "dominant_hex": "#6e6b4e",
+ "saturation": 0.17,
+ "title": "Honey Garden No.23",
+ "handle": "wallco-0023",
+ "image_url": "/designs/img/1778543146002_1550235083.png",
+ "filename": "1778543146002_1550235083.png",
+ "generator": "Replicate SDXL",
+ "seed": 1550235083,
+ "is_published": false,
+ "created_at": "2026-05-11T23:45:53.316274+00:00"
+ },
+ {
+ "id": 24,
+ "kind": "seamless_tile",
+ "category": "floral",
+ "dominant_hex": "#0b1730",
+ "saturation": 0.627,
+ "title": "Sapphire Floret No.24",
+ "handle": "wallco-0024",
+ "image_url": "/designs/img/1778543153319_465538454.png",
+ "filename": "1778543153319_465538454.png",
+ "generator": "Replicate SDXL",
+ "seed": 465538454,
+ "is_published": false,
+ "created_at": "2026-05-11T23:46:04.278548+00:00"
+ },
+ {
+ "id": 25,
+ "kind": "seamless_tile",
+ "category": "floral",
+ "dominant_hex": "#a9a081",
+ "saturation": 0.189,
+ "title": "Honey Botanical No.25",
+ "handle": "wallco-0025",
+ "image_url": "/designs/img/1778543171203_1496299154.png",
+ "filename": "1778543171203_1496299154.png",
+ "generator": "Replicate SDXL",
+ "seed": 1496299154,
+ "is_published": false,
+ "created_at": "2026-05-11T23:46:28.626696+00:00"
+ },
+ {
+ "id": 26,
+ "kind": "seamless_tile",
+ "category": "geometric",
+ "dominant_hex": "#282a1e",
+ "saturation": 0.167,
+ "title": "Olive Tessellation No.26",
+ "handle": "wallco-0026",
+ "image_url": "/designs/img/1778543188661_2139138353.png",
+ "filename": "1778543188661_2139138353.png",
+ "generator": "Replicate SDXL",
+ "seed": 2139138353,
+ "is_published": false,
+ "created_at": "2026-05-11T23:46:39.0279+00:00"
+ },
+ {
+ "id": 27,
+ "kind": "seamless_tile",
+ "category": "geometric",
+ "dominant_hex": "#f1ebdb",
+ "saturation": 0.44,
+ "title": "Honey Geometry No.27",
+ "handle": "wallco-0027",
+ "image_url": "/designs/img/1778543199030_1938744951.png",
+ "filename": "1778543199030_1938744951.png",
+ "generator": "Replicate SDXL",
+ "seed": 1938744951,
+ "is_published": false,
+ "created_at": "2026-05-11T23:46:52.649025+00:00"
+ },
+ {
+ "id": 28,
+ "kind": "seamless_tile",
+ "category": "geometric",
+ "dominant_hex": "#9f6237",
+ "saturation": 0.486,
+ "title": "Amber Meridian No.28",
+ "handle": "wallco-0028",
+ "image_url": "/designs/img/1778543212651_993138679.png",
+ "filename": "1778543212651_993138679.png",
+ "generator": "Replicate SDXL",
+ "seed": 993138679,
+ "is_published": false,
+ "created_at": "2026-05-11T23:46:59.864585+00:00"
+ },
+ {
+ "id": 29,
+ "kind": "seamless_tile",
+ "category": "geometric",
+ "dominant_hex": "#0b1631",
+ "saturation": 0.633,
+ "title": "Sapphire Fan No.29",
+ "handle": "wallco-0029",
+ "image_url": "/designs/img/1778543219867_428383571.png",
+ "filename": "1778543219867_428383571.png",
+ "generator": "Replicate SDXL",
+ "seed": 428383571,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:07.394284+00:00"
+ },
+ {
+ "id": 30,
+ "kind": "seamless_tile",
+ "category": "geometric",
+ "dominant_hex": "#070707",
+ "saturation": 0.0,
+ "title": "Noir Lattice No.30",
+ "handle": "wallco-0030",
+ "image_url": "/designs/img/1778543227397_1802417126.png",
+ "filename": "1778543227397_1802417126.png",
+ "generator": "Replicate SDXL",
+ "seed": 1802417126,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:14.919596+00:00"
+ },
+ {
+ "id": 31,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#8f7d69",
+ "saturation": 0.153,
+ "title": "Amber Damask No.31",
+ "handle": "wallco-0031",
+ "image_url": "/designs/img/1778543234953_386152567.png",
+ "filename": "1778543234953_386152567.png",
+ "generator": "Replicate SDXL",
+ "seed": 386152567,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:22.285707+00:00"
+ },
+ {
+ "id": 32,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#4b4c4d",
+ "saturation": 0.013,
+ "title": "Slate Heirloom No.32",
+ "handle": "wallco-0032",
+ "image_url": "/designs/img/1778543242288_1871092499.png",
+ "filename": "1778543242288_1871092499.png",
+ "generator": "Replicate SDXL",
+ "seed": 1871092499,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:29.881134+00:00"
+ },
+ {
+ "id": 33,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#394155",
+ "saturation": 0.197,
+ "title": "Sapphire Atelier No.33",
+ "handle": "wallco-0033",
+ "image_url": "/designs/img/1778543249885_57994025.png",
+ "filename": "1778543249885_57994025.png",
+ "generator": "Replicate SDXL",
+ "seed": 57994025,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:40.331546+00:00"
+ },
+ {
+ "id": 34,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#d2c0a6",
+ "saturation": 0.328,
+ "title": "Honey Salon No.34",
+ "handle": "wallco-0034",
+ "image_url": "/designs/img/1778543260334_151035740.png",
+ "filename": "1778543260334_151035740.png",
+ "generator": "Replicate SDXL",
+ "seed": 151035740,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:47.588998+00:00"
+ },
+ {
+ "id": 35,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#938e89",
+ "saturation": 0.044,
+ "title": "Ash Baroque No.35",
+ "handle": "wallco-0035",
+ "image_url": "/designs/img/1778543267591_388669124.png",
+ "filename": "1778543267591_388669124.png",
+ "generator": "Replicate SDXL",
+ "seed": 388669124,
+ "is_published": false,
+ "created_at": "2026-05-11T23:47:54.907538+00:00"
+ },
+ {
+ "id": 36,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#8f8b61",
+ "saturation": 0.192,
+ "title": "Honey Studio No.36",
+ "handle": "wallco-0036",
+ "image_url": "/designs/img/1778543274946_268939687.png",
+ "filename": "1778543274946_268939687.png",
+ "generator": "Replicate SDXL",
+ "seed": 268939687,
+ "is_published": false,
+ "created_at": "2026-05-11T23:48:12.213123+00:00"
+ },
+ {
+ "id": 37,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#544f55",
+ "saturation": 0.037,
+ "title": "Slate Origin No.37",
+ "handle": "wallco-0037",
+ "image_url": "/designs/img/1778543292217_879781667.png",
+ "filename": "1778543292217_879781667.png",
+ "generator": "Replicate SDXL",
+ "seed": 879781667,
+ "is_published": false,
+ "created_at": "2026-05-11T23:48:23.023987+00:00"
+ },
+ {
+ "id": 38,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#483a31",
+ "saturation": 0.19,
+ "title": "Amber Folio No.38",
+ "handle": "wallco-0038",
+ "image_url": "/designs/img/1778543303027_965292828.png",
+ "filename": "1778543303027_965292828.png",
+ "generator": "Replicate SDXL",
+ "seed": 965292828,
+ "is_published": false,
+ "created_at": "2026-05-11T23:48:30.32711+00:00"
+ },
+ {
+ "id": 39,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#b3ad84",
+ "saturation": 0.236,
+ "title": "Honey Reverie No.39",
+ "handle": "wallco-0039",
+ "image_url": "/designs/img/1778543310329_821956426.png",
+ "filename": "1778543310329_821956426.png",
+ "generator": "Replicate SDXL",
+ "seed": 821956426,
+ "is_published": false,
+ "created_at": "2026-05-11T23:48:40.995703+00:00"
+ },
+ {
+ "id": 40,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#756263",
+ "saturation": 0.088,
+ "title": "Rose Atelier No.40",
+ "handle": "wallco-0040",
+ "image_url": "/designs/img/1778543320998_1617564843.png",
+ "filename": "1778543320998_1617564843.png",
+ "generator": "Replicate SDXL",
+ "seed": 1617564843,
+ "is_published": false,
+ "created_at": "2026-05-11T23:48:57.994123+00:00"
}
]
\ No newline at end of file
diff --git a/scripts/launch_spoon_chrome.sh b/scripts/launch_spoon_chrome.sh
new file mode 100755
index 0000000..f29192f
--- /dev/null
+++ b/scripts/launch_spoon_chrome.sh
@@ -0,0 +1,46 @@
+#!/bin/zsh
+# Launch a SECOND Chrome instance with remote debugging enabled,
+# using its own user-data-dir so it doesn't disturb Steve's main Chrome.
+#
+# Sign in to Spoonflower once in that window — then the pull scripts
+# auto-attach via http://127.0.0.1:9222 (Path 1 in spoonflower_lib.py).
+#
+# This Chrome stays open in the background — kill it when done with:
+# pkill -f /tmp/spoon-chrome (or just close the window)
+
+set -u
+PROFILE_DIR="/tmp/spoon-chrome"
+DEBUG_PORT=9222
+CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+
+if [[ ! -x "$CHROME" ]]; then
+ echo "❌ Google Chrome not found at $CHROME" >&2
+ exit 1
+fi
+
+# Already running?
+if curl -fsS "http://127.0.0.1:${DEBUG_PORT}/json/version" --max-time 1 >/dev/null 2>&1; then
+ echo "✓ Debug Chrome already running on :${DEBUG_PORT}"
+ echo " → If you're already signed in to Spoonflower in that window, you're good."
+ echo " → Otherwise open it (look for the window using profile ${PROFILE_DIR})"
+ exit 0
+fi
+
+mkdir -p "$PROFILE_DIR"
+
+echo "Launching debug Chrome on :${DEBUG_PORT} (profile: ${PROFILE_DIR})…"
+nohup "$CHROME" \
+ --remote-debugging-port=${DEBUG_PORT} \
+ --user-data-dir="$PROFILE_DIR" \
+ --no-first-run \
+ --no-default-browser-check \
+ "https://www.spoonflower.com/login" \
+ >/tmp/spoon-chrome.log 2>&1 &
+
+echo ""
+echo "▷ A new Chrome window should open at the Spoonflower login page."
+echo "▷ Sign in once with: info@designerwallcoverings.com / *Spoonaccess911*"
+echo "▷ When you see 'My Spoonflower' top-right, you're good — leave the window open."
+echo "▷ Then re-run the pull (or click 'Pull ALL DW designs' in /spoonflower)."
+echo ""
+echo "When done, close the Chrome window or: pkill -f ${PROFILE_DIR}"
diff --git a/scripts/pull_spoonflower_account.py b/scripts/pull_spoonflower_account.py
index c9e2e44..7aae136 100644
--- a/scripts/pull_spoonflower_account.py
+++ b/scripts/pull_spoonflower_account.py
@@ -14,7 +14,10 @@ from pathlib import Path
from playwright.sync_api import sync_playwright
sys.path.insert(0, str(Path(__file__).parent))
-from spoonflower_lib import creds, login, emit # noqa: E402
+from spoonflower_lib import ( # noqa: E402
+ creds, login, emit, try_attach_running_chrome,
+ try_load_saved_cookies, verify_signed_in,
+)
DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "spoonflower-pulled"
DATA_DIR.mkdir(parents=True, exist_ok=True)
@@ -38,37 +41,31 @@ def sql_str(v):
return "'" + str(v).replace("'", "''") + "'"
-def diagnose_account(page) -> dict:
- """Probe Spoonflower stats endpoints to confirm account has uploads.
- Spoonflower exposes get-user-stats publicly; if userID is null the
- session cookie isn't being honored (or account is empty)."""
- try:
- page.goto("https://cart.spoonflower.com/api/spoonflower/get-user-stats",
- wait_until="domcontentloaded", timeout=20000)
- import json as _j
- body = page.evaluate("() => document.body.innerText")
- info = _j.loads(body) if body else {}
- return info.get("data_layer", {}) or {}
- except Exception as e:
- return {"_error": str(e)}
-
-
def list_my_designs(page, limit: int = 0):
"""Navigate the user's 'my designs' page and harvest design URLs.
Strategy:
1. Probe get-user-stats — flag clearly if userID null / designForSale false.
- 2. Visit the SPA at /en/my-spoonflower/designs, wait 12s for hydration,
- scroll, then harvest links matching /design/N-slug OR /fabric/N OR /wallpaper/N.
+ 2. Try /artists/designs/<id> style URLs first (proven pattern from
+ 150dpi/spoonflower_download.js), THEN /en/my-spoonflower/designs SPA.
+ 3. Harvest links matching /artists/designs/<id> OR /design/N-slug
+ OR /fabric/N OR /wallpaper/N.
"""
- stats = diagnose_account(page)
+ stats = verify_signed_in(page)
emit("account_stats", **stats)
+ # Use account username if we have it from stats
+ username = stats.get("userID") or os.getenv("SPOONFLOWER_USERNAME") or ""
candidates = [
+ # Proven artist-profile pattern (months-ago working scraper)
+ f"https://www.spoonflower.com/profiles/{username}/designs" if username else None,
+ f"https://www.spoonflower.com/artists/{username}/designs" if username else None,
+ # SPA fallback
"https://www.spoonflower.com/en/my-spoonflower/designs",
"https://www.spoonflower.com/profiles/designs",
"https://www.spoonflower.com/en/profile/designs",
]
+ candidates = [c for c in candidates if c]
landed = None
for u in candidates:
try:
@@ -104,25 +101,30 @@ def list_my_designs(page, limit: int = 0):
designs = []
seen = set()
for href in anchors:
- m = re.search(r"/(?:en/)?design/(\d+)-([a-z0-9\-]+)", href)
+ did = None
+ slug = ""
+ # Pattern A: /artists/designs/<id> (proven pattern from old scraper)
+ m = re.search(r"/artists/designs/(\d+)", href)
if m:
- did, slug = m.group(1), m.group(2)
- if did in seen:
- continue
- seen.add(did)
- designs.append({"sf_design_id": did, "slug": slug, "design_url": href})
- else:
- # Fallback: /fabric/12345 or /wallpaper/12345 — derive a design URL
- m2 = re.search(r"/(?:en/)?(?:fabric|wallpaper)/(\d+)", href)
- if m2:
- did = m2.group(1)
- if did in seen:
- continue
- seen.add(did)
- designs.append({
- "sf_design_id": did, "slug": "",
- "design_url": f"https://www.spoonflower.com/en/design/{did}-"
- })
+ did = m.group(1)
+ if not did:
+ # Pattern B: /en/design/<id>-<slug>
+ m = re.search(r"/(?:en/)?design/(\d+)-([a-z0-9\-]+)", href)
+ if m:
+ did, slug = m.group(1), m.group(2)
+ if not did:
+ # Pattern C: /fabric/<id> or /wallpaper/<id>
+ m = re.search(r"/(?:en/)?(?:fabric|wallpaper)/(\d+)", href)
+ if m:
+ did = m.group(1)
+ if not did or did in seen:
+ continue
+ seen.add(did)
+ # Prefer the artists URL since that's where File Options shows
+ designs.append({
+ "sf_design_id": did, "slug": slug,
+ "design_url": f"https://www.spoonflower.com/artists/designs/{did}"
+ })
if limit and len(designs) >= limit:
break
@@ -244,23 +246,63 @@ def main():
ap.add_argument("--limit", type=int, default=0,
help="cap number of designs (0 = unlimited)")
ap.add_argument("--account", default="dw", help="tag for the account")
+ ap.add_argument("--auth", choices=["auto","chrome","cookies","login"],
+ default="auto", help="auth strategy (default: auto cascades through all 3)")
args = ap.parse_args()
- email, password = creds()
- emit("start", account=args.account, limit=args.limit)
+ emit("start", account=args.account, limit=args.limit, auth=args.auth)
with sync_playwright() as p:
- browser = p.chromium.launch(headless=True)
- ctx = browser.new_context(
- viewport={"width": 1600, "height": 1000},
- accept_downloads=True,
- )
- page = ctx.new_page()
- if not login(page, email, password):
- emit("done", ok=False, error="login failed")
- browser.close()
- return 2
- emit("login_ok")
+ browser = None
+ ctx = None
+ page = None
+ used_auth = None
+
+ # Strategy 1: attach to running Chrome on :9222
+ if args.auth in ("auto", "chrome"):
+ browser, ctx, page = try_attach_running_chrome(p)
+ if browser:
+ used_auth = "chrome-attach"
+ emit("auth", method=used_auth)
+
+ # Strategy 2: load saved cookie jar
+ if not browser and args.auth in ("auto", "cookies"):
+ browser = p.chromium.launch(headless=True)
+ ctx = browser.new_context(
+ viewport={"width": 1600, "height": 1000},
+ accept_downloads=True,
+ )
+ if try_load_saved_cookies(ctx):
+ page = ctx.new_page()
+ used_auth = "saved-cookies"
+ emit("auth", method=used_auth)
+ else:
+ browser.close()
+ browser = None
+
+ # Strategy 3: programmatic email/password login (fragile)
+ if not browser and args.auth in ("auto", "login"):
+ email, password = creds()
+ browser = p.chromium.launch(headless=True)
+ ctx = browser.new_context(
+ viewport={"width": 1600, "height": 1000},
+ accept_downloads=True,
+ )
+ page = ctx.new_page()
+ if not login(page, email, password):
+ emit("done", ok=False, error="login failed (strategy=login)")
+ browser.close()
+ return 2
+ used_auth = "password-login"
+ emit("auth", method=used_auth)
+
+ if not browser:
+ emit("done", ok=False,
+ error=("no working auth path: Chrome :9222 not running, "
+ f"{os.path.expanduser('~/.spoonflower-cookies.json')} missing, "
+ "and --auth=cookies/chrome forced"))
+ return 4
+ emit("login_ok", method=used_auth)
designs = list_my_designs(page, limit=args.limit)
if not designs:
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
new file mode 100644
index 0000000..236263f
--- /dev/null
+++ b/scripts/refresh_designs_snapshot.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""Refresh data/designs.json from dw_unified.spoon_all_designs.
+
+Keeps the existing schema (id, kind, category, dominant_hex, saturation,
+title, handle, image_url, filename, generator, seed, is_published, created_at).
+
+Generates semantic titles by category palette:
+ floral → Botanical / Bouquet / Blossom + hue-name + No.{id}
+ geometric → Lattice / Tessellation / Geometry + hue-name + No.{id}
+ damask → Baroque / Damask / Heirloom + hue-name + No.{id}
+ mixed → Atelier / Studio / Origin + hue-name + No.{id}
+"""
+import json, subprocess, colorsys, os
+from pathlib import Path
+
+ROOT = Path.home() / 'Projects' / 'wallco-ai'
+OUT = ROOT / 'data' / 'designs.json'
+
+def psql_json(sql):
+ out = subprocess.run(['psql','dw_unified','-At','-q','-c',sql], capture_output=True, text=True, check=True).stdout.strip()
+ return json.loads(out) if out else []
+
+def hex_to_hls(hex_):
+ if not hex_ or not hex_.startswith('#'): return (0,0.5,0)
+ h = hex_.lstrip('#')
+ r = int(h[0:2],16)/255; g = int(h[2:4],16)/255; b = int(h[4:6],16)/255
+ return colorsys.rgb_to_hls(r,g,b)
+
+def hue_name(hex_):
+ h, l, s = hex_to_hls(hex_)
+ deg = h * 360
+ if s < 0.08: return 'Ivory' if l > 0.8 else 'Ash' if l > 0.5 else 'Slate' if l > 0.25 else 'Noir'
+ if deg < 15: return 'Rose'
+ if deg < 35: return 'Amber'
+ if deg < 55: return 'Honey'
+ if deg < 80: return 'Olive'
+ if deg < 150: return 'Sage'
+ if deg < 195: return 'Marine'
+ if deg < 250: return 'Sapphire'
+ if deg < 290: return 'Mauve'
+ if deg < 330: return 'Plum'
+ return 'Rose'
+
+NOUNS = {
+ 'floral': ['Botanical', 'Bouquet', 'Blossom', 'Garden', 'Floret'],
+ 'geometric': ['Lattice', 'Tessellation', 'Geometry', 'Meridian', 'Fan'],
+ 'damask': ['Baroque', 'Damask', 'Heirloom', 'Atelier', 'Salon'],
+ 'mixed': ['Atelier', 'Studio', 'Origin', 'Folio', 'Reverie']
+}
+def title_for(cat, hex_, id_):
+ pool = NOUNS.get(cat, NOUNS['mixed'])
+ hue = hue_name(hex_)
+ noun = pool[id_ % len(pool)]
+ return f"{hue} {noun} No.{id_}"
+
+rows = psql_json(
+ "SELECT COALESCE(json_agg(t),'[]'::json) FROM ("
+ "SELECT id, kind, category, dominant_hex, prompt, generator, seed, local_path, "
+ "is_published, created_at "
+ "FROM spoon_all_designs WHERE generator='replicate' ORDER BY id) t;"
+)
+
+out = []
+for r in rows:
+ h, l, s = hex_to_hls(r['dominant_hex'] or '')
+ filename = os.path.basename(r['local_path'] or '')
+ out.append({
+ "id": r['id'],
+ "kind": r['kind'],
+ "category": r['category'],
+ "dominant_hex": r['dominant_hex'],
+ "saturation": round(s, 3),
+ "title": title_for(r['category'], r['dominant_hex'] or '', r['id']),
+ "handle": f"wallco-{r['id']:04d}",
+ "image_url": f"/designs/img/{filename}",
+ "filename": filename,
+ "generator": "Replicate SDXL",
+ "seed": r['seed'],
+ "is_published": bool(r['is_published']),
+ "created_at": r['created_at']
+ })
+
+OUT.write_text(json.dumps(out, indent=2))
+print(f"Wrote {len(out)} designs → {OUT}")
+print(f" Categories: {dict((c, sum(1 for x in out if x['category']==c)) for c in sorted({x['category'] for x in out}))}")
+print(f" Top saturation 3: {[(x['title'], x['saturation']) for x in sorted(out, key=lambda x: -x['saturation'])[:3]]}")
diff --git a/scripts/spoonflower_lib.py b/scripts/spoonflower_lib.py
index f977aac..02f041a 100644
--- a/scripts/spoonflower_lib.py
+++ b/scripts/spoonflower_lib.py
@@ -1,12 +1,27 @@
"""
-Shared Spoonflower helpers — login + session cookie reuse.
-Used by upload_spoonflower_new.py and pull_spoonflower_account.py.
+Shared Spoonflower helpers — three auth paths in priority order:
+
+ 1. Connect to a running Chrome with --remote-debugging-port=9222
+ (Steve's actual browser, already logged in). This is the pattern
+ that worked in /Users/stevestudio2/Projects/Designer-Wallcoverings/
+ DW-Programming/150dpi/connect_and_download.js — proven reliable.
+
+ 2. Load a saved cookie jar from ~/.spoonflower-cookies.json
+ (export from Chrome DevTools or an extension once). This is what
+ the old 150dpi/spoonflower_download.js used on the Linux deploy.
-Reads creds from env (SPOONFLOWER_EMAIL / SPOONFLOWER_PASSWORD).
-Falls back to ~/Projects/resize-it/.env so we don't duplicate the secret.
+ 3. Programmatic email/password login (current fallback). Fragile
+ because Spoonflower's cart.* subdomain doesn't honor www.* session
+ cookies cleanly — get-user-stats returns userID=null for fresh
+ logins, even with correct creds.
+
+Used by upload_spoonflower_new.py and pull_spoonflower_account.py.
"""
-import os, sys, time, pathlib, json
-from typing import Tuple
+import os, sys, time, pathlib, json, subprocess
+from typing import Tuple, Optional
+
+COOKIES_PATH = pathlib.Path.home() / ".spoonflower-cookies.json"
+CHROME_DEBUG_PORT = 9222
def load_resize_it_env():
"""Bleed resize-it/.env into os.environ if creds missing."""
@@ -34,6 +49,96 @@ def creds() -> Tuple[str, str]:
return email, pw
+def try_attach_running_chrome(p):
+ """Path 1: connect to a running Chrome at 127.0.0.1:9222.
+
+ To open Chrome with debugging enabled:
+ /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\
+ --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-spoon
+
+ Then sign in to spoonflower.com once in that Chrome window.
+ Returns (browser, context, page) or (None, None, None) if not attachable.
+ """
+ try:
+ # Quick port probe
+ rc = subprocess.run(["curl", "-fsS", f"http://127.0.0.1:{CHROME_DEBUG_PORT}/json/version",
+ "--max-time", "2"], capture_output=True)
+ if rc.returncode != 0:
+ return None, None, None
+ except Exception:
+ return None, None, None
+ try:
+ browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{CHROME_DEBUG_PORT}")
+ contexts = browser.contexts
+ ctx = contexts[0] if contexts else browser.new_context()
+ # Try to reuse an existing logged-in tab
+ for c in contexts:
+ for pg in c.pages:
+ if "spoonflower" in (pg.url or ""):
+ return browser, c, pg
+ # Open a new page in the first context
+ page = ctx.new_page() if not ctx.pages else ctx.pages[0]
+ return browser, ctx, page
+ except Exception as e:
+ print(f"[chrome-attach] failed: {e}", file=sys.stderr, flush=True)
+ return None, None, None
+
+
+def try_load_saved_cookies(ctx) -> bool:
+ """Path 2: load cookies from ~/.spoonflower-cookies.json into a fresh context.
+
+ Cookie file format: JSON array as exported by Chrome DevTools
+ Application > Cookies > Right-click > Copy all (newer Chrome) OR
+ by the "EditThisCookie" / "Get cookies.txt" extension (JSON mode).
+ """
+ if not COOKIES_PATH.exists():
+ return False
+ try:
+ raw = json.loads(COOKIES_PATH.read_text())
+ except Exception as e:
+ print(f"[cookies] parse error: {e}", file=sys.stderr, flush=True)
+ return False
+ # Normalize to Playwright cookie shape
+ cookies = []
+ for c in raw:
+ if "name" not in c or "value" not in c:
+ continue
+ d = c.get("domain") or ".spoonflower.com"
+ if not d.startswith("."):
+ d = "." + d.lstrip(".")
+ cookies.append({
+ "name": c["name"],
+ "value": c["value"],
+ "domain": d,
+ "path": c.get("path", "/"),
+ "secure": bool(c.get("secure", True)),
+ "httpOnly": bool(c.get("httpOnly", c.get("http_only", False))),
+ "sameSite": ({"no_restriction":"None","lax":"Lax","strict":"Strict",
+ "unspecified":"Lax"}).get(str(c.get("sameSite","Lax")).lower(), "Lax"),
+ "expires": int(c.get("expirationDate") or c.get("expires") or -1),
+ })
+ try:
+ ctx.add_cookies(cookies)
+ print(f"[cookies] loaded {len(cookies)} cookies from {COOKIES_PATH}",
+ file=sys.stderr, flush=True)
+ return True
+ except Exception as e:
+ print(f"[cookies] add_cookies error: {e}", file=sys.stderr, flush=True)
+ return False
+
+
+def verify_signed_in(page) -> dict:
+ """Hit get-user-stats — userID populated = real signed-in session."""
+ try:
+ page.goto("https://cart.spoonflower.com/api/spoonflower/get-user-stats",
+ wait_until="domcontentloaded", timeout=20000)
+ body = page.evaluate("() => document.body.innerText")
+ info = json.loads(body) if body else {}
+ return info.get("data_layer", {}) or {}
+ except Exception as e:
+ return {"_error": str(e)}
+
+
def login(page, email: str, password: str) -> bool:
"""Sign in to Spoonflower. Returns True on success.
← cf92efe fix wallco.ai missing CSS — add general /public static mount
·
back to Wallco Ai
·
spoonflower: 3-path auth (Chrome attach > cookies > password 225c77f →