Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Download All Tweet Media
- // @namespace http://tampermonkey.net/
- // @version 1.9
- // @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
- // @author Grok
- // @match https://x.com/*
- // @grant none
- // ==/UserScript==
- (function() {
- 'use strict';
- // SVG icons
- const downloadIcon = `
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
- <polyline points="7 10 12 15 17 10"></polyline>
- <line x1="12" y1="15" x2="12" y2="3"></line>
- </svg>
- `;
- const loadingIcon = `
- <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">
- <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
- </svg>
- `;
- const checkIcon = `
- <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">
- <polyline points="20 6 9 17 4 12"></polyline>
- </svg>
- `;
- // Function to download a file using fetch and blob
- async function downloadFile(url, filename) {
- try {
- const response = await fetch(url, { mode: 'cors' });
- if (!response.ok) throw new Error('Network response was not ok');
- const blob = await response.blob();
- const blobUrl = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = blobUrl;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(blobUrl);
- } catch (error) {
- console.error('Download failed:', error);
- // Fallback to direct link
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- }
- }
- // Function to infer file format from URL or fallback to jpg
- function getImageFormat(src) {
- if (src.toLowerCase().includes('.png')) return 'png';
- if (src.toLowerCase().includes('.gif')) return 'gif';
- return 'jpg'; // Default to jpg if unknown
- }
- // Function to extract metadata from tweet
- function getTweetMetadata(tweetElement) {
- // Get username (handle starting with @)
- let username = '';
- const handleSelectors = [
- '[data-testid="User-Name"] a[href^="/"]:not([href*="/status/"]) span', // Handle link
- 'a[href^="/"]:not([href*="/status/"]) span', // Any profile link
- '[data-testid="tweet"] span' // Fallback
- ];
- for (const selector of handleSelectors) {
- const elements = tweetElement.querySelectorAll(selector);
- for (const element of elements) {
- if (element.textContent.startsWith('@')) {
- username = element.textContent.replace(/^@/, '');
- break;
- }
- }
- if (username) break;
- }
- // Fallback: try to extract from tweet URL
- if (!username) {
- const statusLink = tweetElement.querySelector('a[href*="/status/"]');
- if (statusLink) {
- const match = statusLink.href.match(/\/([^/]+)\/status\//);
- if (match) username = match[1];
- }
- }
- // Clean username
- username = username.replace(/[^a-zA-Z0-9_]/g, '');
- // Get tweet ID
- let tweetId = '';
- const statusLink = tweetElement.querySelector('[data-testid="tweet"] a[href*="/status/"]');
- if (statusLink) {
- const match = statusLink.href.match(/\/status\/(\d+)/);
- if (match) tweetId = match[1];
- }
- console.log('Extracted metadata:', { username, tweetId }); // Debug log
- return { username: username || 'unknown', tweetId: tweetId || 'unknown' };
- }
- // Function to check if tweet was previously downloaded
- function isTweetDownloaded(tweetId) {
- const downloadedTweets = JSON.parse(localStorage.getItem('downloadedTweets') || '{}');
- return downloadedTweets[tweetId] === true;
- }
- // Function to mark tweet as downloaded
- function markTweetAsDownloaded(tweetId) {
- const downloadedTweets = JSON.parse(localStorage.getItem('downloadedTweets') || '{}');
- downloadedTweets[tweetId] = true;
- localStorage.setItem('downloadedTweets', JSON.stringify(downloadedTweets));
- }
- // Function to get all media URLs and metadata from a tweet
- function getMediaUrls(tweetElement) {
- const mediaUrls = [];
- const { username, tweetId } = getTweetMetadata(tweetElement);
- // Find images
- const images = tweetElement.querySelectorAll('img[src*="media"]');
- images.forEach((img, index) => {
- let src = img.src;
- src = src.split('?')[0];
- const format = getImageFormat(src);
- const url = `${src}?format=${format}&name=orig`;
- const mediaId = src.split('/').pop() || `media_${index + 1}`;
- const filename = username && tweetId !== 'unknown'
- ? `@${username}-${tweetId}-${mediaId}.${format}`
- : `tweet_media_${index + 1}.${format}`;
- mediaUrls.push({ url, filename });
- });
- // Find videos
- const videos = tweetElement.querySelectorAll('video source, video');
- videos.forEach((video, index) => {
- if (video.src) {
- const mediaId = video.src.split('/').pop()?.split('?')[0] || `video_${index + 1}`;
- const filename = username && tweetId !== 'unknown'
- ? `@${username}-${tweetId}-${mediaId}.mp4`
- : `tweet_media_${index + 1}.mp4`;
- mediaUrls.push({ url: video.src, filename });
- }
- });
- console.log('Media URLs and filenames:', mediaUrls); // Debug log
- return mediaUrls;
- }
- // Function to add download button to tweet
- function addDownloadButton(tweetElement) {
- // Check if button already exists
- if (tweetElement.querySelector('.download-media-button')) return;
- // Check for media
- const hasMedia = tweetElement.querySelector('img[src*="media"], video source, video');
- if (!hasMedia) {
- console.log('No media found in tweet, skipping button'); // Debug log
- return;
- }
- // Find the action bar
- const actionBar = tweetElement.querySelector('div[role="group"]');
- if (!actionBar) return;
- // Get tweet metadata for checking download status
- const { tweetId } = getTweetMetadata(tweetElement);
- // Create download button
- const button = document.createElement('div');
- button.className = 'download-media-button';
- button.style.cssText = `
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- vertical-align: middle;
- cursor: pointer;
- color: rgb(113, 118, 123);
- border-radius: 9999px;
- transition: background-color 0.2s ease-in-out;
- `;
- // Set initial icon based on download status
- button.innerHTML = isTweetDownloaded(tweetId) ? checkIcon : downloadIcon;
- // Add hover effect with blue glow
- button.addEventListener('mouseenter', () => {
- button.style.backgroundColor = 'rgba(29, 155, 240, 0.1)';
- button.style.color = 'rgb(29, 155, 240)';
- });
- button.addEventListener('mouseleave', () => {
- button.style.backgroundColor = 'transparent';
- button.style.color = isTweetDownloaded(tweetId) ? 'rgb(29, 155, 240)' : 'rgb(113, 118, 123)';
- });
- // Add click handler
- button.addEventListener('click', async (e) => {
- e.stopPropagation();
- const mediaItems = getMediaUrls(tweetElement);
- if (mediaItems.length === 0) {
- alert('No media files found in this tweet.');
- return;
- }
- // Show loading animation
- button.innerHTML = loadingIcon;
- button.style.cursor = 'default';
- button.style.backgroundColor = 'transparent';
- button.style.color = 'rgb(113, 118, 123)';
- try {
- for (let index = 0; index < mediaItems.length; index++) {
- const { url, filename } = mediaItems[index];
- await downloadFile(url, filename);
- await new Promise(resolve => setTimeout(resolve, 500)); // Stagger downloads
- }
- // All downloads completed successfully
- markTweetAsDownloaded(tweetId);
- button.innerHTML = checkIcon;
- button.style.cursor = 'pointer';
- button.style.color = 'rgb(29, 155, 240)';
- } catch (error) {
- console.error('Download process failed:', error);
- // Revert to download icon on failure
- button.innerHTML = downloadIcon;
- button.style.cursor = 'pointer';
- button.style.color = 'rgb(113, 118, 123)';
- alert('An error occurred while downloading some files.');
- }
- });
- // Insert button before the share button
- const shareButton = actionBar.querySelector('button[data-testid*="share"]');
- if (shareButton) {
- actionBar.insertBefore(button, shareButton.parentElement);
- } else {
- actionBar.appendChild(button);
- }
- }
- // Function to process all tweets on the page
- function processTweets() {
- const tweets = document.querySelectorAll('article[data-testid="tweet"]');
- tweets.forEach(tweet => addDownloadButton(tweet));
- }
- // Observe DOM changes for dynamically loaded tweets
- const observer = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length) {
- processTweets();
- }
- });
- });
- // Start observing
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- // Initial processing
- processTweets();
- })();
Advertisement
Add Comment
Please, Sign In to add comment