← back to Dw Image Shrink

stout-reattach.js

45 lines

// Now that the cap is cleared: re-attach the real estout image to each of the 79
// gated Stout products and reactivate. estout images are EXTERNAL (cdn.estout.com,
// GC-safe). Attaches by src (full-res = stored thumbnail url minus query params).
// Only flips status→active if the image attached. Logged to stout-reattached.jsonl.
// DRY-RUN by default; --apply to write. --limit N for a canary.
import fs from 'fs';
import https from 'https';
import pg from 'pg';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const ATOK=fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=').slice(1).join('=').replace(/["' ]/g,'');
const DIR='/Users/macstudio3/Projects/dw-image-shrink';
const APPLY=process.argv.includes('--apply');
const arg=(k,d)=>{const a=process.argv.find(x=>x.startsWith('--'+k+'='));return a?a.split('=').slice(1).join('='):d;};
const LIMIT=+arg('limit','0')||1e9;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function raw(m,p,b){return new Promise((res,rej)=>{const data=b?JSON.stringify(b):null;const req=https.request({method:m,host:SHOP,path:p,headers:{'X-Shopify-Access-Token':ATOK,'Content-Type':'application/json',...(data?{'Content-Length':Buffer.byteLength(data)}:{})}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>res({status:r.statusCode,json:(()=>{try{return JSON.parse(d)}catch(e){return d}})()}));});req.on('error',rej);if(data)req.write(data);req.end();});}
async function api(m,p,b){for(let a=0;a<6;a++){const r=await raw(m,p,b);if(r.status===429){await sleep(3000);continue;}return r;}return{status:429,json:{}};}

const gated=fs.readFileSync('/Users/macstudio3/dw-infinite-scroll-debug/stout-gated-imageless.jsonl','utf8').split('\n').filter(Boolean).map(l=>JSON.parse(l));
const ids=gated.map(g=>String(g.id));
const db=new pg.Client({host:'/tmp',database:'dw_unified'}); await db.connect();
const {rows}=await db.query(`SELECT shopify_product_id, image_url FROM stout_catalog WHERE shopify_product_id = ANY($1)`,[ids]);
const imgById={}; for(const r of rows) imgById[String(r.shopify_product_id)]=r.image_url;
await db.end();

const done=new Set(); if(fs.existsSync(DIR+'/stout-reattached.jsonl')) for(const l of fs.readFileSync(DIR+'/stout-reattached.jsonl','utf8').split('\n')) if(l){try{done.add(String(JSON.parse(l).id))}catch(e){}}
const work=gated.filter(g=>imgById[String(g.id)]&&!done.has(String(g.id))).slice(0,LIMIT);
console.log(`${APPLY?'APPLYING':'DRY-RUN'}: ${work.length}/${gated.length} gated Stout products have an estout image to re-attach`);
if(!APPLY){ console.log('sample:', work.slice(0,3).map(g=>g.handle+' → '+(imgById[String(g.id)]||'').split('?')[0])); process.exit(0); }

const out=fs.createWriteStream(DIR+'/stout-reattached.jsonl',{flags:'a'});
let ok=0,err=0;
for(const g of work){
  const fullres=(imgById[String(g.id)]||'').split('?')[0];   // strip thumbnail params → full-res
  const up=await api('POST',`/admin/api/2024-10/products/${g.id}/images.json`,{image:{src:fullres}});
  if(up.status>=300||!up.json.image){ err++; console.log('  ✗',g.handle,up.status,JSON.stringify(up.json).slice(0,80)); await sleep(300); continue; }
  await sleep(300);
  const act=await api('PUT',`/admin/api/2024-10/products/${g.id}.json`,{product:{id:g.id,status:'active'}});
  out.write(JSON.stringify({id:g.id,handle:g.handle,image:fullres,image_id:up.json.image.id,reactivated:act.status<300,ts:new Date().toISOString()})+'\n');
  ok++; if(ok%20===0) console.log(`  re-attached+activated ${ok}`);
  await sleep(300);
}
out.end();
console.log(`DONE: ${ok} Stout products re-imaged + reactivated, ${err} errors.`);