[object Object]

← back to Dw Launches

Media: dedup grid — collapse colorway/reupload duplicates to one tile per pattern (Zoffany 303 SKUs -> 18 patterns)

142baf6c200e5bf7f3e7e2428e9f36c3c059b49a · 2026-06-20 10:42:23 -0700 · Steve

Files touched

Diff

commit 142baf6c200e5bf7f3e7e2428e9f36c3c059b49a
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 20 10:42:23 2026 -0700

    Media: dedup grid — collapse colorway/reupload duplicates to one tile per pattern (Zoffany 303 SKUs -> 18 patterns)
---
 public/index.html               |  39 +++++++++++++++++++++++++++++++++++++--
 screenshot-zoffany-deduped.png  | Bin 0 -> 779334 bytes
 screenshot-zoffany-dups.png     | Bin 0 -> 719486 bytes
 screenshot-zoffany-line.png     | Bin 0 -> 637087 bytes
 screenshot-zoffany-patterns.png | Bin 0 -> 907049 bytes
 5 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index b0ec7ab..bf489d5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1667,8 +1667,34 @@ async function mlLoadSources(){
   }
 }
 
+// Normalised image identity for dedup. Collapses the same photo re-uploaded
+// once per colorway/variant — Shopify suffixes a 4-hex hash to the filename
+// (Y_Shagreen_WALLPAPER_e3e1.jpg, _f1c1.jpg, _3347.jpg…), so strip that +
+// any ?v= cache-buster and key on the base filename.
+function mlImgKey(u){
+  if(!u) return u;
+  try{ u=decodeURIComponent(u.replace(/^\/api\/media\/img\?u=/,'')); }catch(e){}
+  u=u.split('?')[0];
+  const base=(u.split('/').pop()||u).toLowerCase();
+  return base.replace(/_[0-9a-f]{4}\.(jpg|jpeg|png|webp|gif)$/,'') || base;
+}
+// Dedup key per media item. For Shopify products, collapse a whole PATTERN to
+// one tile (colorways/shots like "Darcy - Smoked Pearl", "Darcy - Vine Black",
+// the _01/_02 reshoots all reuse the same room photo) by keying on
+// vendor + pattern-name (title before the first " - " / " | "). Other sources
+// (IG posts) have no such convention → fall back to image identity.
+function mlItemKey(m){
+  if(m.source==='shopify' && m.title){
+    const pattern=m.title.split(/\s[-|]\s/)[0].trim().toLowerCase().replace(/\s+/g,' ');
+    if(pattern) return 'p:'+(m.vendor||'').toLowerCase()+'|'+pattern;
+  }
+  return 'i:'+mlImgKey(m.url);
+}
+
 function mlReset(){
   mlState.offset=0;
+  mlState.seen=new Set();
+  mlState.dups=0;
   document.getElementById('ml-grid').innerHTML='';
   document.getElementById('ml-status').textContent='';
   mediaLoadMore();
@@ -1683,9 +1709,18 @@ async function mediaLoadMore(){
     const qs=new URLSearchParams({source:mlState.source,q:mlState.q,limit:mlState.limit,offset:mlState.offset});
     const d=await api('/api/media?'+qs.toString());
     const grid=document.getElementById('ml-grid');
-    (d.items||[]).forEach(m=>grid.appendChild(mlCard(m)));
+    if(!mlState.seen) mlState.seen=new Set();
+    let added=0;
+    (d.items||[]).forEach(m=>{
+      const k=mlItemKey(m);
+      if(mlState.seen.has(k)){ mlState.dups=(mlState.dups||0)+1; return; } // skip duplicate pattern/image
+      mlState.seen.add(k);
+      grid.appendChild(mlCard(m)); added++;
+    });
     mlState.offset += (d.items||[]).length;
     btn.style.display = d.hasMore ? '' : 'none';
+    // if a whole page collapsed to duplicates, keep pulling so the grid grows
+    if(added===0 && d.hasMore && mlState.offset < 600){ mlState.loading=false; return mediaLoadMore(); }
     if(grid.children.length===0){
       if(mlState.source==='vendor' && !mlState.q){
         grid.innerHTML=`<div class="ml-empty">No vendor Instagram posts ingested yet.<br>`+
@@ -1695,7 +1730,7 @@ async function mediaLoadMore(){
         grid.innerHTML=`<div class="ml-empty muted">No matches.</div>`;
       }
     }
-    status.textContent = (grid.querySelectorAll('.ml-card').length).toLocaleString()+' shown';
+    status.textContent = (grid.querySelectorAll('.ml-card').length).toLocaleString()+' unique image'+(grid.querySelectorAll('.ml-card').length===1?'':'s')+' shown'+(mlState.dups?` · ${mlState.dups.toLocaleString()} duplicate${mlState.dups===1?'':'s'} hidden`:'');
   }catch(e){ status.textContent='Error: '+e.message; }
   finally{ mlState.loading=false; btn.disabled=false; }
 }
diff --git a/screenshot-zoffany-deduped.png b/screenshot-zoffany-deduped.png
new file mode 100644
index 0000000..d2a92ac
Binary files /dev/null and b/screenshot-zoffany-deduped.png differ
diff --git a/screenshot-zoffany-dups.png b/screenshot-zoffany-dups.png
new file mode 100644
index 0000000..9a456d9
Binary files /dev/null and b/screenshot-zoffany-dups.png differ
diff --git a/screenshot-zoffany-line.png b/screenshot-zoffany-line.png
new file mode 100644
index 0000000..3fd7940
Binary files /dev/null and b/screenshot-zoffany-line.png differ
diff --git a/screenshot-zoffany-patterns.png b/screenshot-zoffany-patterns.png
new file mode 100644
index 0000000..7ffb7bf
Binary files /dev/null and b/screenshot-zoffany-patterns.png differ

← b268328 Media: ?media=<query> deep-link opens Vendor IG tab pre-filt  ·  back to Dw Launches  ·  Media: dedup is now a toggle (Show: All colorways [default] 7533052 →