Guest User

Untitled

a guest
May 4th, 2025
24
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.82 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Download All Tweet Media
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.9
  5. // @description Adds a button to download all media files (images/videos) from a tweet on X with metadata in filenames, only for tweets with media
  6. // @author Grok
  7. // @match https://x.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // SVG icons
  15. const downloadIcon = `
  16. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  17. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  18. <polyline points="7 10 12 15 17 10"></polyline>
  19. <line x1="12" y1="15" x2="12" y2="3"></line>
  20. </svg>
  21. `;
  22.  
  23. const loadingIcon = `
  24. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin">
  25. <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
  26. </svg>
  27. `;
  28.  
  29. const checkIcon = `
  30. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="rgb(29, 155, 240)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  31. <polyline points="20 6 9 17 4 12"></polyline>
  32. </svg>
  33. `;
  34.  
  35. // Function to download a file using fetch and blob
  36. async function downloadFile(url, filename) {
  37. try {
  38. const response = await fetch(url, { mode: 'cors' });
  39. if (!response.ok) throw new Error('Network response was not ok');
  40. const blob = await response.blob();
  41. const blobUrl = window.URL.createObjectURL(blob);
  42. const a = document.createElement('a');
  43. a.href = blobUrl;
  44. a.download = filename;
  45. document.body.appendChild(a);
  46. a.click();
  47. document.body.removeChild(a);
  48. window.URL.revokeObjectURL(blobUrl);
  49. } catch (error) {
  50. console.error('Download failed:', error);
  51. // Fallback to direct link
  52. const a = document.createElement('a');
  53. a.href = url;
  54. a.download = filename;
  55. document.body.appendChild(a);
  56. a.click();
  57. document.body.removeChild(a);
  58. }
  59. }
  60.  
  61. // Function to infer file format from URL or fallback to jpg
  62. function getImageFormat(src) {
  63. if (src.toLowerCase().includes('.png')) return 'png';
  64. if (src.toLowerCase().includes('.gif')) return 'gif';
  65. return 'jpg'; // Default to jpg if unknown
  66. }
  67.  
  68. // Function to extract metadata from tweet
  69. function getTweetMetadata(tweetElement) {
  70. // Get username (handle starting with @)
  71. let username = '';
  72. const handleSelectors = [
  73. '[data-testid="User-Name"] a[href^="/"]:not([href*="/status/"]) span', // Handle link
  74. 'a[href^="/"]:not([href*="/status/"]) span', // Any profile link
  75. '[data-testid="tweet"] span' // Fallback
  76. ];
  77. for (const selector of handleSelectors) {
  78. const elements = tweetElement.querySelectorAll(selector);
  79. for (const element of elements) {
  80. if (element.textContent.startsWith('@')) {
  81. username = element.textContent.replace(/^@/, '');
  82. break;
  83. }
  84. }
  85. if (username) break;
  86. }
  87. // Fallback: try to extract from tweet URL
  88. if (!username) {
  89. const statusLink = tweetElement.querySelector('a[href*="/status/"]');
  90. if (statusLink) {
  91. const match = statusLink.href.match(/\/([^/]+)\/status\//);
  92. if (match) username = match[1];
  93. }
  94. }
  95. // Clean username
  96. username = username.replace(/[^a-zA-Z0-9_]/g, '');
  97.  
  98. // Get tweet ID
  99. let tweetId = '';
  100. const statusLink = tweetElement.querySelector('[data-testid="tweet"] a[href*="/status/"]');
  101. if (statusLink) {
  102. const match = statusLink.href.match(/\/status\/(\d+)/);
  103. if (match) tweetId = match[1];
  104. }
  105.  
  106. console.log('Extracted metadata:', { username, tweetId }); // Debug log
  107. return { username: username || 'unknown', tweetId: tweetId || 'unknown' };
  108. }
  109.  
  110. // Function to check if tweet was previously downloaded
  111. function isTweetDownloaded(tweetId) {
  112. const downloadedTweets = JSON.parse(localStorage.getItem('downloadedTweets') || '{}');
  113. return downloadedTweets[tweetId] === true;
  114. }
  115.  
  116. // Function to mark tweet as downloaded
  117. function markTweetAsDownloaded(tweetId) {
  118. const downloadedTweets = JSON.parse(localStorage.getItem('downloadedTweets') || '{}');
  119. downloadedTweets[tweetId] = true;
  120. localStorage.setItem('downloadedTweets', JSON.stringify(downloadedTweets));
  121. }
  122.  
  123. // Function to get all media URLs and metadata from a tweet
  124. function getMediaUrls(tweetElement) {
  125. const mediaUrls = [];
  126. const { username, tweetId } = getTweetMetadata(tweetElement);
  127.  
  128. // Find images
  129. const images = tweetElement.querySelectorAll('img[src*="media"]');
  130. images.forEach((img, index) => {
  131. let src = img.src;
  132. src = src.split('?')[0];
  133. const format = getImageFormat(src);
  134. const url = `${src}?format=${format}&name=orig`;
  135. const mediaId = src.split('/').pop() || `media_${index + 1}`;
  136. const filename = username && tweetId !== 'unknown'
  137. ? `@${username}-${tweetId}-${mediaId}.${format}`
  138. : `tweet_media_${index + 1}.${format}`;
  139. mediaUrls.push({ url, filename });
  140. });
  141.  
  142. // Find videos
  143. const videos = tweetElement.querySelectorAll('video source, video');
  144. videos.forEach((video, index) => {
  145. if (video.src) {
  146. const mediaId = video.src.split('/').pop()?.split('?')[0] || `video_${index + 1}`;
  147. const filename = username && tweetId !== 'unknown'
  148. ? `@${username}-${tweetId}-${mediaId}.mp4`
  149. : `tweet_media_${index + 1}.mp4`;
  150. mediaUrls.push({ url: video.src, filename });
  151. }
  152. });
  153.  
  154. console.log('Media URLs and filenames:', mediaUrls); // Debug log
  155. return mediaUrls;
  156. }
  157.  
  158. // Function to add download button to tweet
  159. function addDownloadButton(tweetElement) {
  160. // Check if button already exists
  161. if (tweetElement.querySelector('.download-media-button')) return;
  162.  
  163. // Check for media
  164. const hasMedia = tweetElement.querySelector('img[src*="media"], video source, video');
  165. if (!hasMedia) {
  166. console.log('No media found in tweet, skipping button'); // Debug log
  167. return;
  168. }
  169.  
  170. // Find the action bar
  171. const actionBar = tweetElement.querySelector('div[role="group"]');
  172. if (!actionBar) return;
  173.  
  174. // Get tweet metadata for checking download status
  175. const { tweetId } = getTweetMetadata(tweetElement);
  176.  
  177. // Create download button
  178. const button = document.createElement('div');
  179. button.className = 'download-media-button';
  180. button.style.cssText = `
  181. display: inline-flex;
  182. align-items: center;
  183. justify-content: center;
  184. width: 36px;
  185. vertical-align: middle;
  186. cursor: pointer;
  187. color: rgb(113, 118, 123);
  188. border-radius: 9999px;
  189. transition: background-color 0.2s ease-in-out;
  190. `;
  191. // Set initial icon based on download status
  192. button.innerHTML = isTweetDownloaded(tweetId) ? checkIcon : downloadIcon;
  193.  
  194. // Add hover effect with blue glow
  195. button.addEventListener('mouseenter', () => {
  196. button.style.backgroundColor = 'rgba(29, 155, 240, 0.1)';
  197. button.style.color = 'rgb(29, 155, 240)';
  198. });
  199. button.addEventListener('mouseleave', () => {
  200. button.style.backgroundColor = 'transparent';
  201. button.style.color = isTweetDownloaded(tweetId) ? 'rgb(29, 155, 240)' : 'rgb(113, 118, 123)';
  202. });
  203.  
  204. // Add click handler
  205. button.addEventListener('click', async (e) => {
  206. e.stopPropagation();
  207. const mediaItems = getMediaUrls(tweetElement);
  208. if (mediaItems.length === 0) {
  209. alert('No media files found in this tweet.');
  210. return;
  211. }
  212.  
  213. // Show loading animation
  214. button.innerHTML = loadingIcon;
  215. button.style.cursor = 'default';
  216. button.style.backgroundColor = 'transparent';
  217. button.style.color = 'rgb(113, 118, 123)';
  218.  
  219. try {
  220. for (let index = 0; index < mediaItems.length; index++) {
  221. const { url, filename } = mediaItems[index];
  222. await downloadFile(url, filename);
  223. await new Promise(resolve => setTimeout(resolve, 500)); // Stagger downloads
  224. }
  225.  
  226. // All downloads completed successfully
  227. markTweetAsDownloaded(tweetId);
  228. button.innerHTML = checkIcon;
  229. button.style.cursor = 'pointer';
  230. button.style.color = 'rgb(29, 155, 240)';
  231. } catch (error) {
  232. console.error('Download process failed:', error);
  233. // Revert to download icon on failure
  234. button.innerHTML = downloadIcon;
  235. button.style.cursor = 'pointer';
  236. button.style.color = 'rgb(113, 118, 123)';
  237. alert('An error occurred while downloading some files.');
  238. }
  239. });
  240.  
  241. // Insert button before the share button
  242. const shareButton = actionBar.querySelector('button[data-testid*="share"]');
  243. if (shareButton) {
  244. actionBar.insertBefore(button, shareButton.parentElement);
  245. } else {
  246. actionBar.appendChild(button);
  247. }
  248. }
  249.  
  250. // Function to process all tweets on the page
  251. function processTweets() {
  252. const tweets = document.querySelectorAll('article[data-testid="tweet"]');
  253. tweets.forEach(tweet => addDownloadButton(tweet));
  254. }
  255.  
  256. // Observe DOM changes for dynamically loaded tweets
  257. const observer = new MutationObserver((mutations) => {
  258. mutations.forEach(mutation => {
  259. if (mutation.addedNodes.length) {
  260. processTweets();
  261. }
  262. });
  263. });
  264.  
  265. // Start observing
  266. observer.observe(document.body, {
  267. childList: true,
  268. subtree: true
  269. });
  270.  
  271. // Initial processing
  272. processTweets();
  273. })();
Advertisement
Add Comment
Please, Sign In to add comment