Download images from Twitter posts and pack them into a ZIP archive with metadata.
当前为
// ==UserScript==
// @name Twitter Image Downloader
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Download images from Twitter posts and pack them into a ZIP archive with metadata.
// @author Dramorian
// @license MIT
// @match https://twitter.com/*
// @match https://x.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Add a download button to the tweet actions bar
function addDownloadButton(tweetElement) {
// Check if button already exists
if (tweetElement.querySelector('.download-images-btn')) return;
// Check if the tweet contains at least one image (ignoring emojis)
const imageElements = tweetElement.querySelectorAll('div.css-175oi2r a[href*="/photo/"]');
if (imageElements.length === 0) return; // Don't add button if no images are found
const button = document.createElement('button');
button.innerText = 'Download Images';
button.className = 'download-images-btn';
button.style.marginLeft = '10px';
button.addEventListener('click', () => downloadImages(tweetElement));
const actionBar = tweetElement.querySelector('[role="group"]');
if (actionBar) {
actionBar.appendChild(button);
}
}
// Download images and metadata
async function downloadImages(tweetElement) {
const zip = new JSZip();
const tweetLinkElement = tweetElement.querySelector('a[href*="/status/"]');
const tweetLink = tweetLinkElement.href;
const tweetParts = tweetLink.match(/https:\/\/(?:x\.com|twitter\.com)\/([^\/]+)\/status\/(\d+)/);
const authorHandle = tweetParts[1];
const tweetId = tweetParts[2];
const authorCommentElement = tweetElement.querySelector('div[lang]');
const authorComment = authorCommentElement ? authorCommentElement.innerText : '';
const dateElement = tweetElement.querySelector('time');
const postDateTime = dateElement ? new Date(dateElement.getAttribute('datetime')) : new Date();
const imageElements = tweetElement.querySelectorAll('div.css-175oi2r a[href*="/photo/"] img:not([src*="emoji"])');
// Metadata starts with the link to the tweet, followed by the author comment (if any), handle, date and time, and links to each image
let metadata = `${tweetLink}\n`;
if (authorComment) {
metadata += `${authorComment}\n`;
}
metadata += `@${authorHandle}\n${postDateTime.toLocaleString()}\n`;
// Add images to the zip
let imageIndex = 1;
for (const imgElement of imageElements) {
let imgUrl = imgElement.src;
imgUrl = imgUrl.replace(/(?<=name=)[^&]+/, 'orig');
const imgData = await fetch(imgUrl).then(res => res.blob());
const imageName = `${authorHandle}_${tweetId}_${imageIndex}.jpg`;
zip.file(imageName, imgData);
// Append each image link to the metadata
metadata += `https://x.com${imgElement.closest('a').getAttribute('href')}\n`;
imageIndex++;
}
// Add metadata files
zip.file('metadata.txt', metadata.trim());
zip.file(`${authorHandle}_${tweetId}.url`, `[InternetShortcut]\nURL=${tweetLink}`);
// Generate the zip file and trigger the download
const content = await zip.generateAsync({ type: 'blob' });
saveAs(content, `${authorHandle}_${tweetId}.zip`);
}
// Observe the DOM for new tweets and add the download button
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a tweet or contains tweets
const tweetElements = addedNode.matches('article') ? [addedNode] : addedNode.querySelectorAll('article');
tweetElements.forEach(tweetElement => {
addDownloadButton(tweetElement);
});
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();