← 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
A agents/instagram-agent/find-dup-images.js
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 →