← back to Yolo Agent

scripts/collection-image-refresh.js

177 lines

#!/usr/bin/env node
const path = require('path');
try { require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); } catch (_) {}
/**
 * Collection Image Refresh — Update collection covers from swatches to room settings.
 * Only updates collections with swatch-only images.
 * Skips collections with < 5 products.
 */

const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const API = `https://${SHOP}/admin/api/2024-10`;
const fs = require('fs');
if (!TOKEN) {
  console.error('Missing SHOPIFY_ADMIN_TOKEN.');
  process.exit(1);
}

async function shopifyGet(url) {
  const resp = await fetch(url, {
    headers: { 'X-Shopify-Access-Token': TOKEN }
  });
  if (resp.status === 429) {
    const wait = parseFloat(resp.headers.get('Retry-After') || '4');
    await new Promise(r => setTimeout(r, (wait + 1) * 1000));
    return shopifyGet(url);
  }
  if (!resp.ok) throw new Error(`GET ${resp.status}: ${url}`);
  return resp.json();
}

async function shopifyPut(url, data) {
  await new Promise(r => setTimeout(r, 600));
  const resp = await fetch(url, {
    method: 'PUT',
    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  if (resp.status === 429) {
    const wait = parseFloat(resp.headers.get('Retry-After') || '4');
    await new Promise(r => setTimeout(r, (wait + 1) * 1000));
    return shopifyPut(url, data);
  }
  if (!resp.ok) {
    const body = await resp.text();
    throw new Error(`PUT ${resp.status}: ${body.slice(0, 200)}`);
  }
  return resp.json();
}

function isSwatchImage(imageUrl) {
  if (!imageUrl) return true; // No image = treat as swatch
  const lower = imageUrl.toLowerCase();
  // Swatch indicators: small size, "swatch" in name, "color" in name
  return lower.includes('swatch') || lower.includes('_s.') || lower.includes('color-chip');
}

async function findRoomSettingImage(collectionId) {
  // Get products from collection, look for room setting images
  try {
    const data = await shopifyGet(`${API}/collections/${collectionId}/products.json?limit=10&fields=id,images`);
    const products = data.products || [];

    for (const p of products) {
      if (!p.images || p.images.length < 2) continue;
      // Room settings are typically at position 2+ and larger
      for (let i = 1; i < p.images.length; i++) {
        const img = p.images[i];
        if (img.width >= 800 || !img.width) { // width might not be returned, still try
          return img.src;
        }
      }
    }

    // Fallback: use first product's first image if it's large enough
    for (const p of products) {
      if (p.images && p.images.length > 0) {
        const img = p.images[0];
        if (img.width >= 600 || !img.width) {
          return img.src;
        }
      }
    }
  } catch (e) {
    console.error(`  Error getting products for collection ${collectionId}: ${e.message}`);
  }
  return null;
}

async function run() {
  const logFile = path.join(__dirname, '..', 'logs', 'collection-image-refresh.log');
  let checked = 0, updated = 0, skipped = 0, noImage = 0;

  console.log(`Collection Image Refresh — Starting at ${new Date().toISOString()}`);
  fs.appendFileSync(logFile, `\n=== Run ${new Date().toISOString()} ===\n`);

  // Get all smart collections
  let sinceId = 0;
  let allCollections = [];

  while (true) {
    const data = await shopifyGet(`${API}/smart_collections.json?limit=250&since_id=${sinceId}`);
    const cols = data.smart_collections || [];
    if (cols.length === 0) break;
    allCollections = allCollections.concat(cols);
    sinceId = cols[cols.length - 1].id;
    await new Promise(r => setTimeout(r, 300));
  }

  // Also get custom collections
  sinceId = 0;
  while (true) {
    const data = await shopifyGet(`${API}/custom_collections.json?limit=250&since_id=${sinceId}`);
    const cols = data.custom_collections || [];
    if (cols.length === 0) break;
    allCollections = allCollections.concat(cols);
    sinceId = cols[cols.length - 1].id;
    await new Promise(r => setTimeout(r, 300));
  }

  console.log(`Found ${allCollections.length} total collections`);

  for (const col of allCollections) {
    checked++;

    // Skip collections with < 5 products
    if (col.products_count !== undefined && col.products_count < 5) {
      skipped++;
      continue;
    }

    // Check if current image looks like a swatch
    const currentImage = col.image?.src || '';
    if (currentImage && !isSwatchImage(currentImage)) {
      // Already has a non-swatch image
      continue;
    }

    // No image or swatch-only — try to find a room setting
    const roomSettingUrl = await findRoomSettingImage(col.id);
    if (!roomSettingUrl) {
      noImage++;
      continue;
    }

    // Determine collection type for API endpoint
    const endpoint = col.rules ? 'smart_collections' : 'custom_collections';

    try {
      await shopifyPut(`${API}/${endpoint}/${col.id}.json`, {
        [endpoint.replace(/s$/, '')]: {
          id: col.id,
          image: { src: roomSettingUrl }
        }
      });
      updated++;
      const msg = `UPDATED: ${col.id} | ${col.title} | ${roomSettingUrl.slice(-60)}`;
      console.log(msg);
      fs.appendFileSync(logFile, msg + '\n');
    } catch (e) {
      const msg = `FAIL: ${col.id} | ${col.title} | ${e.message.slice(0, 80)}`;
      console.error(msg);
      fs.appendFileSync(logFile, msg + '\n');
    }

    if (checked % 50 === 0) {
      console.log(`Progress: checked=${checked}/${allCollections.length} updated=${updated}`);
    }
  }

  const summary = `DONE: checked=${checked} updated=${updated} skipped=${skipped} noImage=${noImage}`;
  console.log(summary);
  fs.appendFileSync(logFile, summary + '\n');
}

run().catch(e => { console.error('Fatal:', e); process.exit(1); });