-
-
Notifications
You must be signed in to change notification settings - Fork 89
Expand file tree
/
Copy pathaudienceOverlap.js
More file actions
142 lines (125 loc) · 7.1 KB
/
Copy pathaudienceOverlap.js
File metadata and controls
142 lines (125 loc) · 7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright (c) 2024-2026 nich (@nichxbt). Licensed under the Apache License, Version 2.0.
// scripts/audienceOverlap.js
// Browser console script for analyzing shared followers between X accounts
// Paste in DevTools console on x.com/ACCOUNT/followers
// by nichxbt
(() => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// =============================================
// CONFIGURATION
// =============================================
const CONFIG = {
accounts: ['accountA', 'accountB'], // Two accounts to compare
maxFollowers: 300, // Max followers to scrape per account
scrollRounds: 8, // Scroll rounds per account
scrollDelay: 2000, // ms between scrolls
exportResults: true, // Auto-download JSON
};
// =============================================
const download = (data, filename) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
console.log(`📥 Downloaded: ${filename}`);
};
const datasets = {};
const scrapeFollowers = async (label) => {
const followers = new Map();
for (let round = 0; round < CONFIG.scrollRounds && followers.size < CONFIG.maxFollowers; round++) {
const cells = document.querySelectorAll('[data-testid="UserCell"]');
for (const cell of cells) {
if (followers.size >= CONFIG.maxFollowers) break;
const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
if (!link) continue;
const match = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
if (!match || ['home', 'explore', 'notifications', 'messages', 'i'].includes(match[1])) continue;
const username = match[1].toLowerCase();
if (followers.has(username)) continue;
const nameSpans = cell.querySelectorAll('a[href^="/"] span');
const displayName = nameSpans.length > 0 ? nameSpans[0].textContent.trim() : match[1];
followers.set(username, { username: match[1], displayName });
}
console.log(` 📜 ${label} — Round ${round + 1}: ${followers.size} followers`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
}
return followers;
};
const compare = () => {
const [keyA, keyB] = Object.keys(datasets);
if (!keyA || !keyB) {
console.log('❌ Need both datasets. Run analyze() with two accounts.');
return;
}
const a = datasets[keyA];
const b = datasets[keyB];
const aSet = new Set(a.keys());
const bSet = new Set(b.keys());
const shared = [...aSet].filter(u => bSet.has(u));
const onlyA = [...aSet].filter(u => !bSet.has(u));
const onlyB = [...bSet].filter(u => !aSet.has(u));
const union = new Set([...aSet, ...bSet]);
const overlapPct = ((shared.length / union.size) * 100).toFixed(1);
console.log('\n╔════════════════════════════════════════════════╗');
console.log('║ 🔀 AUDIENCE OVERLAP ANALYSIS ║');
console.log('╚════════════════════════════════════════════════╝');
console.log(`\n Account A: @${keyA} (${aSet.size} followers)`);
console.log(` Account B: @${keyB} (${bSet.size} followers)`);
console.log(` Shared: ${shared.length}`);
console.log(` Only A: ${onlyA.length}`);
console.log(` Only B: ${onlyB.length}`);
console.log(` Overlap: ${overlapPct}% (Jaccard)\n`);
if (shared.length > 0) {
console.log(` 👥 SHARED (${shared.length}): ${shared.slice(0, 15).map(u => '@' + (a.get(u)?.username || u)).join(', ')}${shared.length > 15 ? '...' : ''}`);
}
console.log(` 🅰️ ONLY @${keyA} (${onlyA.length}): ${onlyA.slice(0, 10).map(u => '@' + (a.get(u)?.username || u)).join(', ')}${onlyA.length > 10 ? '...' : ''}`);
console.log(` 🅱️ ONLY @${keyB} (${onlyB.length}): ${onlyB.slice(0, 10).map(u => '@' + (b.get(u)?.username || u)).join(', ')}${onlyB.length > 10 ? '...' : ''}`);
if (parseFloat(overlapPct) > 50) console.log('\n 💡 HIGH overlap — great collab partners.');
else if (parseFloat(overlapPct) > 20) console.log('\n 💡 MODERATE overlap — cross-promo would help.');
else console.log('\n 💡 LOW overlap — big opportunity for cross-pollination!');
if (CONFIG.exportResults) {
download({
accountA: keyA, accountB: keyB,
overlap: { shared: shared.length, onlyA: onlyA.length, onlyB: onlyB.length, jaccardPct: parseFloat(overlapPct) },
sharedFollowers: shared.map(u => a.get(u)?.username || u),
uniqueToA: onlyA.slice(0, 100).map(u => a.get(u)?.username || u),
uniqueToB: onlyB.slice(0, 100).map(u => b.get(u)?.username || u),
analyzedAt: new Date().toISOString(),
}, `xactions-overlap-${keyA}-vs-${keyB}.json`);
}
};
const analyze = async (accountA, accountB) => {
const acctA = accountA || CONFIG.accounts[0];
const acctB = accountB || CONFIG.accounts[1];
if (acctA === 'accountA' || acctB === 'accountB') {
console.log('❌ Set real account names in CONFIG or pass to analyze("acctA","acctB")');
return;
}
console.log('╔════════════════════════════════════════════════╗');
console.log('║ 🔀 AUDIENCE OVERLAP ANALYZER ║');
console.log('║ by nichxbt — v1.0 ║');
console.log('╚════════════════════════════════════════════════╝');
console.log(`\n📊 Scraping @${acctA}'s followers...`);
window.location.href = `https://x.com/${acctA}/followers`;
await sleep(4000);
datasets[acctA] = await scrapeFollowers(acctA);
console.log(` ✅ @${acctA}: ${datasets[acctA].size} followers\n`);
console.log(`📊 Scraping @${acctB}'s followers...`);
window.location.href = `https://x.com/${acctB}/followers`;
await sleep(4000);
datasets[acctB] = await scrapeFollowers(acctB);
console.log(` ✅ @${acctB}: ${datasets[acctB].size} followers\n`);
compare();
};
window.XActions = window.XActions || {};
window.XActions.analyze = analyze;
window.XActions.compare = compare;
console.log('╔════════════════════════════════════════════════╗');
console.log('║ 🔀 AUDIENCE OVERLAP ANALYZER — Ready ║');
console.log('║ by nichxbt — v1.0 ║');
console.log('╚════════════════════════════════════════════════╝');
console.log('\n📋 Usage: XActions.analyze("accountA", "accountB")');
})();