← back to Dw Image Shrink
enum-videos.js
69 lines
// READ-ONLY. Enumerate every product-attached Video → videos-inventory.jsonl.
// Paginates products (250/page) and pulls their Video media (first:10). Writes one
// JSONL line per Video with product linkage + original source size, so the worker
// knows what to re-encode and where to re-attach it. Safe to run any time (no writes).
// node enum-videos.js [--limit-pages N]
import fs from 'fs';
const OUT='/Users/macstudio3/Projects/dw-image-shrink/videos-inventory.jsonl';
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 arg=(k,d)=>{const a=process.argv.find(x=>x.startsWith('--'+k+'='));return a?a.split('=').slice(1).join('='):d;};
const MAXPAGES=+arg('limit-pages','0')||1e9;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(query,variables){
for(let a=0;a<12;a++){
const r=await fetch(`https://${SHOP}/admin/api/2024-10/graphql.json`,{method:'POST',
headers:{'X-Shopify-Access-Token':ATOK,'Content-Type':'application/json'},
body:JSON.stringify({query,variables})});
if(r.status===429||r.status>=500){await sleep(2000);continue;}
const j=await r.json();
if(j.errors&&JSON.stringify(j.errors).includes('THROTTLED')){await sleep(2500);continue;}
if(j.errors)throw new Error(JSON.stringify(j.errors).slice(0,300));
// gentle pacing on the leaky bucket
const cost=j.extensions&&j.extensions.cost&&j.extensions.cost.throttleStatus;
if(cost&&cost.currentlyAvailable<400)await sleep(1200); else await sleep(300);
return j.data;
}
throw new Error('gql: exhausted retries');
}
const Q=`query($cursor:String){
products(first:250, after:$cursor){
pageInfo{hasNextPage endCursor}
nodes{
id handle status
media(first:10){nodes{
... on Video { id filename status
originalSource{ url fileSize format mimeType width height } }
}}
}
}
}`;
let cursor=null, pages=0, prods=0, vids=0, bytes=0;
const ws=fs.createWriteStream(OUT);
while(pages<MAXPAGES){
const d=await gql(Q,{cursor});
const p=d.products;
for(const n of p.nodes){
prods++;
const pid=Number(n.id.split('/').pop());
for(const m of (n.media?.nodes||[])){
if(!m||!m.id||!m.id.includes('/Video/'))continue;
const os=m.originalSource||{};
if(!os.url)continue; // no downloadable original yet (still processing) → skip
vids++; bytes+=(os.fileSize||0);
ws.write(JSON.stringify({product_id:pid,handle:n.handle,prod_status:n.status,
media_id:m.id,media_status:m.status,orig_url:os.url,orig_bytes:os.fileSize||0,
orig_format:os.format||'',orig_width:os.width||0,orig_height:os.height||0})+'\n');
}
}
pages++;
if(pages%10===0)console.log(` ...${pages} pages | ${prods} products | ${vids} videos | ${(bytes/1073741824).toFixed(2)} GB`);
if(!p.pageInfo.hasNextPage)break;
cursor=p.pageInfo.endCursor;
}
ws.end();
console.log(`DONE: ${pages} pages, ${prods} products scanned, ${vids} product-videos → ${OUT} | total original bytes ${(bytes/1073741824).toFixed(2)} GB`);