[object Object]

← back to Wallco Ai

spoonflower upload — fix hydration race + add --batch CSV mode. Per 2026-05 technical-researcher finding: Spoonflower is a React SPA so input[type=file] renders AFTER networkidle fires; the old 2s post-navigation sleep was racing. Bumped to 10s hydration wait + explicit page.wait_for_selector(state='attached', timeout=15s) on the file input. Adds --batch CSV mode iterating image_path,title,tags rows with --throttle (default 15s between items) to avoid Spoonflower rate-limits. CSV supports # comment lines + optional tags column. Dry-by-default safety preserved.

f4354f979d3b82e74485d18000abc38e16e109f8 · 2026-05-12 06:46:27 -0700 · Steve Abrams

Files touched

Diff

commit f4354f979d3b82e74485d18000abc38e16e109f8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 06:46:27 2026 -0700

    spoonflower upload — fix hydration race + add --batch CSV mode. Per 2026-05 technical-researcher finding: Spoonflower is a React SPA so input[type=file] renders AFTER networkidle fires; the old 2s post-navigation sleep was racing. Bumped to 10s hydration wait + explicit page.wait_for_selector(state='attached', timeout=15s) on the file input. Adds --batch CSV mode iterating image_path,title,tags rows with --throttle (default 15s between items) to avoid Spoonflower rate-limits. CSV supports # comment lines + optional tags column. Dry-by-default safety preserved.
---
 scripts/upload_spoonflower_new.py | 73 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 69 insertions(+), 4 deletions(-)

diff --git a/scripts/upload_spoonflower_new.py b/scripts/upload_spoonflower_new.py
index cc4854c..8daa092 100644
--- a/scripts/upload_spoonflower_new.py
+++ b/scripts/upload_spoonflower_new.py
@@ -39,10 +39,17 @@ def upload_new(image_path: str, title: str, tags: str = "", dry: bool = True):
             return 2
         emit("login_ok")
 
+        # Post-navigation hydration wait: Spoonflower is a React SPA so the file
+        # input + form chrome render *after* networkidle fires. 2s was too short
+        # in 2026-05 (technical-researcher finding); 10s clears React hydration
+        # on the slow path. We also explicitly wait_for() the file input so we
+        # don't proceed against a stale DOM.
+        HYDRATION_WAIT = 10
+
         if dry:
             # Dry-run: verify upload page is reachable, no submit.
             page.goto(UPLOAD_URL, wait_until="networkidle", timeout=60000)
-            time.sleep(2)
+            time.sleep(HYDRATION_WAIT)
             has_upload_widget = False
             for sel in ('input[type="file"]',
                         'button:has-text("Upload")',
@@ -62,7 +69,16 @@ def upload_new(image_path: str, title: str, tags: str = "", dry: bool = True):
 
         # ─── real upload ───
         page.goto(UPLOAD_URL, wait_until="networkidle", timeout=60000)
-        time.sleep(2)
+        time.sleep(HYDRATION_WAIT)
+
+        # Wait_for the file input to actually attach to the DOM (React hydration)
+        # rather than racing against the page render.
+        try:
+            page.wait_for_selector('input[type="file"]', state="attached", timeout=15000)
+        except Exception:
+            emit("done", ok=False, error="file input never attached after hydration wait")
+            browser.close()
+            return 3
 
         file_input = page.locator('input[type="file"]').first
         if not file_input:
@@ -134,16 +150,65 @@ def upload_new(image_path: str, title: str, tags: str = "", dry: bool = True):
         return 0
 
 
+def run_batch(csv_path: str, dry: bool, throttle_seconds: int = 15) -> int:
+    """Iterate a CSV of `image_path,title,tags` rows and call upload_new() on each.
+    Returns 0 if all succeeded, else the count of failures.
+
+    CSV format (no header):
+        /abs/path/to/image1.png,Title One,"tag1,tag2"
+        /abs/path/to/image2.png,Title Two,
+    Tags column may be omitted. Lines starting with # or blank are skipped.
+    """
+    import csv as _csv
+    path = Path(csv_path)
+    if not path.exists():
+        emit("done", ok=False, error=f"batch csv not found: {csv_path}")
+        return 1
+    rows = []
+    with path.open(newline="") as f:
+        for row in _csv.reader(f):
+            if not row or (row[0] or "").strip().startswith("#"):
+                continue
+            if len(row) < 2:
+                continue
+            img = row[0].strip()
+            title = row[1].strip()
+            tags = row[2].strip() if len(row) >= 3 else ""
+            if img and title:
+                rows.append((img, title, tags))
+    emit("batch_start", count=len(rows), dry=dry, csv=str(path))
+    fails = 0
+    for i, (img, title, tags) in enumerate(rows, 1):
+        emit("batch_item", index=i, total=len(rows), image=img, title=title, tags=tags)
+        rc = upload_new(img, title, tags, dry=dry)
+        if rc != 0:
+            fails += 1
+            emit("batch_item_failed", index=i, rc=rc, image=img)
+        # Throttle between items so Spoonflower doesn't rate-limit
+        if i < len(rows):
+            time.sleep(throttle_seconds)
+    emit("batch_done", total=len(rows), failures=fails)
+    return fails
+
+
 if __name__ == "__main__":
     ap = argparse.ArgumentParser()
-    ap.add_argument("image")
-    ap.add_argument("title")
+    ap.add_argument("image", nargs="?")
+    ap.add_argument("title", nargs="?")
     ap.add_argument("--tags", default="")
     ap.add_argument("--dry", action="store_true",
                     help="dry-run: login + upload-page reachability only (default)")
     ap.add_argument("--live", action="store_true",
                     help="actually upload (overrides --dry)")
+    ap.add_argument("--batch", default=None, metavar="CSV",
+                    help="batch upload from a CSV of image_path,title,tags rows (skips header lines starting with #)")
+    ap.add_argument("--throttle", type=int, default=15,
+                    help="seconds to wait between batch items (default 15)")
     args = ap.parse_args()
 
     dry = not args.live  # safety: default dry unless --live passed
+    if args.batch:
+        sys.exit(run_batch(args.batch, dry=dry, throttle_seconds=args.throttle))
+    if not args.image or not args.title:
+        ap.error("image + title required when not using --batch")
     sys.exit(upload_new(args.image, args.title, args.tags, dry=dry))

← 1d2bb76 wallco.ai · /designs JSON-LD — ItemList of Products + Breadc  ·  back to Wallco Ai  ·  wallco.ai · /design/:id Share button — Web Share API + clipb 680a09e →