Greasy Fork

Greasy Fork is available in English.

X 博主图片分批下载(每批1000张,断点续传+重置+批量下载调试)

分批(1000张)下载博主所有图片,支持断点续传、重置,下载阶段每批10张并行,暂停3秒再继续,并打日志~

当前为 2025-04-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X 博主图片分批下载(每批1000张,断点续传+重置+批量下载调试)
// @namespace    http://tampermonkey.net/
// @version      0.9.1
// @description  分批(1000张)下载博主所有图片,支持断点续传、重置,下载阶段每批10张并行,暂停3秒再继续,并打日志~
// @author       chatGPT
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// @license MIT 
// ==/UserScript==

(function () {
    'use strict';

    // —— 配置项 ——
    const BATCH_SIZE     = 1000;   // 每批滚动+下载数量
    const DOWNLOAD_BATCH = 50;     // 每次并发下载张数
    const DOWNLOAD_PAUSE = 1000;   // 批次下载之间暂停(ms)
    const scrollInterval = 3000;   // 滚动间隔(ms)
    const maxScrollCount = 10000;  // 最大滚动次数
    // ——————————

    let cancelDownload = false;
    const imageSet      = new Set();
    let hideTimeoutId   = null;

    // 本地存储 key
    const KEY_COUNT = 'tm_downloadedCount';
    const KEY_POS   = 'tm_lastScrollPos';

    // 读取断点信息
    let downloadedCount = parseInt(localStorage.getItem(KEY_COUNT) || '0', 10);
    let lastScrollPos   = parseInt(localStorage.getItem(KEY_POS)   || '0', 10);

    // 获取用户名
    function getUsername() {
        const m = window.location.pathname.match(/^\/([^\/\?]+)/);
        return m ? m[1] : 'unknown_user';
    }

    // 创建并插入提示框
    const progressBox = document.createElement('div');
    Object.assign(progressBox.style, {
        position: 'fixed', top: '20px', left: '20px',
        padding: '10px', backgroundColor: 'rgba(0,0,0,0.8)',
        color: '#fff', fontSize: '14px', zIndex: 9999,
        borderRadius: '8px', display: 'none',
    });
    document.body.appendChild(progressBox);
    function updateProgress(txt) {
        progressBox.innerText = txt;
    }

    // 下载单张
    async function downloadImage(url, idx, prefix) {
        console.log(`🔗 发起下载:${idx} -> ${url}`);
        try {
            const res = await fetch(url);
            const blob = await res.blob();
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = `${prefix}_img_${idx}.jpg`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            // URL.revokeObjectURL(a.href); // 可选
        } catch (e) {
            console.error('下载失败', url, e);
        }
    }

    // 收集可见图片
    function collectVisibleImages() {
        document.querySelectorAll('img[src*="twimg.com/media"]').forEach(img => {
            let url = img.src
                .replace(/&name=\w+/, '&name=orig')
                .replace(/&format=\w+/, '&format=jpg');
            imageSet.add(url);
        });
    }

    // 主流程
    async function autoScrollAndDownload() {
        cancelDownload = false;
        imageSet.clear();

        // 恢复到上次滚动位置
        window.scrollTo(0, lastScrollPos);
        updateProgress(`📌 从 ${lastScrollPos}px 恢复,已下载 ${downloadedCount} 张`);
        progressBox.style.display = 'block';

        // 滚动收集
        let count = 0, lastHeight = 0, stable = 0;
        while (
            !cancelDownload &&
            count < maxScrollCount &&
            imageSet.size < downloadedCount + BATCH_SIZE &&
            stable < 3
        ) {
            collectVisibleImages();
            updateProgress(`📦 已收集 ${imageSet.size} 张,目标 ${(downloadedCount + BATCH_SIZE)} 张`);
            window.scrollTo(0, document.body.scrollHeight);
            await new Promise(r => setTimeout(r, scrollInterval));
            const h = document.body.scrollHeight;
            if (h === lastHeight) stable++; else stable = 0;
            lastHeight = h;
            count++;
        }
        collectVisibleImages(); // 最后一轮

        if (cancelDownload) {
            updateProgress('❌ 已取消,进度已保存');
            finishAndSave();
            return;
        }

        // 准备下载这一批
        const arr = Array.from(imageSet);
        const end = Math.min(arr.length, downloadedCount + BATCH_SIZE);
        const prefix = getUsername();
        console.log(`🚀 批量下载区间:${downloadedCount+1} ~ ${end}`);
        updateProgress(`⬇️ 批量下载 ${downloadedCount + 1} ~ ${end} 张`);

        // 按 DOWNLOAD_BATCH 并发下载
        for (let i = downloadedCount; i < end; i += DOWNLOAD_BATCH) {
            if (cancelDownload) break;
            const chunkEnd = Math.min(i + DOWNLOAD_BATCH, end);
            console.log(`📦 下载批次:${i+1} ~ ${chunkEnd}`);
            updateProgress(`⬇️ 下载${i+1}~${chunkEnd}/${end}`);

            // 并发下载这一批
            await Promise.all(
                Array.from({ length: chunkEnd - i }, (_, k) =>
                    downloadImage(arr[i + k], i + k + 1, prefix)
                )
            );

            if (chunkEnd < end) {
                console.log(`⏸️ 批次完成,暂停 ${DOWNLOAD_PAUSE}ms`);
                updateProgress(`⏸️ 暂停 ${DOWNLOAD_PAUSE/1000}s`);
                await new Promise(r => setTimeout(r, DOWNLOAD_PAUSE));
            }
        }

        // 更新断点
        downloadedCount = cancelDownload ? downloadedCount : end;
        lastScrollPos   = window.scrollY;
        localStorage.setItem(KEY_COUNT, downloadedCount);
        localStorage.setItem(KEY_POS, lastScrollPos);

        if (cancelDownload) {
            updateProgress('❌ 已取消,进度已保存');
        } else {
            updateProgress(`✅ 本批完成,共下载 ${downloadedCount} 张`);
        }
        finishAndSave();
    }

    // 取消/完成后收尾
    function finishAndSave() {
        startBtn.disabled = false;
        cancelBtn.style.display = 'none';
        hideTimeoutId = setTimeout(() => {
            updateProgress('准备中...');
            progressBox.style.display = 'none';
        }, 5000);
    }

    // 按钮:开始/继续
    const startBtn = document.createElement('button');
    startBtn.innerText = '📸 开始/继续下载下一批';
    Object.assign(startBtn.style, {
        position: 'fixed', top: '20px', right: '20px',
        zIndex: 10000, padding: '10px',
        backgroundColor: '#1DA1F2', color: '#fff',
        border: 'none', borderRadius: '5px', cursor: 'pointer',
    });
    startBtn.onclick = () => {
        clearTimeout(hideTimeoutId);
        startBtn.disabled = true;
        cancelBtn.style.display = 'block';
        autoScrollAndDownload();
    };

    // 按钮:取消
    const cancelBtn = document.createElement('button');
    cancelBtn.innerText = '❌ 取消';
    Object.assign(cancelBtn.style, {
        position: 'fixed', top: '100px', right: '20px',
        zIndex: 10000, padding: '10px',
        backgroundColor: '#ff4d4f', color: '#fff',
        border: 'none', borderRadius: '5px',
        cursor: 'pointer', display: 'none',
    });
    cancelBtn.onclick = () => {
        cancelDownload = true;
        cancelBtn.innerText = '⏳ 停止中...';
    };

    // 按钮:重置进度
    const resetBtn = document.createElement('button');
    resetBtn.innerText = '🔄 重置进度';
    Object.assign(resetBtn.style, {
        position: 'fixed', top: '200px', right: '20px',
        zIndex: 10001, padding: '10px',
        backgroundColor: '#888', color: '#fff',
        border: 'none', borderRadius: '5px',
        cursor: 'pointer',
    });
    resetBtn.onclick = () => {
        localStorage.removeItem(KEY_COUNT);
        localStorage.removeItem(KEY_POS);
        downloadedCount = 0;
        lastScrollPos   = 0;
        updateProgress('🔄 进度已重置');
        clearTimeout(hideTimeoutId);
        hideTimeoutId = setTimeout(() => {
            updateProgress('准备中...');
            progressBox.style.display = 'none';
        }, 3000);
    };

    document.body.appendChild(startBtn);
    document.body.appendChild(cancelBtn);
    document.body.appendChild(resetBtn);

    // 初始化提示
    updateProgress(`准备中... (已下载 ${downloadedCount} 张)`);
})();