← 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
M scripts/upload_spoonflower_new.py
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 →