// ==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 // @downloadURL https://update.greasyfork.icu/scripts/533499/X%20%E5%8D%9A%E4%B8%BB%E5%9B%BE%E7%89%87%E5%88%86%E6%89%B9%E4%B8%8B%E8%BD%BD%EF%BC%88%E6%AF%8F%E6%89%B91000%E5%BC%A0%EF%BC%8C%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0%2B%E9%87%8D%E7%BD%AE%2B%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E8%B0%83%E8%AF%95%EF%BC%89.user.js // @updateURL https://update.greasyfork.icu/scripts/533499/X%20%E5%8D%9A%E4%B8%BB%E5%9B%BE%E7%89%87%E5%88%86%E6%89%B9%E4%B8%8B%E8%BD%BD%EF%BC%88%E6%AF%8F%E6%89%B91000%E5%BC%A0%EF%BC%8C%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0%2B%E9%87%8D%E7%BD%AE%2B%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E8%B0%83%E8%AF%95%EF%BC%89.meta.js // ==/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} 张)`); })();