← back to Dw Fleet Registry

harvest-catalog.mjs

54 lines

#!/usr/bin/env node
// Harvest the full designerwallcoverings.com Shopify catalog (public products.json)
// into a HIGH-RES image pool tagged for niche matching. Read-only. Uses the API's
// width/height fields — no per-image download needed. Output: catalog-hires.json.

import fs from 'node:fs';
import path from 'node:path';

const SELF = path.dirname(new URL(import.meta.url).pathname);
const STORE = 'https://designerwallcoverings.com';
const MIN_LONG = 1600;   // long edge must clear this (catches landscape room shots a both-dims gate misses)
const MIN_SHORT = 1100;  // short edge floor — still sharp enough full-bleed, rejects thin banners/icons

// Classify an image by filename + product context. Room/lifestyle photos are the
// Museum-Wallpaper look Steve wants; swatch/roll product shots are the fallback.
function classify(src) {
  const f = src.split('/').pop().split('?')[0].toLowerCase();
  if (/(img_\d|lifestyle|roomset|room[-_ ]?setting|_rs_|interior|install|scene|insitu|in[-_]situ|styled|vignette|lookbook|rolledout|rolled[-_]out|onthewall|on[-_]wall)/.test(f)) return 'room';
  if (/(swatch|sample|cover|_sq|square|roll(?![a-z])|chip|thumb|memo)/.test(f)) return 'swatch';
  return 'pattern';
}

const pool = [];
const seenSrc = new Set();
for (let page = 1; page <= 60; page++) {
  let products = [];
  try {
    const r = await fetch(`${STORE}/products.json?limit=250&page=${page}`, { headers: { 'User-Agent': 'dw-catalog-harvest/2.0' } });
    const j = await r.json();
    products = j.products || [];
  } catch (e) { console.error('page', page, 'failed:', e.message); break; }
  if (!products.length) break;
  for (const p of products) {
    const meta = ((p.title || '') + ' ' + (p.product_type || '') + ' ' + (Array.isArray(p.tags) ? p.tags.join(' ') : (p.tags || ''))).toLowerCase();
    for (const im of (p.images || [])) {
      if (!im.src || seenSrc.has(im.src)) continue;
      const w = im.width || 0, h = im.height || 0;
      const long = Math.max(w, h), short = Math.min(w, h);
      if (long >= MIN_LONG && short >= MIN_SHORT) {
        seenSrc.add(im.src);
        pool.push({ src: im.src, w, h, title: p.title, handle: p.handle, meta, kind: classify(im.src), landscape: w > h });
      }
    }
  }
  if (page % 5 === 0) console.error(`  …page ${page}, pool ${pool.length}`);
}

// room shots first, then patterns, then swatches; within each, highest-res first.
const rank = { room: 0, pattern: 1, swatch: 2 };
pool.sort((a, b) => (rank[a.kind] - rank[b.kind]) || (Math.max(b.w, b.h) - Math.max(a.w, a.h)));
const byKind = pool.reduce((m, x) => (m[x.kind] = (m[x.kind] || 0) + 1, m), {});
fs.writeFileSync(path.join(SELF, 'catalog-hires.json'), JSON.stringify({ harvestedAt: new Date().toISOString(), minLong: MIN_LONG, minShort: MIN_SHORT, count: pool.length, byKind, pool }) + '\n');
console.log(`High-res pool: ${pool.length} images → catalog-hires.json`, byKind);