Greasy Fork

Greasy Fork is available in English.

Twitter Media Downloader

Download all media images in original quality.

当前为 2025-01-08 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Twitter Media Downloader
// @description  Download all media images in original quality.
// @icon         https://www.google.com/s2/favicons?sz=64&domain=x.com
// @version      1.0
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://twitter.com/*
// @match        https://x.com/*
// @grant        GM_xmlhttpRequest
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// ==/UserScript==

(function() {
    'use strict';

    let extractedUrls = new Set();
    let isScrolling = false;
    let isPaused = false;
    let isDownloading = false;
    let zip = new JSZip();
    let downloadedCount = 0;

    const downloadIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" style="vertical-align: middle; cursor: pointer;">
        <defs><style>.fa-secondary{opacity:.4}</style></defs>
        <path class="fa-secondary" fill="currentColor" d="M0 256C0 397.4 114.6 512 256 512s256-114.6 256-256c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 106-86 192-192 192S64 362 64 256c0-17.7-14.3-32-32-32s-32 14.3-32 32z"/>
        <path class="fa-primary" fill="currentColor" d="M390.6 185.4c12.5 12.5 12.5 32.8 0 45.3l-112 112c-12.5 12.5-32.8 12.5-45.3 0l-112-112c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L224 242.7 224 32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 210.7 57.4-57.4c12.5-12.5 32.8-12.5 45.3 0z"/>
    </svg>`;

    function createOverlayElements() {
        const overlay = document.createElement('div');
        overlay.id = 'media-downloader-overlay';
        overlay.style.cssText = `
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 9999;
            display: none;
            flex-direction: column;
            gap: 10px;
            align-items: flex-end;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
        `;

        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 10px;
        `;

        const buttonRow = document.createElement('div');
        buttonRow.className = 'buttonRow';
        buttonRow.style.cssText = `
            display: flex;
            gap: 10px;
            justify-content: center;
        `;

        const statusText = document.createElement('div');
        statusText.id = 'download-status';
        statusText.style.cssText = `
            color: white;
            background-color: rgba(0, 0, 0, 0.7);
            padding: 0px 10px;
            border-radius: 4px;
            margin-top: 5px;
            display: none;
            font-family: inherit;
            font-size: 12px;
            text-align: center;
            width: 100%;
        `;

        const progressContainer = document.createElement('div');
        progressContainer.id = 'progress-container';
        progressContainer.style.cssText = `
            width: 200px;
            display: none;
            flex-direction: column;
            gap: 5px;
        `;

        const progressBar = document.createElement('div');
        progressBar.id = 'progress-bar';
        progressBar.style.cssText = `
            width: 100%;
            height: 2px;
            background-color: #f0f0f0;
            border-radius: 1px;
            position: relative;
            overflow: hidden;
        `;

        const progressFill = document.createElement('div');
        progressFill.id = 'progress-fill';
        progressFill.style.cssText = `
            width: 0%;
            height: 100%;
            background-color: #1da1f2;
            position: absolute;
            transition: width 0.3s;
        `;

        const progressStats = document.createElement('div');
        progressStats.style.cssText = `
            display: flex;
            justify-content: space-between;
            color: white;
            font-size: 12px;
            font-family: inherit;
        `;

        const progressCount = document.createElement('div');
        progressCount.id = 'progress-count';
        progressCount.style.cssText = `
            color: white;
            background-color: rgba(0, 0, 0, 0.7);
            padding: 2px 6px;
            border-radius: 4px;
        `;

        const progressPercent = document.createElement('div');
        progressPercent.id = 'progress-percent';
        progressPercent.style.cssText = `
            color: white;
            background-color: rgba(0, 0, 0, 0.7);
            padding: 2px 6px;
            border-radius: 4px;
        `;

        const pauseResumeButton = createButton('Pause', '#1da1f2');
        const stopButton = createButton('Stop', '#e0245e');

        pauseResumeButton.id = 'pause-resume-button';
        stopButton.id = 'stop-button';

        pauseResumeButton.addEventListener('click', () => {
            if (isDownloading) {
                isPaused = !isPaused;
                pauseResumeButton.textContent = isPaused ? 'Resume' : 'Pause';
                if (!isPaused) {
                    scrollAndExtract();
                }
            }
        });

        stopButton.addEventListener('click', () => {
            if (isDownloading) {
                isDownloading = false;
                isPaused = false;
                finishDownload();
            }
        });

        progressBar.appendChild(progressFill);
        progressStats.appendChild(progressCount);
        progressStats.appendChild(progressPercent);
        progressContainer.appendChild(progressBar);
        progressContainer.appendChild(progressStats);

        buttonRow.appendChild(pauseResumeButton);
        buttonRow.appendChild(stopButton);
        buttonContainer.appendChild(buttonRow);
        buttonContainer.appendChild(statusText);

        overlay.appendChild(buttonContainer);
        overlay.appendChild(progressContainer);
        document.body.appendChild(overlay);
    }

    function createButton(text, bgColor) {
        const button = document.createElement('button');
        button.textContent = text;
        const darkenColor = (color, amount) => {
            return '#' + color.replace(/^#/, '').replace(/../g, color => 
                ('0' + Math.min(255, Math.max(0, parseInt(color, 16) - amount)).toString(16)).substr(-2)
            );
        };
        const hoverColor = darkenColor(bgColor, 20);
        button.style.cssText = `
            padding: 5px 10px;
            background-color: ${bgColor};
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            width: 80px;
            font-family: inherit;
            text-align: center;
            transition: background-color 0.3s;
            font-size: 11px;
        `;
        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = hoverColor;
        });
        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = bgColor;
        });
        return button;
    }

    function updateStatus(message) {
        const statusElement = document.getElementById('download-status');
        if (statusElement) {
            statusElement.textContent = message;
            statusElement.style.display = 'block';
            statusElement.style.textAlign = 'center';
        }
    }

    function showProgressBar(progress, total) {
        const progressContainer = document.getElementById('progress-container');
        const progressFill = document.getElementById('progress-fill');
        const progressCount = document.getElementById('progress-count');
        const progressPercent = document.getElementById('progress-percent');
        const buttonRow = document.querySelector('#media-downloader-overlay .buttonRow');

        if (progressContainer && progressFill && progressCount && progressPercent && buttonRow) {
            progressContainer.style.display = 'flex';
            buttonRow.style.display = 'none';
            progressFill.style.width = `${progress}%`;
            progressPercent.textContent = `${Math.round(progress)}%`;
        }
    }

    function insertDownloadIcon() {
        const usernameDivs = document.querySelectorAll('[data-testid="UserName"]');

        usernameDivs.forEach(usernameDiv => {
            if (!usernameDiv.querySelector('.download-icon')) {
                let verifiedButton = usernameDiv.querySelector('[aria-label*="verified"], [aria-label*="Verified"]')?.closest('button');
                let targetElement = verifiedButton ? verifiedButton.parentElement : usernameDiv.querySelector('.css-1jxf684')?.closest('span');

                if (targetElement) {
                    const iconDiv = document.createElement('div');
                    iconDiv.className = 'download-icon css-175oi2r r-1awozwy r-xoduu5';
                    iconDiv.style.cssText = `
                        display: inline-flex;
                        align-items: center;
                        margin-left: 6px;
                        transition: transform 0.1s ease;
                    `;
                    iconDiv.innerHTML = downloadIconSvg;

                    iconDiv.addEventListener('mouseenter', () => {
                        iconDiv.style.transform = 'scale(1.1)';
                    });

                    iconDiv.addEventListener('mouseleave', () => {
                        iconDiv.style.transform = 'scale(1)';
                    });

                    const wrapperDiv = document.createElement('div');
                    wrapperDiv.style.cssText = `
                        display: inline-flex;
                        align-items: center;
                        gap: 4px;
                    `;
                    wrapperDiv.appendChild(iconDiv);

                    targetElement.parentNode.insertBefore(wrapperDiv, targetElement.nextSibling);

                    iconDiv.addEventListener('click', async (e) => {
                        e.stopPropagation();
                        extractedUrls.clear();
                        downloadedCount = 0;
                        zip = new JSZip();
                        isDownloading = true;
                        isPaused = false;

                        const overlay = document.getElementById('media-downloader-overlay');
                        const progressContainer = document.getElementById('progress-container');
                        const buttonContainer = document.querySelector('#media-downloader-overlay > div');
                        const buttonRow = document.querySelector('#media-downloader-overlay .buttonRow');

                        if (overlay) {
                            overlay.style.display = 'flex';
                            progressContainer.style.display = 'none';
                            buttonContainer.style.display = 'flex';
                            buttonRow.style.display = 'flex';
                            updateStatus('Starting download...');
                        }

                        await scrollAndExtract();
                    });
                }
            }
        });
    }

    function extractUrls() {
        const elements = document.querySelectorAll('div[data-testid="cellInnerDiv"]');
        let newUrlsFound = false;

        elements.forEach(element => {
            const style = element.getAttribute('style');
            if (style && style.includes('translateY')) {
                const imgElements = element.querySelectorAll('img[src*="https://pbs.twimg.com/media/"]');
                imgElements.forEach(img => {
                    const src = img.getAttribute('src');
                    if (src && src.includes('format=jpg&name=')) {
                        const largeSrc = src.replace(/name=\w+x\w+/, 'name=large');
                        if (!extractedUrls.has(largeSrc)) {
                            extractedUrls.add(largeSrc);
                            newUrlsFound = true;
                            downloadImage(largeSrc);
                        }
                    }
                });
            }
        });

        updateStatus(`${extractedUrls.size} Images Collected`);
        return newUrlsFound;
    }

    function downloadImage(url) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'arraybuffer',
            onload: function(response) {
                const filename = `${url.split('/').pop().split('?')[0]}.jpg`;
                zip.file(filename, response.response);
                downloadedCount++;
            }
        });
    }

    async function smoothScroll(distance, duration) {
        const start = window.pageYOffset;
        const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();

        function scroll() {
            if (isPaused || !isDownloading) return;

            const now = 'now' in window.performance ? performance.now() : new Date().getTime();
            const time = Math.min(1, ((now - startTime) / duration));

            window.scrollTo(0, start + (distance * time));

            if (time < 1) {
                requestAnimationFrame(scroll);
            }
        }

        scroll();
        return new Promise(resolve => setTimeout(resolve, duration));
    }

    async function scrollAndExtract() {
        if (isScrolling || !isDownloading) return;
        isScrolling = true;

        const scrollStep = 1000;
        const scrollDuration = 1000;
        const waitTime = 1000;
        let consecutiveEmptyScrolls = 0;
        const maxEmptyScrolls = 3;

        while (isDownloading && !isPaused && consecutiveEmptyScrolls < maxEmptyScrolls) {
            await smoothScroll(scrollStep, scrollDuration);
            await new Promise(resolve => setTimeout(resolve, waitTime));

            if (!isDownloading || isPaused) break;

            const newUrlsFound = extractUrls();
            if (!newUrlsFound) {
                consecutiveEmptyScrolls++;
                await smoothScroll(scrollStep * 2, scrollDuration);
                await new Promise(resolve => setTimeout(resolve, waitTime));
                if (!extractUrls()) consecutiveEmptyScrolls++;
            } else {
                consecutiveEmptyScrolls = 0;
            }
        }

        isScrolling = false;
        if (isDownloading && !isPaused) {
            finishDownload();
        }
    }

    function finishDownload() {
        if (extractedUrls.size === 0) {
            updateStatus('No Images Found');
            return;
        }

        updateStatus('Zipping file...');
        showProgressBar(0, extractedUrls.size);

        const currentUrl = window.location.href;
        const match = currentUrl.match(/https:\/\/(?:x|twitter)\.com\/([^\/]+)/);
        const username = match ? match[1] : 'Unknown';

        zip.generateAsync({type:"blob"}, metadata => {
            showProgressBar(metadata.percent, extractedUrls.size);
        })
        .then(function(content) {
            const a = document.createElement('a');
            a.href = URL.createObjectURL(content);
            a.download = `${username}-${extractedUrls.size}.zip`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);

            setTimeout(() => {
                const overlay = document.getElementById('media-downloader-overlay');
                if (overlay) {
                    overlay.style.display = 'none';
                }
                extractedUrls.clear();
                isDownloading = false;
                isPaused = false;
            }, 2000);
        });
    }

    const observer = new MutationObserver(() => {
        insertDownloadIcon();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    insertDownloadIcon();
    createOverlayElements();
})();