← back to Designer Wallcoverings

onboarding/sangetsu-lilycolor/lily-image-fetch.cjs

93 lines

#!/usr/bin/env node
/**
 * Selective Lilycolor swatch fetcher — random-access ONE image out of a remote
 * 100–400MB catalog zip via HTTP Range, using offsets from the zip central directory.
 * Pulls the PATTERN swatch (_C, else _P/_SP) per SKU. JPEGs are STORED (method 0) in
 * these zips, so we write the bytes straight out (deflate path handled just in case).
 *
 * Lands images on the HENRY 2TB drive (internal disk is tight) — idempotent (skips
 * already-fetched). Read-only network; no Shopify/dw_unified/publish.
 *   node lily-image-fetch.cjs [limit]   (default: all manifest SKUs lacking a fetched swatch)
 */
const https = require('https');
const zlib = require('zlib');
const fs = require('fs');
const path = require('path');

const DIR = path.join(__dirname, 'staging');
const MANIFEST = path.join(DIR, 'lilycolor-image-manifest.json');
const OUT_DIR = '/Volumes/Henry/dw-lily-images';
const TAIL = 4 * 1024 * 1024;
const KIND_PREF = { C: 0, P: 1, SP: 2, R: 3 };

function getBuf(url, headers) {
  return new Promise((resolve, reject) => {
    https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 Chrome/120', ...headers } }, (res) => {
      if (res.statusCode >= 400) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
      const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => resolve(Buffer.concat(chunks)));
    }).on('error', reject);
  });
}

// Parse the central directory tail -> map filename -> {offset, compSize, method}
function parseCentralDir(buf) {
  const map = {};
  for (let i = 0; i + 46 <= buf.length; i++) {
    if (buf.readUInt32LE(i) !== 0x02014b50) continue;
    const method = buf.readUInt16LE(i + 10);
    const compSize = buf.readUInt32LE(i + 20);
    const nameLen = buf.readUInt16LE(i + 28);
    const extraLen = buf.readUInt16LE(i + 30);
    const commentLen = buf.readUInt16LE(i + 32);
    const offset = buf.readUInt32LE(i + 42);
    if (nameLen === 0 || nameLen > 300 || i + 46 + nameLen > buf.length) continue;
    const name = buf.slice(i + 46, i + 46 + nameLen).toString('utf8').split('/').pop();
    if (/\.jpe?g$/i.test(name)) map[name] = { offset, compSize, method };
    i += 46 + nameLen + extraLen + commentLen - 1;
  }
  return map;
}

async function fetchFile(zipUrl, rec) {
  // local header: 30 fixed + nameLen + extraLen, then data (compSize). Range over the lot (+slack for extra).
  const start = rec.offset;
  const end = rec.offset + 30 + 300 + 64 + rec.compSize; // generous slack
  const buf = await getBuf(zipUrl, { Range: `bytes=${start}-${end}` });
  if (buf.readUInt32LE(0) !== 0x04034b50) throw new Error('bad local header');
  const nameLen = buf.readUInt16LE(26), extraLen = buf.readUInt16LE(28);
  const dataStart = 30 + nameLen + extraLen;
  const data = buf.slice(dataStart, dataStart + rec.compSize);
  return rec.method === 8 ? zlib.inflateRawSync(data) : data;
}

(async () => {
  if (!fs.existsSync('/Volumes/Henry')) { console.error('Henry drive not mounted at /Volumes/Henry'); process.exit(1); }
  fs.mkdirSync(OUT_DIR, { recursive: true });
  const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
  const limit = parseInt(process.argv[2] || '999999', 10);

  // group target files by zip so we read each central directory once
  const cdCache = {};
  const targets = [];        // {sku, zipUrl, name}
  for (const [sku, imgs] of Object.entries(manifest)) {
    if (fs.existsSync(path.join(OUT_DIR, `${sku}.jpg`))) continue;       // idempotent
    const best = [...imgs].sort((a, b) => (KIND_PREF[a.kind] ?? 9) - (KIND_PREF[b.kind] ?? 9))[0];
    if (best) targets.push({ sku, zipUrl: best.zipUrl, name: best.name });
    if (targets.length >= limit) break;
  }
  console.error(`fetching ${targets.length} swatches -> ${OUT_DIR}`);
  let ok = 0, fail = 0;
  for (const t of targets) {
    try {
      if (!cdCache[t.zipUrl]) cdCache[t.zipUrl] = parseCentralDir(await getBuf(t.zipUrl, { Range: `bytes=-${TAIL}` }));
      const rec = cdCache[t.zipUrl][t.name];
      if (!rec) { fail++; continue; }
      const bytes = await fetchFile(t.zipUrl, rec);
      fs.writeFileSync(path.join(OUT_DIR, `${t.sku}.jpg`), bytes);
      ok++;
      if (ok % 50 === 0) process.stderr.write(`  ${ok}/${targets.length}\n`);
    } catch (e) { fail++; if (fail <= 5) process.stderr.write(`FAIL ${t.sku}: ${e.message}\n`); }
  }
  console.log(JSON.stringify({ fetched: ok, failed: fail, dir: OUT_DIR }, null, 2));
})().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });