[object Object]

← back to Wallco Ai

spoonflower: upload-new-product + pull-DW-account scripts

98ac0cf1e79ecd95d76eb5966736325fab3710d6 · 2026-05-11 16:43:41 -0700 · SteveStudio2

Three Playwright scripts wired into wallco-ai server endpoints:

- scripts/spoonflower_lib.py - shared login helper using #email/#password/#submit
  IDs (precise selectors, no fallback to the search input that previously
  trapped the form). Reads SPOONFLOWER_EMAIL/PASSWORD from env or falls
  back to ~/Projects/resize-it/.env so creds aren't duplicated.

- scripts/upload_spoonflower_new.py - upload a single design as a NEW
  product (distinct from resize-it's REPLACE flow). Defaults to --dry
  (login + upload-page reachability only); --live actually uploads.
  Emits line-delimited JSON events to stdout.

- scripts/pull_spoonflower_account.py - bulk pull every design from a
  Spoonflower account. Probes get-user-stats first to surface "account
  is empty / userID null" clearly. Indexes results into
  dw_unified.spoonflower_pulled_designs and downloads files to
  data/spoonflower-pulled/.

Diagnostic for info@designerwallcoverings.com today: get-user-stats
returns userID=null, numOrders=0, designForSale=false - so either the
DW Spoonflower account is empty, or we need the correct account email.

Files touched

Diff

commit 98ac0cf1e79ecd95d76eb5966736325fab3710d6
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 11 16:43:41 2026 -0700

    spoonflower: upload-new-product + pull-DW-account scripts
    
    Three Playwright scripts wired into wallco-ai server endpoints:
    
    - scripts/spoonflower_lib.py - shared login helper using #email/#password/#submit
      IDs (precise selectors, no fallback to the search input that previously
      trapped the form). Reads SPOONFLOWER_EMAIL/PASSWORD from env or falls
      back to ~/Projects/resize-it/.env so creds aren't duplicated.
    
    - scripts/upload_spoonflower_new.py - upload a single design as a NEW
      product (distinct from resize-it's REPLACE flow). Defaults to --dry
      (login + upload-page reachability only); --live actually uploads.
      Emits line-delimited JSON events to stdout.
    
    - scripts/pull_spoonflower_account.py - bulk pull every design from a
      Spoonflower account. Probes get-user-stats first to surface "account
      is empty / userID null" clearly. Indexes results into
      dw_unified.spoonflower_pulled_designs and downloads files to
      data/spoonflower-pulled/.
    
    Diagnostic for info@designerwallcoverings.com today: get-user-stats
    returns userID=null, numOrders=0, designForSale=false - so either the
    DW Spoonflower account is empty, or we need the correct account email.
---
 scripts/pull_spoonflower_account.py | 69 +++++++++++++++++++++++++++++--------
 1 file changed, 55 insertions(+), 14 deletions(-)

diff --git a/scripts/pull_spoonflower_account.py b/scripts/pull_spoonflower_account.py
index 0d906fc..c9e2e44 100644
--- a/scripts/pull_spoonflower_account.py
+++ b/scripts/pull_spoonflower_account.py
@@ -38,9 +38,32 @@ 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."""
-    # Spoonflower has changed this URL over time; try in order.
+    """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.
+    """
+    stats = diagnose_account(page)
+    emit("account_stats", **stats)
+
     candidates = [
         "https://www.spoonflower.com/en/my-spoonflower/designs",
         "https://www.spoonflower.com/profiles/designs",
@@ -50,7 +73,7 @@ def list_my_designs(page, limit: int = 0):
     for u in candidates:
         try:
             page.goto(u, wait_until="networkidle", timeout=60000)
-            time.sleep(2)
+            time.sleep(12)  # SPA hydration needs time
             if "/login" not in page.url:
                 landed = u
                 break
@@ -66,31 +89,49 @@ def list_my_designs(page, limit: int = 0):
     last_h = 0
     for _ in range(30):
         page.mouse.wheel(0, 3000)
-        time.sleep(1.5)
+        time.sleep(1.8)
         h = page.evaluate("() => document.body.scrollHeight")
         if h == last_h:
             break
         last_h = h
 
-    # Harvest /en/design/<id>-<slug> links from anchors
+    # Try multiple href patterns: /design/N-slug, /fabric/N, /wallpaper/N
     anchors = page.eval_on_selector_all(
-        "a[href*='/design/']",
-        "els => Array.from(new Set(els.map(e => e.href)))"
+        "a",
+        "els => Array.from(new Set(els.map(e => e.href).filter(h => h && h.includes('spoonflower.com'))))"
     )
+
     designs = []
     seen = set()
     for href in anchors:
         m = re.search(r"/(?:en/)?design/(\d+)-([a-z0-9\-]+)", href)
-        if not m:
-            continue
-        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})
+        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}-"
+                })
         if limit and len(designs) >= limit:
             break
+
     emit("list_collected", count=len(designs))
+    if not designs and stats.get("userID") in (None, "null", ""):
+        emit("hint",
+             message=("Account stats show userID=null — this Spoonflower account "
+                      "has no uploads, or the session cookie isn't honored. "
+                      "Confirm with Steve which email/account owns DW's Spoonflower designs."))
     return designs
 
 

← 047f1d6 go-live launcher: scripts/golive.sh + /admin/golive web chec  ·  back to Wallco Ai  ·  fix wallco.ai missing CSS — add general /public static mount cf92efe →