← back to Designer Wallcoverings
Onboard Daisy Bennett Natural Wallcoverings (WallQuest→Phillipe Romano, 63 SKUs, isolated daisy_bennett_catalog)
532c47beeb2a5414ac74c1cda6cbed477f8c97bf · 2026-07-06 11:13:04 -0700 · Steve
Files touched
A scripts/wallquest-refresh/enrich-daisy-bennett.cjsA scripts/wallquest-refresh/load-db-daisy-bennett.cjsA scripts/wallquest-refresh/normalize-daisy-bennett.cjsA scripts/wallquest-refresh/onboard-daisy-bennett.cjs
Diff
commit 532c47beeb2a5414ac74c1cda6cbed477f8c97bf
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 6 11:13:04 2026 -0700
Onboard Daisy Bennett Natural Wallcoverings (WallQuest→Phillipe Romano, 63 SKUs, isolated daisy_bennett_catalog)
---
scripts/wallquest-refresh/enrich-daisy-bennett.cjs | 73 +++++++++++
.../wallquest-refresh/load-db-daisy-bennett.cjs | 89 +++++++++++++
.../wallquest-refresh/normalize-daisy-bennett.cjs | 55 ++++++++
.../wallquest-refresh/onboard-daisy-bennett.cjs | 141 +++++++++++++++++++++
4 files changed, 358 insertions(+)
diff --git a/scripts/wallquest-refresh/enrich-daisy-bennett.cjs b/scripts/wallquest-refresh/enrich-daisy-bennett.cjs
new file mode 100644
index 00000000..38dce24b
--- /dev/null
+++ b/scripts/wallquest-refresh/enrich-daisy-bennett.cjs
@@ -0,0 +1,73 @@
+// Phase 3 — Gemini vision enrichment for the Daisy Bennett naturals book.
+// Reads normalized records, enriches each primary image with colors/hex/styles/description,
+// writes an enriched JSON. Local cost logged. No DB write here (kept file-based pre-gate).
+const fs = require('fs');
+const https = require('https');
+const http = require('http');
+
+const KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
+if (!KEY) { console.error('no GEMINI_API_KEY'); process.exit(1); }
+const MODEL = process.env.GEMINI_MODEL || 'gemini-3.5-flash';
+const URL_ = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${KEY}`;
+const IN = process.env.IN || '/tmp/wq-daisy-bennett-normalized.json';
+const OUT = process.env.OUT || '/tmp/wq-daisy-bennett-enriched.json';
+
+const PROMPT = `You are cataloguing a NATURAL-TEXTURE wallcovering (grasscloth, raffia, paperweave, cork, mica, bamboo, netting, sisal, etc.). Analyze the image and return STRICT JSON only:
+{"dominant_color":"plain-english name","dominant_hex":"#RRGGBB","background_hex":"#RRGGBB","all_colors":["name",...max4],"styles":["Coastal","Organic Modern",...max4],"material_texture":"grasscloth|raffia|paperweave|cork|mica|bamboo|netting|sisal|linen|other","description":"1-2 sentence luxury retail description, NO brand names","image_type":"scan_swatch|photo_full|room_scene"}
+Return ONLY the JSON object, no markdown.`;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function dl(url) {
+ return new Promise((resolve, reject) => {
+ const req = (u, n = 0) => {
+ if (n > 5) return reject(new Error('redirects'));
+ (u.startsWith('https') ? https : http).get(u, { timeout: 15000 }, res => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) return req(res.headers.location, n + 1);
+ if (res.statusCode !== 200) { res.resume(); return reject(new Error('HTTP ' + res.statusCode)); }
+ const c = []; res.on('data', d => c.push(d));
+ res.on('end', () => { const b = Buffer.concat(c); resolve({ base64: b.toString('base64'), mime: (res.headers['content-type'] || 'image/jpeg').split(';')[0].trim() }); });
+ res.on('error', reject);
+ }).on('error', reject).on('timeout', function () { this.destroy(); reject(new Error('timeout')); });
+ }; req(url);
+ });
+}
+function gemini(base64, mime) {
+ const payload = { contents: [{ parts: [{ text: PROMPT }, { inline_data: { mime_type: mime, data: base64 } }] }], generationConfig: { temperature: 0.2, maxOutputTokens: 2048, responseMimeType: 'application/json' } };
+ return new Promise((resolve, reject) => {
+ const u = new URL(URL_); const body = JSON.stringify(payload);
+ const r = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 40000 }, res => {
+ let d = ''; res.on('data', c => d += c);
+ res.on('end', () => { try { const j = JSON.parse(d); if (j.error) return reject(new Error(j.error.message)); let t = j.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || ''; t = t.replace(/^```json\s*/i, '').replace(/```\s*$/, '').trim(); const m = t.match(/\{[\s\S]*\}/); resolve(JSON.parse(m ? m[0] : t)); } catch (e) { reject(e); } });
+ });
+ r.on('error', reject); r.on('timeout', () => { r.destroy(); reject(new Error('gemini timeout')); });
+ r.write(body); r.end();
+ });
+}
+
+(async () => {
+ const recs = JSON.parse(fs.readFileSync(IN, 'utf8'));
+ let ok = 0, err = 0, calls = 0;
+ for (const rec of recs) {
+ const img = (rec.images || [])[0];
+ if (!img) { rec.enrich_error = 'no image'; err++; continue; }
+ try {
+ const { base64, mime } = await dl(img);
+ const g = await gemini(base64, mime); calls++;
+ rec.ai_dominant_color = g.dominant_color;
+ rec.color_hex = g.dominant_hex;
+ rec.ai_background_color = g.background_hex;
+ rec.ai_colors = g.all_colors;
+ rec.ai_styles = g.styles;
+ rec.ai_material_texture = g.material_texture;
+ rec.ai_description = g.description;
+ rec.ai_image_type = g.image_type;
+ ok++;
+ process.stderr.write(`\r[${ok + err}/${recs.length}] ok=${ok} err=${err} ${rec.dw_sku} ${g.dominant_color||''} `);
+ } catch (e) { rec.enrich_error = String(e).slice(0, 100); err++; }
+ await sleep(1500); // rate limit
+ }
+ fs.writeFileSync(OUT, JSON.stringify(recs, null, 2));
+ const cost = (calls * 0.0006).toFixed(4);
+ console.error(`\nENRICH DONE ok=${ok} err=${err} calls=${calls} → ${OUT}`);
+ console.error(`COST ~$${cost} (Gemini ${MODEL}, ${calls} img calls @ ~$0.0006)`);
+})();
diff --git a/scripts/wallquest-refresh/load-db-daisy-bennett.cjs b/scripts/wallquest-refresh/load-db-daisy-bennett.cjs
new file mode 100644
index 00000000..9cfb871a
--- /dev/null
+++ b/scripts/wallquest-refresh/load-db-daisy-bennett.cjs
@@ -0,0 +1,89 @@
+// Phase 6 — load Daisy Bennett Natural Wallcoverings into dw_unified.daisy_bennett_catalog
+// with full specs + Phillipe Romano private-label routing. Dedicated table (isolated from
+// wallquest_catalog / Malibu) so the leak-scanner + weekly refresh never treat these as Malibu.
+// Idempotent upsert keyed on mfr_sku. Unlike Carl Robinson (beach-city rename), Daisy Bennett's
+// vendor pattern names (Boccali, Venezia — neutral Italian words, no vendor leak) are PRESERVED.
+// NO Shopify publish here — that stays Steve-gated (real_vendor_name never goes customer-facing).
+const fs = require('fs');
+const { Client } = require('pg');
+
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const ENR = process.env.ENR || '/tmp/wq-daisy-bennett-enriched.json';
+const NORM = process.env.NORM || '/tmp/wq-daisy-bennett-normalized.json';
+const recs = JSON.parse(fs.readFileSync(fs.existsSync(ENR) ? ENR : NORM, 'utf8'));
+
+const DDL = `
+CREATE TABLE IF NOT EXISTS daisy_bennett_catalog (
+ id serial PRIMARY KEY,
+ mfr_sku text UNIQUE NOT NULL,
+ dw_sku text NOT NULL,
+ pattern_name text, -- vendor design name (Boccali Grass, Venezia Grass, …)
+ color_name text,
+ collection text, -- pattern family bucket
+ material text,
+ product_type text DEFAULT 'Wallcovering',
+ price_retail numeric, -- our cost
+ our_price numeric, -- our retail (cost/0.65/0.85)
+ price_dw numeric,
+ width text, length text, repeat_v text, match_type text, backing text,
+ specs jsonb,
+ image_url text,
+ all_images jsonb,
+ local_image_path text,
+ color_hex text,
+ ai_dominant_color text,
+ ai_background_color text,
+ ai_colors jsonb,
+ ai_styles jsonb,
+ ai_description text,
+ room_setting_images jsonb DEFAULT '[]'::jsonb,
+ private_label_vendor text DEFAULT 'Phillipe Romano',
+ real_vendor_name text DEFAULT 'WallQuest',
+ source_book text DEFAULT 'Daisy Bennett - Natural Wallcoverings',
+ on_shopify boolean DEFAULT false,
+ product_url text,
+ created_at timestamptz DEFAULT now(),
+ updated_at timestamptz DEFAULT now()
+);`;
+
+(async () => {
+ const db = new Client({ connectionString: CONN });
+ await db.connect();
+ await db.query(DDL);
+ let n = 0;
+ for (const r of recs) {
+ // collection bucket = leading material word of the pattern (Grass, Weave, Cloth, …)
+ const collection = (r.pattern || '').split(/\s+/).slice(-1)[0] || 'Naturals';
+ await db.query(`
+ INSERT INTO daisy_bennett_catalog
+ (mfr_sku,dw_sku,pattern_name,color_name,collection,material,product_type,
+ price_retail,our_price,price_dw,width,length,repeat_v,match_type,backing,specs,
+ image_url,all_images,color_hex,ai_dominant_color,ai_background_color,
+ ai_colors,ai_styles,ai_description,product_url,updated_at)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,now())
+ ON CONFLICT (mfr_sku) DO UPDATE SET
+ pattern_name=EXCLUDED.pattern_name, color_name=EXCLUDED.color_name, collection=EXCLUDED.collection,
+ material=EXCLUDED.material, price_retail=EXCLUDED.price_retail, our_price=EXCLUDED.our_price,
+ price_dw=EXCLUDED.price_dw, width=EXCLUDED.width, length=EXCLUDED.length, repeat_v=EXCLUDED.repeat_v,
+ match_type=EXCLUDED.match_type, backing=EXCLUDED.backing, specs=EXCLUDED.specs,
+ image_url=EXCLUDED.image_url, all_images=EXCLUDED.all_images,
+ color_hex=EXCLUDED.color_hex, ai_dominant_color=EXCLUDED.ai_dominant_color,
+ ai_background_color=EXCLUDED.ai_background_color, ai_colors=EXCLUDED.ai_colors,
+ ai_styles=EXCLUDED.ai_styles, ai_description=EXCLUDED.ai_description,
+ product_url=EXCLUDED.product_url, updated_at=now()`,
+ [r.mfr_sku, r.dw_sku, r.pattern || '', r.color || '', collection, r.material || '', r.product_type || 'Wallcovering',
+ r.price_retail ?? null, r.our_price ?? null, r.price_retail ? +(r.price_retail / 0.55).toFixed(2) : null,
+ r.width || '', r.length || '', r.repeat || '', r.match || '', r.backing || '', JSON.stringify(r.specs || {}),
+ (r.images || [])[0] || '', JSON.stringify(r.images || []),
+ r.color_hex || null, r.ai_dominant_color || null, r.ai_background_color || null,
+ JSON.stringify(r.ai_colors || []), JSON.stringify(r.ai_styles || []), r.ai_description || null,
+ r.product_url || '']);
+ n++;
+ }
+ const { rows } = await db.query(`SELECT count(*) total, count(*) FILTER (WHERE price_retail>0) priced,
+ count(*) FILTER (WHERE ai_description IS NOT NULL) enriched,
+ count(DISTINCT collection) families FROM daisy_bennett_catalog`);
+ console.log(`loaded/updated ${n} rows into dw_unified.daisy_bennett_catalog`);
+ console.log('DB tally:', rows[0]);
+ await db.end();
+})().catch(e => { console.error('DB ERROR:', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/normalize-daisy-bennett.cjs b/scripts/wallquest-refresh/normalize-daisy-bennett.cjs
new file mode 100644
index 00000000..2f412271
--- /dev/null
+++ b/scripts/wallquest-refresh/normalize-daisy-bennett.cjs
@@ -0,0 +1,55 @@
+// Phase 2 — normalize the raw Daisy Bennett scrape → structured records + provisional DW SKUs.
+// RAW source = /tmp/wq-daisy-bennett.jsonl (WallQuest). TARGET private label = Phillipe Romano
+// (naturals book, same house as Carl Robinson). Mfr SKU family = BX##### (bx10121 → BX10121).
+// dw_sku prefix DWAY- is PROVISIONAL (phone-safe: A,Y allowed; not registered until gated publish).
+// This pass structures vendor data + surfaces the distinct pattern families; NO rename, NO publish.
+const fs = require('fs');
+const IN = process.env.IN || '/tmp/wq-daisy-bennett.jsonl';
+const OUT = process.env.OUT || '/tmp/wq-daisy-bennett-normalized.json';
+const PREFIX = process.env.PREFIX || 'DWAY'; // provisional; confirm at publish gate
+
+const rows = fs.readFileSync(IN, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse)
+ .filter(r => r.title && r.priceValue); // keep only fully-scraped rows
+
+const rec = rows.map(r => {
+ const slug = r.url.replace(/https?:\/\/[^/]+\//i, '').split('?')[0].replace(/\/$/, '');
+ const mfr_sku = slug.toUpperCase(); // bx10121 → BX10121
+ const numTail = (mfr_sku.match(/\d+/) || ['0'])[0]; // 10121
+ const dw_sku = `${PREFIX}-${numTail}`;
+ const s = r.specs || {};
+ // Vendor titles read "Pattern - Color" (e.g. "Boccali Grass - Natural Tan Cream")
+ const pattern = (s['Design Name'] || (r.title.split(' - ')[0] || '')).trim();
+ const color = (s['Colorway Name'] || (r.title.split(' - ').slice(1).join(' - ') || '')).trim();
+ const cost = parseFloat(String(r.priceValue).replace(/,/g, ''));
+ return {
+ mfr_sku, dw_sku,
+ src_title: r.title,
+ pattern, color,
+ price_retail: cost, // = our COST (WallQuest portal convention)
+ our_price: +(cost / 0.65 / 0.85).toFixed(2), // new-product retail formula
+ material: s['Material'] || '',
+ backing: s['Backing Material'] || '',
+ width: s['Roll Width'] || s['Width'] || '',
+ length: s['Roll Length'] || s['Length'] || '',
+ repeat: s['Design Repeat'] || s['Vertical Repeat'] || '',
+ match: s['Match'] || s['Design Match'] || '',
+ product_type: s['Product Type'] || 'Wallcovering',
+ specs: s,
+ images: r.imgs || [],
+ product_url: r.url,
+ };
+});
+
+fs.writeFileSync(OUT, JSON.stringify(rec, null, 2));
+
+// Report distinct pattern families
+const byPattern = {};
+for (const x of rec) (byPattern[x.pattern] = byPattern[x.pattern] || []).push(x.color);
+console.log(`normalized ${rec.length} rows → ${OUT} (prefix ${PREFIX}-)`);
+console.log(`\nDISTINCT PATTERN FAMILIES (${Object.keys(byPattern).length}):`);
+for (const [p, cols] of Object.entries(byPattern).sort()) {
+ console.log(` • ${p} (${cols.length} colorways: ${cols.join(', ')})`);
+}
+const noPrice = rows.length - rec.length;
+if (noPrice) console.log(`\n⚠ ${noPrice} scraped rows dropped (missing title/price) — will re-check`);
+if (rec.length) console.log(`\ncost range: $${Math.min(...rec.map(r=>r.price_retail))}–$${Math.max(...rec.map(r=>r.price_retail))}`);
diff --git a/scripts/wallquest-refresh/onboard-daisy-bennett.cjs b/scripts/wallquest-refresh/onboard-daisy-bennett.cjs
new file mode 100644
index 00000000..79187b17
--- /dev/null
+++ b/scripts/wallquest-refresh/onboard-daisy-bennett.cjs
@@ -0,0 +1,141 @@
+// Daisy Bennett — Natural Wallcoverings — collection ONBOARD scraper.
+// Mirrors onboard-carl-robinson.cjs exactly (sibling-terminal convention): discovers
+// product-detail URLs from the collection listing, then login-scrapes each
+// (specs/price/images/title/sku). Captures RAW vendor (WallQuest) data only — NO rename,
+// NO SKU assignment, NO publish. Private-label routing happens in later phases.
+// MODE=discover → paginate listing, save product URLs (cheap; ~1 bb session)
+// MODE=scrape → login-scrape each discovered URL → JSONL
+const Browserbase = require('@browserbasehq/sdk').default;
+const { chromium } = require('playwright-core');
+const fs = require('fs');
+require('dotenv').config({ path: require('os').homedir() + '/.claude/skills/browserbase/.env' });
+
+const COLLECTION = 'https://www.wallquest.com/daisy-bennett-natural-wallcoverings';
+const WPW = '*Wallquestaccess911*';
+const WUSER = 'info@designerwallcoverings.com';
+const URLS_FILE = '/tmp/wq-daisy-bennett-urls.json';
+const OUT = '/tmp/wq-daisy-bennett.jsonl';
+const MODE = process.env.MODE || 'discover';
+// Any Daisy Bennett collection-landing slug is NOT a product (the priceless WQ-DAISYBENNETT* rows).
+const COLLECTION_SLUG = /daisy-bennett/i;
+
+const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
+const goto = async (p, u) => { for (let i = 0; i < 3; i++) { try { await p.goto(u, { waitUntil: 'domcontentloaded', timeout: 45000 }); return 1; } catch (e) { await p.waitForTimeout(3000); } } return 0; };
+const isLoggedIn = (page) => page.evaluate(() =>
+ /log\s*out|sign\s*out/i.test(document.body.innerText) ||
+ !!document.querySelector('a.ico-logout, .header-links a[href*="logout"]')
+).catch(() => false);
+
+async function login(page) {
+ for (let attempt = 1; attempt <= 3; attempt++) {
+ await goto(page, 'https://www.wallquest.com/login');
+ await page.waitForTimeout(7000);
+ try {
+ await page.fill('#Email', WUSER);
+ await page.fill('#Password', WPW);
+ await page.press('#Password', 'Enter');
+ } catch (e) { /* form not ready; loop retries */ }
+ await page.waitForTimeout(9000);
+ if (await isLoggedIn(page)) { console.error(` login OK (attempt ${attempt})`); return true; }
+ console.error(` login attempt ${attempt} not confirmed, retrying…`);
+ await page.waitForTimeout(3000);
+ }
+ console.error(' LOGIN FAILED after 3 attempts — chunk will likely be priceless');
+ return false;
+}
+
+// Collect product-detail URLs from a nopCommerce collection listing page.
+const discoverOnPage = (p) => p.evaluate(() => {
+ const bad = /\/(login|logout|cart|wishlist|compare|register|account|search)|#/i;
+ const set = new Set();
+ document.querySelectorAll('.item-box a, .product-item a, .product-title a, .picture a, h2.product-title a').forEach(a => {
+ const href = a.href || '';
+ if (!href || bad.test(href)) return;
+ if (!/wallquest\.com\//i.test(href)) return;
+ const path = href.replace(/https?:\/\/[^/]+/i, '').split('?')[0].replace(/\/$/, '');
+ if (!path || path.split('/').length > 2) return;
+ if (/daisy-bennett/i.test(path)) return; // collection page/anchors, not products
+ set.add(href.split('?')[0]);
+ });
+ return [...set];
+});
+
+const extract = (p) => p.evaluate(() => {
+ const Q = s => [...document.querySelectorAll(s)];
+ const specs = {};
+ Q('.spec-name').forEach(n => { const v = n.nextElementSibling; if (v) specs[n.textContent.trim()] = v.textContent.trim(); });
+ const imgs = [...new Set(
+ Q('a[data-full-image-url]').map(a => a.getAttribute('data-full-image-url'))
+ .concat(Q('#cloudZoomImage,.picture img').map(i => i.src))
+ )].filter(u => u && /jpe?g|png/i.test(u));
+ const pv = ((Q('[class*=price-value]')[0] || {}).textContent || '').match(/[\d,]+\.\d{2}/);
+ const title = (document.querySelector('.product-name h1, h1.product-title, .product-essential h1, h1') || {}).textContent || '';
+ const sku = (document.querySelector('[itemprop=sku], .sku .value, .sku-number, .product-sku .value') || {}).textContent || '';
+ return { title: title.trim(), sku: sku.trim(), specs, imgs: imgs.slice(0, 20), priceValue: pv ? pv[0] : null };
+});
+
+(async () => {
+ if (MODE === 'discover') {
+ const s = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID, proxies: true, browserSettings: { solveCaptchas: true } });
+ const br = await chromium.connectOverCDP(s.connectUrl);
+ const page = br.contexts()[0].pages()[0] || await br.contexts()[0].newPage();
+ try {
+ await login(page);
+ const all = new Set();
+ for (let pn = 1; pn <= 10; pn++) {
+ if (!await goto(page, `${COLLECTION}?orderby=50&pagesize=100&viewmode=grid&pagenumber=${pn}`)) break;
+ await page.waitForTimeout(6000);
+ const found = await discoverOnPage(page);
+ const before = all.size; found.forEach(u => all.add(u));
+ console.error(`[page ${pn}] found=${found.length} total=${all.size}`);
+ if (all.size === before) break; // no new products → past the end
+ }
+ const urls = [...all];
+ fs.writeFileSync(URLS_FILE, JSON.stringify(urls, null, 2));
+ console.error(`DISCOVER DONE — ${urls.length} product URLs → ${URLS_FILE}`);
+ console.log(JSON.stringify({ count: urls.length, sample: urls.slice(0, 12) }, null, 2));
+ } finally { await br.close().catch(() => {}); }
+ return;
+ }
+
+ // MODE=scrape (env-tunable: SRC url-file, OUTF output, CHUNK, WAITMS)
+ const SRC = process.env.SRC || URLS_FILE;
+ const OUTF = process.env.OUTF || OUT;
+ const CHUNK = parseInt(process.env.CHUNK || '40', 10);
+ const WAITMS = parseInt(process.env.WAITMS || '9000', 10);
+ const urls = JSON.parse(fs.readFileSync(SRC, 'utf8'));
+ fs.writeFileSync(OUTF, '');
+ const out = fs.createWriteStream(OUTF, { flags: 'a' });
+ let done = 0, priced = 0;
+ for (let i = 0; i < urls.length; i += CHUNK) {
+ const chunk = urls.slice(i, i + CHUNK); let br, page, authed = false;
+ for (let sTry = 1; sTry <= 3 && !authed; sTry++) {
+ try {
+ if (br) await br.close().catch(() => {});
+ const s = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID, proxies: true, browserSettings: { solveCaptchas: true } });
+ br = await chromium.connectOverCDP(s.connectUrl);
+ page = br.contexts()[0].pages()[0] || await br.contexts()[0].newPage();
+ authed = await login(page);
+ if (!authed) console.error(` session ${sTry} did not authenticate — recreating`);
+ } catch (e) { console.error('session create err', String(e).slice(0, 120)); }
+ }
+ if (!authed) { console.error(`[chunk ${Math.floor(i / CHUNK) + 1}] SKIP — no authed session`); if (br) await br.close().catch(() => {}); continue; }
+ try {
+ for (const url of chunk) {
+ try {
+ if (!await goto(page, url)) { out.write(JSON.stringify({ url, nav: 0 }) + '\n'); done++; continue; }
+ await page.waitForTimeout(WAITMS);
+ let x = await extract(page);
+ if (!x.priceValue) { await page.waitForTimeout(6000); x = await extract(page); }
+ if (x.priceValue) priced++;
+ out.write(JSON.stringify({ url, ...x }) + '\n');
+ } catch (e) { out.write(JSON.stringify({ url, err: String(e).slice(0, 120) }) + '\n'); }
+ done++;
+ }
+ } catch (e) { console.error('session err', String(e).slice(0, 160)); }
+ finally { if (br) await br.close().catch(() => {}); }
+ console.error(`[chunk ${Math.floor(i / CHUNK) + 1}] done=${done}/${urls.length} priced=${priced}`);
+ }
+ out.end();
+ console.error(`SCRAPE DONE done=${done} priced=${priced} → ${OUTF}`);
+})();
← 0eddd307 Carl Robinson: Shopify push (draft) + activate/publish-all-c
·
back to Designer Wallcoverings
·
Onboard Daisy Bennett Naturals Vol 2 (WallQuest→Phillipe Rom 95ef94b5 →