← back to Chinoiseriewallpaper

scripts/pull-from-shopify.js

108 lines

// Paginate DW Shopify /products.json — filter to chinoiserie niche
// Uses curl (follows redirects, passes CF bot check better) then parses JSON
// Run: node scripts/pull-from-shopify.js
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { classifyAesthetic } = require('../../_shared/microsite-aesthetic');
const SITE_CFG_RAILS = (() => { try { return require('../site.config.json').rails || []; } catch { return []; } })();

const OUT = path.join(__dirname, '..', 'data', 'products.json');

// Tags that signal chinoiserie / oriental heritage wallcoverings
const CHINOIS_TAGS = /^(chinoiserie|oriental|asian|hand-painted|handpainted|scenic|peacock|pagoda|peony|peonies|dynasty|imperial|ming|bamboo|blossom|cherry blossom|toile|chinois|asian inspired|asian-inspired|japanese|japanse|tropical|paradise|palm|botanical|floral|scenic mural|panoramic|mural)$/i;
const CHINOIS_TITLE = /\b(chinoiserie|peacock|pagoda|peony|peonies|dynasty|imperial|bamboo|blossom|cherry.?blossom|toile|oriental|hand.?paint|handpaint|scenic|panoramic|magnolia|plum.?blossom|paradise|phoeni|crane|koi|lotus|chrysanthemum|willow|ming|manchu|celestial|chinois|japanes|japanse)\b/i;
const REJECT_TITLE = /(visual.{0,3}merchandiser|bh.?90210|beverly.?hills.?90210|iconic.{0,4}bh|\bimage[ _-]?4\b|lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine)/i;

function fetchPage(page) {
  const url = `https://designerwallcoverings.com/products.json?limit=250&page=${page}`;
  try {
    const body = execFileSync('curl', [
      '-sS', '-L',
      '-A', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
      '--max-time', '30',
      url
    ], { encoding: 'utf8', timeout: 35000 });
    const data = JSON.parse(body);
    return data.products || [];
  } catch(e) {
    console.error(`  Page ${page} error: ${e.message}`);
    return [];
  }
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

function aestheticOf(tags, title) {
  const blob = [...(tags || []).map(x => x.toLowerCase()), (title || '').toLowerCase()].join(' ');
  if (/peacock|crane|phoeni/.test(blob)) return 'avian';
  if (/pagoda|temple|palace|lantern|ming|dynasty|imperial|manchu|celestial|chinois/.test(blob)) return 'imperial';
  if (/peony|peonies|blossom|cherry|magnolia|lotus|chrysanthemum|plum/.test(blob)) return 'botanica';
  if (/bamboo|willow|koi|fish|frog|dragonfly/.test(blob)) return 'garden';
  if (/toile|scenic|panoramic|mural|landscape|mountainscape/.test(blob)) return 'scenic';
  if (/hand.?paint|handpaint/.test(blob)) return 'handpainted';
  if (/japanes|japanse/.test(blob)) return 'japanese';
  return 'chinoiserie';
}

function classifyForSite(tags, title) {
  const railsMatch = classifyAesthetic(tags, SITE_CFG_RAILS);
  if (railsMatch && railsMatch !== 'all') return railsMatch;
  return aestheticOf(tags, title);
}

(async () => {
  const all = [];
  for (let page = 1; page <= 40; page++) {
    const products = fetchPage(page);
    if (!products.length) { console.log(`Page ${page}: empty — stopping.`); break; }
    all.push(...products);
    process.stdout.write(`  page ${page}: ${products.length} (running total: ${all.length})\n`);
    if (products.length < 250) break;
    await sleep(300);
  }
  console.log(`\nTotal fetched from Shopify: ${all.length}`);

  const chinois = all
    .filter(p => p.product_type && /wallcovering/i.test(p.product_type))
    .filter(p => !REJECT_TITLE.test(p.title || ''))
    .filter(p => p.images && p.images.length > 0 && p.images[0].src)
    .filter(p => {
      const hasChinoisTag = (p.tags || []).some(t => CHINOIS_TAGS.test(t));
      const hasChinoisTitle = CHINOIS_TITLE.test(p.title || '');
      return hasChinoisTag || hasChinoisTitle;
    })
    .map(p => {
      const images = (p.images || []).slice(0, 4).map(i => i.src);
      const firstVariant = (p.variants || [])[0] || {};
      const price = parseFloat(firstVariant.price) || 0;
      const comparePrice = parseFloat(firstVariant.compare_at_price) || 0;
      // Never expose Command54 vendor name
      const vendor = (p.vendor === 'Command54' ? 'Designer Wallcoverings' : (p.vendor || '').trim());
      return {
        sku: p.handle,
        handle: p.handle,
        title: p.title,
        vendor,
        product_type: p.product_type,
        image_url: images[0],
        images,
        tags: p.tags || [],
        aesthetic: classifyForSite(p.tags, p.title),
        price,
        compare_price: comparePrice,
        product_url: `https://designerwallcoverings.com/products/${p.handle}`,
        sample_url: `https://designerwallcoverings.com/products/${p.handle}#sample`,
      };
    });

  console.log(`Chinoiserie filtered: ${chinois.length}`);
  const byA = {};
  for (const p of chinois) byA[p.aesthetic] = (byA[p.aesthetic] || 0) + 1;
  console.log('By aesthetic:', JSON.stringify(byA, null, 2));

  fs.mkdirSync(path.dirname(OUT), { recursive: true });
  fs.writeFileSync(OUT, JSON.stringify(chinois, null, 2));
  console.log(`Wrote ${chinois.length} products to ${OUT}`);
})().catch(e => { console.error('FATAL:', e.message); process.exit(1); });