← back to Letsbegin

fix_dwpx_images.js

115 lines

const https = require('https');
const http = require('http');
const fs = require('fs');
const { execFileSync } = require('child_process');

const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_PRODUCT_TOKEN;
if (!TOKEN) {
  throw new Error('SHOPIFY_PRODUCT_TOKEN environment variable is required');
}

// DW Italian name → Astek English name mapping
const NAME_MAP = {
  'spazzolato': 'brushed metal wallcovering',
  'ondulato': 'corrugated wallcovering',
  'coccodrillo': 'croc wallcovering',
  'sbalzato': 'embossed metal wallcovering',
  'metallico': 'metal wallcovering',
  'tessuto metallico': 'metallic textile wallcovering',
  'trapuntato': 'quilted vinyl wallcovering',
};

function gql(body) {
  return new Promise((res, rej) => {
    const data = JSON.stringify(body);
    const req = https.request({ hostname: STORE, path: '/admin/api/2024-10/graphql.json', method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
    }, r => { let c=''; r.on('data',d=>c+=d); r.on('end',()=>res(JSON.parse(c))); });
    req.on('error', rej); req.write(data); req.end();
  });
}

function download(url, dest) {
  return new Promise((res, rej) => {
    const mod = url.startsWith('https') ? https : http;
    const file = fs.createWriteStream(dest);
    mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, r => {
      if (r.statusCode >= 300 && r.headers.location) { file.close(); return download(r.headers.location, dest).then(res).catch(rej); }
      r.pipe(file); file.on('finish', () => { file.close(); res(); });
    }).on('error', e => { file.close(); rej(e); });
  });
}

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function main() {
  console.log('=== DWPX Image Fixer (Astek Decadent) ===\n');

  // Load Astek lookup
  const astek = JSON.parse(fs.readFileSync('/tmp/astek_lookup.json'));

  // Load no-image products
  const products = JSON.parse(fs.readFileSync('/tmp/no_img_products.json'));
  console.log(`${products.length} products need images\n`);

  let ok = 0, noMatch = 0, errors = 0;

  for (let i = 0; i < products.length; i++) {
    const p = products[i];
    // Parse: "Spazzolato - Bronze Wallcovering | Phillipe Romano"
    const titleParts = p.title.split('|')[0].trim();
    const dashParts = titleParts.split(' - ');
    const dwPattern = (dashParts[0] || '').trim().toLowerCase();
    const colorPart = (dashParts[1] || '').replace(/wallcovering/gi, '').trim().toLowerCase();

    // Map DW name to Astek name
    const astekPattern = NAME_MAP[dwPattern];
    if (!astekPattern) { console.log(`  [${i+1}] ${p.title.slice(0,40)} — no pattern map for "${dwPattern}"`); noMatch++; continue; }

    const key = `${astekPattern}|${colorPart}`;
    const match = astek[key];
    if (!match || !match.img) { console.log(`  [${i+1}] ${p.title.slice(0,40)} — no Astek match for "${key}"`); noMatch++; continue; }

    // Download image
    const tmpFile = '/tmp/dwpx_img.jpg';
    try {
      await download(match.img, tmpFile);
      const fileSize = fs.statSync(tmpFile).size;

      // Staged upload
      const stage = await gql({
        query: `mutation { stagedUploadsCreate(input: [{resource: IMAGE, filename: "${match.sku}.jpg", mimeType: "image/jpeg", httpMethod: POST, fileSize: "${fileSize}"}]) { stagedTargets { url parameters { name value } resourceUrl } } }`
      });
      const target = stage?.data?.stagedUploadsCreate?.stagedTargets?.[0];
      if (!target) { errors++; continue; }

      const curlArgs = ['-s', '-X', 'POST', target.url];
      for (const param of target.parameters) curlArgs.push('-F', `${param.name}=${param.value}`);
      curlArgs.push('-F', `file=@${tmpFile}`);
      execFileSync('curl', curlArgs, { timeout: 30000 });

      // Create media
      const pid = p.id.split('/').pop();
      const r = await gql({
        query: `mutation { productCreateMedia(productId: "gid://shopify/Product/${pid}", media: [{originalSource: "${target.resourceUrl}", mediaContentType: IMAGE, alt: "${p.title.split('|')[0].trim()}"}]) { media { id } mediaUserErrors { message } } }`
      });

      if (r?.data?.productCreateMedia?.media?.[0]) {
        ok++;
        // Also set manufacturer_sku
        await gql({
          query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }',
          variables: { m: [{ ownerId: p.id, namespace: 'custom', key: 'manufacturer_sku', value: match.sku.split('__')[0], type: 'single_line_text_field' }] }
        });
        console.log(`  [${i+1}] ${p.title.slice(0,40)} → ${match.sku} ✓`);
      } else { errors++; }
    } catch (e) { errors++; console.log(`  [${i+1}] ERROR: ${e.message?.slice(0,50)}`); }

    await sleep(800);
  }

  console.log(`\n=== DONE === Images: ${ok} | No match: ${noMatch} | Errors: ${errors}`);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });