← back to Dw Photo Capture
Video transcode poll: /api/media-status reads Shopify video processing state; client polls after upload until every clip is READY (or fails), updating the note in-place
aa3a6cebdd423e8ba6efa90daf51de72e9945f2c · 2026-07-06 20:38:09 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit aa3a6cebdd423e8ba6efa90daf51de72e9945f2c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 20:38:09 2026 -0700
Video transcode poll: /api/media-status reads Shopify video processing state; client polls after upload until every clip is READY (or fails), updating the note in-place
---
public/index.html | 26 ++++++++++++++++++++++----
server.js | 21 +++++++++++++++++++++
2 files changed, 43 insertions(+), 4 deletions(-)
diff --git a/public/index.html b/public/index.html
index 1a5322a..5d6ac04 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1478,6 +1478,18 @@ async function uploadVideos(product_id, dwSku){ const vids=addVideosList(); let
for(let i=0;i<vids.length;i++){ $('#addNote').textContent=`🎥 Uploading video ${i+1}/${vids.length}…`;
try{ const r=await(await fetch('/api/video',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({product_id,dw_sku:dwSku,dataUrl:vids[i].dataUrl})})).json(); if(r.ok)ok++; }catch(e){} }
return ok; }
+// Shopify transcodes video async — poll until every clip is READY (or a couple minutes pass).
+async function pollVideoStatus(product_id, baseMsg){ const started=Date.now();
+ while(Date.now()-started < 150000){
+ await new Promise(r=>setTimeout(r,5000));
+ let s; try{ s=await(await fetch('/api/media-status?product_id='+encodeURIComponent(product_id))).json(); }catch(e){ continue; }
+ if(!s || !s.ok) continue;
+ if(s.all_ready){ $('#addNote').innerHTML=`${baseMsg} · 🎥 ${s.ready} video(s) ready ✓`; toast('🎥 Videos ready on Shopify'); return; }
+ if(s.failed){ $('#addNote').innerHTML=`${baseMsg} · ⚠ ${s.failed} video(s) failed to process`; return; }
+ $('#addNote').innerHTML=`${baseMsg} · 🎥 transcoding ${s.ready}/${s.total}…`;
+ }
+ $('#addNote').innerHTML=`${baseMsg} · 🎥 still transcoding — check Shopify shortly.`;
+}
async function addPreview(){ if(_addMode==='update') return addUpdate();
const p=addPayload(false); if(!p.mfr||!p.vendor){ $('#addNote').textContent='Mfr# and Vendor are required.'; return; }
$('#addNote').textContent='Previewing…'; $('#addResult').innerHTML='';
@@ -1491,8 +1503,11 @@ async function addPreview(){ if(_addMode==='update') return addUpdate();
async function addCommit(){ const b=$('#addCommit'); b.disabled=true; $('#addNote').textContent='Creating draft…';
const r=await(await fetch('/api/create-item',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(addPayload(true))})).json();
if(!r.ok){ $('#addNote').textContent='✗ '+(r.err||'failed'); b.disabled=false; return; }
- let vmsg=''; if(addVideosList().length && r.product_id){ const n=await uploadVideos(r.product_id, r.dw_sku); vmsg=` + ${n} video(s)`; }
- $('#addNote').innerHTML=`✅ Created DRAFT · ${r.dw_sku}${vmsg} <small>— not live; publish in Shopify when ready</small>`; toast('✅ Draft created: '+r.dw_sku);
+ const hadVideos=addVideosList().length; let vmsg='';
+ if(hadVideos && r.product_id){ const n=await uploadVideos(r.product_id, r.dw_sku); vmsg=` + ${n} video(s)`; }
+ const baseMsg=`✅ Created DRAFT · ${r.dw_sku}${vmsg}`;
+ $('#addNote').innerHTML=`${baseMsg} <small>— not live; publish in Shopify when ready</small>`; toast('✅ Draft created: '+r.dw_sku);
+ if(hadVideos && r.product_id) pollVideoStatus(r.product_id, baseMsg);
}
// UPDATE mode: resolve the existing product from the scanned mfr#, then push media LIVE.
async function addResolveUpdate(){ const mfr=$('#addMfr').value.trim(); if(!mfr) return;
@@ -1505,8 +1520,11 @@ async function addUpdate(){ if(!_updatePid) await addResolveUpdate();
const photos=addPhotosList(); if(!photos.length && !addVideosList().length){ $('#addNote').textContent='Add at least one photo or video.'; return; }
$('#addNote').textContent='⬆ Pushing to Shopify (LIVE)…'; let added=0;
if(photos.length){ const r=await(await fetch('/api/photos',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dw_sku:_updateSku,product_id:_updatePid,dataUrls:photos})})).json(); added=r.added||0; }
- const vn=addVideosList().length ? await uploadVideos(_updatePid,_updateSku) : 0;
- $('#addNote').innerHTML=`✅ LIVE: ${added} photo(s) + ${vn} video(s) → ${_updateSku}`; toast('✅ Updated '+_updateSku+' live');
+ const hadVideos=addVideosList().length;
+ const vn=hadVideos ? await uploadVideos(_updatePid,_updateSku) : 0;
+ const baseMsg=`✅ LIVE: ${added} photo(s) + ${vn} video(s) → ${_updateSku}`;
+ $('#addNote').innerHTML=baseMsg; toast('✅ Updated '+_updateSku+' live');
+ if(hadVideos) pollVideoStatus(_updatePid, baseMsg);
}
$('#addBtn').addEventListener('click',()=>openAddModal());
$('#addClose').addEventListener('click',()=>{ $('#addModal').hidden=true; document.body.style.overflow=''; });
diff --git a/server.js b/server.js
index a5fe4f5..5842d9a 100644
--- a/server.js
+++ b/server.js
@@ -176,6 +176,18 @@ async function shopifyAddVideo(productId, buf, filename, mime) {
return { ok: true, processing: true }; // Shopify transcodes async
}
+// Poll a product's VIDEO media processing state (Shopify transcodes async: UPLOADED→PROCESSING→READY|FAILED).
+async function shopifyMediaStatus(productId) {
+ const q = `query($id:ID!){ product(id:$id){ media(first:50){ edges{ node{ mediaContentType status } } } } }`;
+ const d = await gql(q, { id: `gid://shopify/Product/${productId}` });
+ const nodes = ((d.data && d.data.product && d.data.product.media && d.data.product.media.edges) || []).map(e => e.node);
+ const vids = nodes.filter(n => n.mediaContentType === 'VIDEO');
+ const ready = vids.filter(v => v.status === 'READY').length;
+ const failed = vids.filter(v => v.status === 'FAILED').length;
+ const processing = vids.length - ready - failed;
+ return { total: vids.length, ready, processing, failed, all_ready: vids.length > 0 && processing === 0 && failed === 0 };
+}
+
// After a photo lands, make the SKU live IF it passes the activation gate
// (now has image + a real price + width + a description). Else stays draft.
async function shopifyActivateIfReady(productId) {
@@ -1448,6 +1460,15 @@ const appHandler = (req, res) => {
return;
}
+ // Poll a product's video transcode state (Shopify processes uploaded video async).
+ if (u.pathname === '/api/media-status' && req.method === 'GET') {
+ const pid = (u.searchParams.get('product_id') || '').trim();
+ if (!pid) return send(res, 400, { err: 'product_id required' });
+ shopifyMediaStatus(pid).then(s => send(res, 200, Object.assign({ ok: true }, s)))
+ .catch(e => send(res, 200, { ok: false, err: e.message }));
+ return;
+ }
+
if (u.pathname === '/api/photo' && req.method === 'POST') {
let body = '';
req.on('data', c => { body += c; if (body.length > 25 * 1024 * 1024) req.destroy(); });
← 4a51d5b Camera-first capture flow + 10-photo/6-video gallery: pill→c
·
back to Dw Photo Capture
·
5x: 2 consecutive clean sweeps (0 app defects) — six-way + f 942ce16 →