← back to Hollywood Import

momentum-recon3.mjs

52 lines

// Recon #3: clean structured price+sku extraction for N detail pages, reusing saved auth.
// Prints labeled price fields + item number + pattern/color + unit so we can map Momentum's
// price tiers onto our stored list_price (calibrate the ×1.448 model). Read-only.
import { chromium } from 'playwright';
import fs from 'node:fs';
const STATE = new URL('momentum-auth.json', import.meta.url).pathname;
const BASE = 'https://momentumco.com';
// color numbers to sample (first from grid + a few more discovered)
const seeds = process.argv.slice(2);

const browser = await chromium.launch({ channel: 'chrome', headless: true });
const ctx = await browser.newContext({ storageState: STATE, viewport: { width: 1400, height: 1000 } });
const page = await ctx.newPage();

// Pull color numbers off the grid if none passed
let nums = seeds;
if (!nums.length) {
  await page.goto(BASE + '/products?forceSync=1', { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(()=>{});
  await page.waitForTimeout(5000);
  for (let i=0;i<2;i++){ await page.mouse.wheel(0,5000); await page.waitForTimeout(2000); }
  nums = await page.evaluate(() => [...new Set([...document.querySelectorAll('a[href*="/products/"]')]
    .map(a => (a.getAttribute('href')||'').split('/products/')[1]).filter(Boolean))].slice(0, 6));
}

const results = [];
for (const num of nums) {
  await page.goto(`${BASE}/products/${num}`, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(()=>{});
  await page.waitForTimeout(3000);
  const r = await page.evaluate(() => {
    const grab = (re) => { const m = document.body.innerText.match(re); return m ? m[0] : null; };
    // labeled price fields
    const labels = {};
    document.body.innerText.split('\n').forEach(line => {
      const m = line.match(/^\s*(List Price|Net Price|Price|Cut Price|Retail|Memo|Sample|Your Price)\s*:?\s*\$?\s*([\d,]+\.\d{2})\s*(\/\s*\w+|per\s+\w+)?/i);
      if (m) labels[m[1].trim()] = { value: m[2], unit: (m[3]||'').trim() };
    });
    // item number — look for "Item", "SKU", "Pattern #", or codes near title
    const itemNo = grab(/(Item\s*#?|SKU|Pattern\s*#?|Product\s*#?)\s*:?\s*[A-Za-z0-9-]+/i);
    const allCodes = [...new Set((document.body.innerText.match(/\b[A-Z0-9]{2,5}-?\d{3,6}\b/g)||[]))].slice(0,10);
    return { url: location.href, title: document.title,
      pattern: (document.title.split(' - ')[0]||'').trim(),
      color: (document.title.split(' - ')[1]||'').trim(),
      labels, itemNo, allCodes,
      // capture the price widget outerHTML for one product to see exact structure
    };
  });
  results.push(r);
}
fs.writeFileSync(new URL('recon3-out.json', import.meta.url), JSON.stringify(results, null, 2));
console.log(JSON.stringify(results, null, 2));
await browser.close();