[object Object]

← back to Norma

IG: duplicate-image detector — md5-hash each post's image per account, flag OLDEST of each dup set for deletion (feeds delete cockpit; deletion stays manual)

eb6bf21b1daf45d6b2cb226b96dd4fc5fd15bafc · 2026-08-15 07:52:09 -0700 · Steve

Files touched

Diff

commit eb6bf21b1daf45d6b2cb226b96dd4fc5fd15bafc
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Aug 15 07:52:09 2026 -0700

    IG: duplicate-image detector — md5-hash each post's image per account, flag OLDEST of each dup set for deletion (feeds delete cockpit; deletion stays manual)
---
 agents/instagram-agent/find-dup-images.js | 43 +++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)

diff --git a/agents/instagram-agent/find-dup-images.js b/agents/instagram-agent/find-dup-images.js
new file mode 100644
index 0000000..1ea0d83
--- /dev/null
+++ b/agents/instagram-agent/find-dup-images.js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+/**
+ * find-dup-images.js <account> — find posts sharing the SAME image and mark the OLDEST for deletion.
+ * Instagram exposes no stable image id, so we download each media's image and md5-hash the bytes.
+ * Groups identical hashes; keeps the NEWEST, lists older ones as delete-candidates. Deletion itself
+ * is manual (Graph API can't delete; openclaw is IG-checkpointed) — output feeds the delete cockpit.
+ *   node find-dup-images.js <account> [--limit N]
+ */
+const accounts = require('./accounts');
+const crypto = require('crypto'); const https = require('https');
+const acct = accounts.resolve((process.argv[2] || 'designerwallcoverings').replace(/^@/, ''));
+if (!acct) { console.error('unknown account'); process.exit(1); }
+const { ig_user_id: ID, access_token: T, graph_host: H, graph_version: V, handle } = acct;
+const LIMIT = parseInt((() => { const i = process.argv.indexOf('--limit'); return i >= 0 ? process.argv[i + 1] : '80'; })(), 10);
+const g = async (u) => (await fetch(u)).json();
+const md5 = (url) => new Promise((res) => { const h = crypto.createHash('md5'); https.get(url, (r) => { r.on('data', (d) => h.update(d)); r.on('end', () => res(h.digest('hex'))); }).on('error', () => res(null)); });
+
+(async () => {
+  let after = '', media = [];
+  while (media.length < LIMIT) {
+    const j = await g(`${H}/${V}/${ID}/media?fields=media_url,permalink,timestamp,media_type,caption&limit=50${after ? '&after=' + after : ''}&access_token=${encodeURIComponent(T)}`);
+    if (!j.data || !j.data.length) break;
+    media.push(...j.data.filter((m) => m.media_type === 'IMAGE' && m.media_url));
+    if (!j.paging || !j.paging.cursors || !j.paging.cursors.after) break;
+    after = j.paging.cursors.after;
+  }
+  media = media.slice(0, LIMIT);
+  process.stderr.write(`@${handle}: hashing ${media.length} image posts...\n`);
+  for (const m of media) m.hash = await md5(m.media_url);
+  const byHash = {};
+  for (const m of media) { if (!m.hash) continue; (byHash[m.hash] = byHash[m.hash] || []).push(m); }
+  const dupGroups = Object.values(byHash).filter((g) => g.length > 1);
+  let toDelete = [];
+  for (const grp of dupGroups) {
+    grp.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); // oldest first
+    const keep = grp[grp.length - 1]; const older = grp.slice(0, -1);
+    console.log(`\nDUPLICATE (${grp.length}x): "${(keep.caption || '').split('\n')[0].slice(0, 45)}"`);
+    console.log(`  KEEP newest: ${keep.timestamp.slice(0, 10)} ${keep.permalink}`);
+    older.forEach((o) => { console.log(`  ✗ DELETE older: ${o.timestamp.slice(0, 10)} ${o.permalink}`); toDelete.push(o.permalink); });
+  }
+  console.log(`\n@${handle}: ${dupGroups.length} duplicate set(s), ${toDelete.length} older post(s) to delete.`);
+  if (toDelete.length) require('fs').appendFileSync('data/dup-delete-hitlist.jsonl', toDelete.map((p) => JSON.stringify({ handle, permalink: p, reason: 'older duplicate image' })).join('\n') + '\n');
+})().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });

← d822b58 IG posts: 2-slide carousel — room-setting photo first, produ  ·  back to Norma  ·  IG cadence: dedup by IMAGE filename (not just product handle 191eee1 →