Guest User

Untitled

a guest
Jul 10th, 2025
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 7.79 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Twitter/X User Media Tab Deduplicator
  3. // @namespace https://x.com
  4. // @version 2.0
  5. // @description Hide all duplicate media in user profiles media tab
  6. // @author KBD99
  7. // @match https://x.com/*/media*
  8. // @match https://twitter.com/*/media*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. let dedupEnabled = true;
  16. const HASH_KEY = 'twitterMediaHashes';
  17. let seenHashes = JSON.parse(localStorage.getItem(HASH_KEY) || '{}');
  18. const processedElements = new WeakSet();
  19.  
  20. // UI Toggle Button
  21. const toggleButton = document.createElement('button');
  22. toggleButton.textContent = 'Dedup ON';
  23. Object.assign(toggleButton.style, {
  24. position: 'fixed',
  25. top: '10px',
  26. right: '10px',
  27. zIndex: 9999,
  28. padding: '8px 12px',
  29. fontSize: '12px',
  30. backgroundColor: '#1da1f2',
  31. color: 'white',
  32. border: 'none',
  33. borderRadius: '6px',
  34. cursor: 'pointer',
  35. fontWeight: 'bold',
  36. boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
  37. });
  38. toggleButton.onclick = () => {
  39. dedupEnabled = !dedupEnabled;
  40. toggleButton.textContent = dedupEnabled ? 'Dedup ON' : 'Dedup OFF';
  41. toggleButton.style.backgroundColor = dedupEnabled ? '#1da1f2' : '#657786';
  42. if (dedupEnabled) {
  43. runDeduplication();
  44. } else {
  45. // Show all hidden elements
  46. document.querySelectorAll('[data-dedup-hidden="true"]').forEach(el => {
  47. el.style.display = '';
  48. el.removeAttribute('data-dedup-hidden');
  49. });
  50. }
  51. };
  52.  
  53. // Clear Cache Button
  54. const clearButton = document.createElement('button');
  55. clearButton.textContent = 'Clear Cache';
  56. Object.assign(clearButton.style, {
  57. position: 'fixed',
  58. top: '10px',
  59. right: '120px',
  60. zIndex: 9999,
  61. padding: '8px 12px',
  62. fontSize: '12px',
  63. backgroundColor: '#e0245e',
  64. color: 'white',
  65. border: 'none',
  66. borderRadius: '6px',
  67. cursor: 'pointer',
  68. fontWeight: 'bold',
  69. boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
  70. });
  71. clearButton.onclick = () => {
  72. localStorage.removeItem(HASH_KEY);
  73. seenHashes = {};
  74. processedElements.clear();
  75. // Show all hidden elements
  76. document.querySelectorAll('[data-dedup-hidden="true"]').forEach(el => {
  77. el.style.display = '';
  78. el.removeAttribute('data-dedup-hidden');
  79. });
  80. alert('Media cache cleared! Duplicates will be detected again.');
  81. setTimeout(runDeduplication, 1000);
  82. };
  83.  
  84. document.body.appendChild(toggleButton);
  85. document.body.appendChild(clearButton);
  86.  
  87. // Generate hash from image URL and dimensions
  88. function getMediaHash(el) {
  89. try {
  90. let src = el.src || el.querySelector('img')?.src;
  91. if (!src) return null;
  92.  
  93. // Remove size parameters and query strings that don't affect content
  94. src = src.replace(/[?&](name|format)=[^&]*/g, '');
  95. src = src.replace(/[?&]$/g, '');
  96.  
  97. // For Twitter/X images, extract the base URL without size modifiers
  98. const twitterImageMatch = src.match(/^(https:\/\/pbs\.twimg\.com\/media\/[^?]+)/);
  99. if (twitterImageMatch) {
  100. src = twitterImageMatch[1];
  101. }
  102.  
  103. // Include element dimensions for better duplicate detection
  104. const width = el.naturalWidth || el.videoWidth || 0;
  105. const height = el.naturalHeight || el.videoHeight || 0;
  106.  
  107. return `${src}_${width}x${height}`;
  108. } catch (e) {
  109. return null;
  110. }
  111. }
  112.  
  113. // Find the tweet container for a media element
  114. function findTweetContainer(el) {
  115. // Look for the tweet article container
  116. let container = el.closest('article[data-testid="tweet"]');
  117. if (container) return container;
  118.  
  119. // Fallback: look for common tweet container patterns
  120. container = el.closest('div[data-testid]');
  121. if (container) return container;
  122.  
  123. // Last resort: find a container with reasonable size
  124. let current = el.parentElement;
  125. while (current && current !== document.body) {
  126. const rect = current.getBoundingClientRect();
  127. if (rect.width > 300 && rect.height > 100) {
  128. return current;
  129. }
  130. current = current.parentElement;
  131. }
  132.  
  133. return null;
  134. }
  135.  
  136. // Deduplication Logic
  137. function runDeduplication() {
  138. if (!dedupEnabled) return;
  139.  
  140. // More specific selectors for Twitter/X media
  141. const mediaSelectors = [
  142. 'img[src*="pbs.twimg.com"]',
  143. 'img[src*="video.twimg.com"]',
  144. 'video[src*="video.twimg.com"]',
  145. 'div[data-testid="tweetPhoto"] img',
  146. 'div[data-testid="videoPlayer"] video',
  147. 'div[aria-label*="Image"] img'
  148. ];
  149.  
  150. mediaSelectors.forEach(selector => {
  151. const mediaElements = document.querySelectorAll(selector);
  152. mediaElements.forEach(el => {
  153. // Skip if already processed
  154. if (processedElements.has(el)) return;
  155.  
  156. // Skip if not loaded yet
  157. if (el.tagName === 'IMG' && (!el.complete || el.naturalWidth === 0)) {
  158. // Try again when image loads
  159. el.addEventListener('load', () => {
  160. if (!processedElements.has(el)) {
  161. processMediaElement(el);
  162. }
  163. }, { once: true });
  164. return;
  165. }
  166.  
  167. processMediaElement(el);
  168. });
  169. });
  170. }
  171.  
  172. function processMediaElement(el) {
  173. if (processedElements.has(el)) return;
  174. processedElements.add(el);
  175.  
  176. const hash = getMediaHash(el);
  177. if (!hash) return;
  178.  
  179. const container = findTweetContainer(el);
  180. if (!container) return;
  181.  
  182. if (seenHashes[hash]) {
  183. // Hide the duplicate
  184. container.style.setProperty('display', 'none', 'important');
  185. container.setAttribute('data-dedup-hidden', 'true');
  186. console.log('Hidden duplicate media:', hash);
  187. } else {
  188. // Mark as seen
  189. seenHashes[hash] = true;
  190. localStorage.setItem(HASH_KEY, JSON.stringify(seenHashes));
  191. console.log('New media found:', hash);
  192. }
  193. }
  194.  
  195. // Observe for lazy-loaded media with throttling
  196. let observerTimeout;
  197. const observer = new MutationObserver(() => {
  198. clearTimeout(observerTimeout);
  199. observerTimeout = setTimeout(runDeduplication, 500);
  200. });
  201.  
  202. // Start observing
  203. observer.observe(document.body, {
  204. childList: true,
  205. subtree: true,
  206. attributes: false,
  207. attributeOldValue: false,
  208. characterData: false
  209. });
  210.  
  211. // Initial run with multiple attempts
  212. function initialRun() {
  213. setTimeout(runDeduplication, 1000);
  214. setTimeout(runDeduplication, 3000);
  215. setTimeout(runDeduplication, 5000);
  216. }
  217.  
  218. // Run when page loads
  219. if (document.readyState === 'loading') {
  220. document.addEventListener('DOMContentLoaded', initialRun);
  221. } else {
  222. initialRun();
  223. }
  224.  
  225. // Re-run when scrolling stops (for infinite scroll)
  226. let scrollTimeout;
  227. window.addEventListener('scroll', () => {
  228. clearTimeout(scrollTimeout);
  229. scrollTimeout = setTimeout(runDeduplication, 1000);
  230. });
  231.  
  232. console.log('Twitter/X Media Deduplicator loaded');
  233. })();
Advertisement
Add Comment
Please, Sign In to add comment