← back to Dw Image Shrink

enum-attached-media.js

43 lines

// Build the "referenced universe" from PRODUCTS (GraphQL, products token):
//   (1) attached-gids.txt   — numeric ids of every MediaImage attached to a product
//   (2) referenced-urls.txt — normalized basenames of every cdn.shopify file URL
//                             embedded inline in any product descriptionHtml
// These two sets are what an orphan must be ABSENT from. READ-ONLY.
import fs from 'fs';
import https from 'https';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const TOK=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 sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(query){return new Promise((res,rej)=>{const body=JSON.stringify({query});const req=https.request({method:'POST',host:SHOP,path:'/admin/api/2024-10/graphql.json',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}},r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{try{res(JSON.parse(d))}catch(e){res({errors:[{message:d.slice(0,120)}]})}});});req.on('error',rej);req.write(body);req.end();});}
// normalize a shopify cdn url → stable basename (drop query, drop _WxH size suffix)
export function norm(u){ if(!u) return ''; let s=u.split('?')[0].split('/').pop(); s=s.replace(/_\d+x\d+(?=\.[a-z0-9]+$)/i,''); return s.toLowerCase(); }
const URLRE=/https?:\/\/cdn\.shopify\.com\/[^\s"'<>)]+/gi;

const Q=(c)=>`{ products(first: 40${c?`, after: "${c}"`:''}) { pageInfo{ hasNextPage endCursor } edges { node { id descriptionHtml media(first: 30) { edges { node { ... on MediaImage { id } } } } } } } }`;

const gidOut=fs.createWriteStream(DIR+'/attached-gids.txt');
const urlSet=new Set();
let cursor=null, page=0, prods=0, gids=0;
const t0=Date.now();
while(true){
  let r; for(let a=0;a<6;a++){ r=await gql(Q(cursor)); if(r.errors&&/throttl/i.test(JSON.stringify(r.errors))){await sleep(2000);continue;} break; }
  if(r.errors){ console.error('ERR',JSON.stringify(r.errors).slice(0,160)); break; }
  const conn=r.data.products;
  let buf='';
  for(const e of conn.edges){ prods++;
    for(const m of e.node.media.edges){ const id=m.node&&m.node.id; if(id){ buf+=id.split('/').pop()+'\n'; gids++; } }
    const html=e.node.descriptionHtml||''; const mm=html.match(URLRE); if(mm) for(const u of mm) urlSet.add(norm(u));
  }
  if(buf) gidOut.write(buf);
  page++;
  if(page%50===0) console.log(`page ${page} | products ${prods} | attached-gids ${gids} | inline-urls ${urlSet.size} | ${((Date.now()-t0)/1000).toFixed(0)}s`);
  if(!conn.pageInfo.hasNextPage) break;
  cursor=conn.pageInfo.endCursor;
  const c=r.extensions&&r.extensions.cost&&r.extensions.cost.throttleStatus;
  await sleep(c && c.currentlyAvailable<600 ? 900 : 120);
}
gidOut.end();
fs.writeFileSync(DIR+'/referenced-urls.txt',[...urlSet].join('\n'));
console.log(`DONE: ${prods} products | ${gids} attached media gids | ${urlSet.size} inline-referenced urls | ${((Date.now()-t0)/1000).toFixed(0)}s`);