← 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); });