Twitter Image Downloader

Download images from Twitter posts and pack them into a ZIP archive with metadata.

当前为 2024-08-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 });
})();