← back to Schumacher Internal

_sangetsu_spec.mjs

75 lines

// sangetsu-spec-scrape.mjs — headless-render each Sangetsu product page and
// parse its SPECIFICATIONS block (the store-API description LACKS this; the
// rendered page HAS it, e.g. "Dimension Per Unit: 54\" x 30 yards | Backing:
// Vinyl Fabric Back | Origin: USA"). Recovers width + backing/origin/repeat/match
// for patterns that expose a specs section (spec-less "coming soon" books yield
// nothing — reported, never fabricated).
//
// FREE (local chromium). Reads pattern list from sangetsu-staging.jsonl, writes
// staging/sangetsu-page-specs.jsonl keyed by pattern. Resumable: skips patterns
// already present in the out file.
import { chromium } from 'playwright';
import fs from 'fs';

const DIR = '/Users/macstudio3/Projects/Designer-Wallcoverings/onboarding/sangetsu-lilycolor/staging';
const IN = `${DIR}/sangetsu-staging.jsonl`;
const OUT = `${DIR}/sangetsu-page-specs.jsonl`;

const pats = fs.readFileSync(IN, 'utf8').trim().split('\n').map(JSON.parse);
const done = new Set(
  fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l).pattern) : []
);

function parseWidth(seg) {
  if (!seg) return '';
  let m;
  // "Width 52" / 132 cm" -> prefer cm
  m = seg.match(/width\s*:?\s*\d{1,3}\s*["”″]\s*\/\s*(\d{2,3})\s*cm/i); if (m) return `${m[1]} cm`;
  // "width 0.91 m" / "x width 0.91 m" -> cm
  m = seg.match(/width\s*:?\s*(\d(?:\.\d+)?)\s*m\b/i);                 if (m) return `${Math.round(parseFloat(m[1]) * 100)} cm`;
  // "Width: 132 cm" / "Width 132 cm"
  m = seg.match(/width\s*:?\s*(\d{2,3})\s*cm/i);                       if (m) return `${m[1]} cm`;
  // "Dimension Per Unit: 54" x 30 yards" -> inches
  m = seg.match(/(\d{2,3})\s*["”″]\s*x\s*\d/i);                        if (m) return `${m[1]} Inches`;
  // "Width 52"" (inch only)
  m = seg.match(/width\s*:?\s*(\d{2,3})\s*["”″]/i);                    if (m) return `${m[1]} Inches`;
  return '';
}
function field(txt, label) {
  const re = new RegExp(label + '\\s*:?\\s*([^\\n|]{1,40}?)(?=\\s{2,}|$|[A-Z][a-z]+\\s+[A-Z]|Width|Match|Backing|Weight|Fire|Meets|Custom|CARE|Type|Countries)', 'i');
  const m = txt.match(re); return m ? m[1].trim().replace(/[:.\s]+$/, '') : '';
}

const b = await chromium.launch({ headless: true });
const stream = fs.createWriteStream(OUT, { flags: 'a' });
let hit = 0, miss = 0, i = 0;
for (const p of pats) {
  i++;
  if (done.has(p.pattern)) continue;
  const pg = await b.newPage();
  let rec = { pattern: p.pattern, width: '', backing: '', origin: '', repeat: '', match: '', has_spec: false };
  try {
    await pg.goto(p.source_url, { waitUntil: 'domcontentloaded', timeout: 40000 });
    await pg.waitForTimeout(900);
    const body = (await pg.innerText('body').catch(() => '')) || '';
    const si = body.search(/SPECIFICATIONS/i);
    if (si >= 0) {
      const seg = body.slice(si, si + 500).replace(/\s+/g, ' ');
      rec.has_spec = true;
      rec.width = parseWidth(seg);
      rec.backing = field(seg, 'Backing');
      rec.origin = field(seg, 'Origin');
      rec.repeat = field(seg, 'Repeat');
      rec.match = field(seg, 'Match');
      rec.spec_raw = seg.slice(0, 240);
    }
    if (rec.width) hit++; else miss++;
  } catch (e) { miss++; rec.err = e.message.slice(0, 40); }
  stream.write(JSON.stringify(rec) + '\n');
  await pg.close();
  if (i % 50 === 0) console.log(`[${i}/${pats.length}] width_hit=${hit} miss=${miss}`);
}
stream.end();
await b.close();
console.log(JSON.stringify({ patterns: pats.length, width_recovered: hit, no_width: miss }));