← back to Letsbegin

sequin_colors_titles.js

108 lines

const https = require('https');
const http = require('http');
const fs = require('fs');

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

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.setTimeout(30000, () => { req.destroy(); rej(new Error('timeout')); }); req.write(data); req.end();
  });
}

function gemini(reqBody) {
  return new Promise((res) => {
    const data = JSON.stringify(reqBody);
    const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`);
    const req = https.request({ hostname: url.hostname, path: url.pathname + url.search, method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
    }, r => { let c=''; r.on('data',d=>c+=d); r.on('end',()=>{ try{res(JSON.parse(c))}catch{res(null)} }); });
    req.on('error', ()=>res(null)); req.setTimeout(30000, ()=>{req.destroy();res(null)}); 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.statusCode < 400 && 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); });
  });
}

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

async function main() {
  console.log('=== Sequin Color Analysis + Title Update ===\n');
  const r = await gql({ query: '{ eel: products(first: 50, query: "eel skin sequin AND status:active") { edges { node { id title images(first:1) { edges { node { url } } } } } } sting: products(first: 50, query: "sting ray sequin AND status:active") { edges { node { id title images(first:1) { edges { node { url } } } } } } }' });

  const products = [...r.data.eel.edges.map(e => e.node), ...r.data.sting.edges.map(e => e.node)];
  // Deduplicate by ID
  const seen = new Set();
  const unique = products.filter(p => { if (seen.has(p.id)) return false; seen.add(p.id); return true; });
  console.log(`${unique.length} unique products\n`);

  let ok = 0, err = 0;
  for (let i = 0; i < unique.length; i++) {
    const p = unique[i];
    const imgUrl = p.images?.edges?.[0]?.node?.url;
    if (!imgUrl) { console.log(`[${i+1}] ${p.title} — no image, skip`); err++; continue; }

    // Download image
    const tmp = '/tmp/seq_color.jpg';
    await download(imgUrl, tmp);
    const b64 = fs.readFileSync(tmp).toString('base64');

    // Gemini: get dominant color
    const result = await gemini({
      contents: [{ parts: [
        { text: 'What is the dominant color of this sequin/glitter wallcovering? Reply with ONLY a color name like Gold, Silver, Red, Blue, Pink, Champagne, Bronze, Copper, etc. Just the color, nothing else.' },
        { inlineData: { mimeType: 'image/jpeg', data: b64 } }
      ]}],
      generationConfig: { temperature: 0.1, maxOutputTokens: 10 }
    });

    const color = (result?.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();
    if (!color || color.length > 30) { console.log(`[${i+1}] ${p.title} — bad color response`); err++; await sleep(1200); continue; }

    // Build new title: "Pattern Name - Color | Glitter Walls"
    const patternBase = p.title.split('|')[0].trim(); // "Eel Skin Sequin" or "Sting Ray Sequin"
    const newTitle = `${patternBase} - ${color} | Glitter Walls`;

    // Update title + set color metafield
    await gql({
      query: 'mutation ($i: ProductInput!) { productUpdate(input: $i) { product { id } userErrors { message } } }',
      variables: { i: { id: p.id, title: newTitle } }
    });

    await gql({
      query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }',
      variables: { m: [
        { ownerId: p.id, namespace: 'custom', key: 'color_name', value: color, type: 'single_line_text_field' },
      ]}
    });

    ok++;
    console.log(`[${i+1}/${unique.length}] ${newTitle}`);
    await sleep(1500);
  }

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