← back to Letsbegin

sequin_monty_noimages.js

153 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.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('=== Sequin Full Monty (NO room settings) ===\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 } } } metafields(first: 3, keys:["custom.color_details","custom.width"]) { edges { node { key value } } } } } } sting: products(first: 50, query: "sting ray sequin AND status:active") { edges { node { id title images(first:1) { edges { node { url } } } metafields(first: 3, keys:["custom.color_details","custom.width"]) { edges { node { key value } } } } } } }' });

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

  let colors = 0, specs = 0, bodies = 0, errors = 0;

  for (let i = 0; i < products.length; i++) {
    const p = products[i];
    const imgUrl = p.images?.edges?.[0]?.node?.url;
    const hasColors = p.metafields?.edges?.some(m => m.node.key === 'color_details' && m.node.value);
    const hasWidth = p.metafields?.edges?.some(m => m.node.key === 'width' && m.node.value);

    console.log(`[${i+1}/${products.length}] ${p.title.slice(0,40)}`);

    // 1. AI COLOR ANALYSIS (if missing)
    if (!hasColors && imgUrl) {
      try {
        const tmp = '/tmp/seq_mc.jpg';
        await download(imgUrl, tmp);
        const b64 = fs.readFileSync(tmp).toString('base64');

        const cr = await gemini({
          contents: [{ parts: [
            { text: 'Analyze this wallcovering. Return ONLY valid JSON: {"dominantColor":"name","dominantHex":"#XXXXXX","backgroundColor":"name","backgroundHex":"#XXXXXX","colors":[{"name":"Color","hex":"#XXXXXX","percentage":40}]}. 3-6 colors summing to 100%. Title Case.' },
            { inlineData: { mimeType: 'image/jpeg', data: b64 } }
          ]}],
          generationConfig: { temperature: 0.2, maxOutputTokens: 500 }
        });

        const text = (cr?.candidates?.[0]?.content?.parts?.[0]?.text || '').replace(/```json?\s*/g,'').replace(/```/g,'').trim();
        const cd = JSON.parse(text);

        await gql({
          query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }',
          variables: { m: [
            { ownerId: p.id, namespace: 'custom', key: 'color_hex', value: cd.dominantHex, type: 'single_line_text_field' },
            { ownerId: p.id, namespace: 'custom', key: 'background_color', value: cd.backgroundColor, type: 'single_line_text_field' },
            { ownerId: p.id, namespace: 'custom', key: 'color_details', value: JSON.stringify(cd.colors), type: 'json' },
          ]}
        });
        colors++;
        console.log(`  Color: ${cd.dominantColor} (${cd.dominantHex})`);
      } catch (e) { console.log(`  Color: error`); errors++; }
      await sleep(1200);
    } else { colors++; }

    // 2. SPECS (if missing)
    if (!hasWidth) {
      await gql({
        query: 'mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message } } }',
        variables: { m: [
          { ownerId: p.id, namespace: 'custom', key: 'width', value: '45 Inches', type: 'single_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'material', value: 'Sequin Wallcovering', type: 'multi_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'fire_rating', value: 'ASTM E84-98 Class A Rated', type: 'single_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'packaging', value: '11 Yard Bolts Only', type: 'single_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'unit_of_measure', value: 'Sold in 11 Yard Bolts Only', type: 'single_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'brand', value: 'Glitter Walls', type: 'single_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'match_type', value: 'Random Match', type: 'single_line_text_field' },
          { ownerId: p.id, namespace: 'custom', key: 'pattern_repeat', value: 'N/A', type: 'single_line_text_field' },
        ]}
      });
      specs++;
    }

    // 3. BODY (2 paragraphs)
    const colorName = p.title.split(' - ')[1]?.split(' |')[0]?.trim() || 'Shimmer';
    const patternBase = p.title.includes('Eel') ? 'Eel Skin Sequin' : 'Sting Ray Sequin';
    const firstPara = `${patternBase} in ${colorName} is a luxurious sequin wallcovering by Glitter Walls. This eye-catching design delivers dramatic shimmer and light-catching texture to any interior.`;

    let aiDesc = '';
    if (imgUrl) {
      try {
        const b64 = fs.readFileSync('/tmp/seq_mc.jpg').toString('base64');
        const dr = await gemini({
          contents: [{ parts: [
            { text: `Describe this ${colorName} sequin wallcovering in one sentence for a design-trade buyer. Focus on the texture, light play, and best commercial/residential uses. Do NOT say "wallpaper".` },
            { inlineData: { mimeType: 'image/jpeg', data: b64 } }
          ]}],
          generationConfig: { temperature: 0.3, maxOutputTokens: 100 }
        });
        aiDesc = (dr?.candidates?.[0]?.content?.parts?.[0]?.text || '').replace(/wallpaper/gi, 'wallcovering').trim();
      } catch {}
      await sleep(1200);
    }
    if (!aiDesc) aiDesc = 'A stunning decorative wallcovering perfect for feature walls in hospitality, retail, and high-end residential settings.';

    const newBody = `<p>${firstPara}</p>\n<p>${aiDesc}</p>`;
    await gql({
      query: 'mutation ($i: ProductInput!) { productUpdate(input: $i) { product { id } userErrors { message } } }',
      variables: { i: { id: p.id, bodyHtml: newBody } }
    });
    bodies++;

    await sleep(500);
    if ((i+1) % 10 === 0) console.log(`  --- ${i+1}/${products.length} done ---`);
  }

  console.log(`\n=== COMPLETE ===`);
  console.log(`Colors: ${colors} | Specs: ${specs} | Bodies: ${bodies} | Errors: ${errors}`);
}
main().catch(e => { console.error('Fatal:', e); process.exit(1); });