您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Batch download all media images in original quality.
当前为
// ==UserScript== // @name Twitter/X Media Batch Downloader // @description Batch download all media images in original quality. // @icon https://www.google.com/s2/favicons?sz=64&domain=x.com // @version 1.4 // @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 // @connect pbs.twimg.com // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js // ==/UserScript== (function() { 'use strict'; const imageIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" style="vertical-align: middle; cursor: pointer;"> <path fill="currentColor" d="M448 80c8.8 0 16 7.2 16 16l0 319.8-5-6.5-136-176c-4.5-5.9-11.6-9.3-19-9.3s-14.4 3.4-19 9.3L202 340.7l-30.5-42.7C167 291.7 159.8 288 152 288s-15 3.7-19.5 10.1l-80 112L48 416.3l0-.3L48 96c0-8.8 7.2-16 16-16l384 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm80 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/> </svg>`; const zipIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="16" height="16"> <path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM96 48c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm0 64c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16zm-6.3 71.8c3.7-14 16.4-23.8 30.9-23.8l14.8 0c14.5 0 27.2 9.7 30.9 23.8l23.5 88.2c1.4 5.4 2.1 10.9 2.1 16.4c0 35.2-28.8 63.7-64 63.7s-64-28.5-64-63.7c0-5.5 .7-11.1 2.1-16.4l23.5-88.2zM112 336c-8.8 0-16 7.2-16 16s7.2 16 16 16l32 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0z"/> </svg>`; const downloadIcon = `<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>`; const pauseResumeIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="16" height="16"> <path fill="currentColor" d="M116.5 71.4c-9.5-7.9-22.8-9.7-34.1-4.4S64 83.6 64 96l0 320c0 12.4 7.2 23.7 18.4 29s24.5 3.6 34.1-4.4l192-160c7.3-6.1 11.5-15.1 11.5-24.6s-4.2-18.5-11.5-24.6l-192-160zM448 96c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 320c0 17.7 14.3 32 32 32s32-14.3 32-32l0-320zm128 0c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 320c0 17.7 14.3 32 32 32s32-14.3 32-32l0-320z"/> </svg>`; const stopIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="16" height="16"> <path fill="currentColor" d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/> </svg>`; let extractedUrls = []; let isScrolling = false; let isPaused = false; let shouldStop = false; let imageCounter; let controlPanel = null; let isDownloading = false; async function downloadImages() { if (isDownloading) return; isDownloading = true; const zip = new JSZip(); const username = window.location.pathname.split('/')[1]; const progressContainer = controlPanel.panel.querySelector('.progress-container'); const progressFill = progressContainer.querySelector('.progress-fill'); const progressText = progressContainer.querySelector('.progress-text'); const buttonsContainer = controlPanel.panel.querySelector('.buttons-container'); buttonsContainer.style.display = 'none'; progressContainer.style.display = 'block'; imageCounter.innerHTML = `${zipIcon} ${extractedUrls.length}`; let successfulDownloads = 0; const totalImages = extractedUrls.length; const batchSize = 5; const batches = []; for (let i = 0; i < extractedUrls.length; i += batchSize) { const batch = extractedUrls.slice(i, i + batchSize).map(async (url, batchIndex) => { try { const response = await fetch(url, { method: 'GET', credentials: 'omit', headers: { 'Accept': 'image/jpeg,image/*', } }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const blob = await response.blob(); const fileNumber = (i + batchIndex + 1).toString(); zip.file(`${username}_${fileNumber}.jpg`, blob); successfulDownloads++; const progress = Math.round((successfulDownloads / totalImages) * 100); progressFill.style.width = `${progress}%`; progressText.textContent = `Downloading: (${successfulDownloads}/${totalImages}) ${progress}%`; return true; } catch (error) { console.error('Error downloading image:', error); return false; } }); batches.push(Promise.all(batch)); } for (const batch of batches) { await batch; } if (successfulDownloads > 0) { progressText.textContent = `Creating ZIP: (0/${successfulDownloads}) 0%`; const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 3 } }, metadata => { const progress = Math.round(metadata.percent); const processedImages = Math.round((progress / 100) * successfulDownloads); progressFill.style.width = `${progress}%`; progressText.textContent = `Creating ZIP: (${processedImages}/${successfulDownloads}) ${progress}%`; }); const downloadUrl = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = downloadUrl; a.download = `${username}_${successfulDownloads}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(downloadUrl); } isDownloading = false; hideControlPanel(); } function createControlPanel() { const styles = ` .control-panel { position: fixed; top: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; background-color: rgba(35, 35, 35, 0.75); padding: 12px; border-radius: 6px; transform: translateX(calc(100% + 16px)); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; pointer-events: none; width: 200px; } .control-panel.visible { transform: translateX(0); opacity: 1; pointer-events: all; } .control-panel.hiding { transform: translateX(calc(100% + 16px)); opacity: 0; pointer-events: none; } .buttons-container { display: flex; gap: 8px; margin-bottom: 8px; transition: display 0.3s ease; justify-content: center; width: 100%; } .control-button { padding: 8px 16px; border: none; border-radius: 6px; font-family: inherit; cursor: pointer; transition: background-color 0.2s ease; width: 120px; text-align: center; display: flex; align-items: center; justify-content: center; gap: 6px; color: white; font-size: 14px; flex: 1; max-width: 100px; } .pause-button { background-color: #1da1f2; } .pause-button:hover { background-color: #1a91da; } .stop-button { background-color: #dc2626; } .stop-button:hover { background-color: #b31f1f; } .image-counter { color: white; text-align: center; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 20px; } .progress-container { display: none; margin-top: 8px; width: 100%; } .progress-bar { width: 100%; height: 4px; background-color: #1a1a1a; border-radius: 2px; } .progress-fill { width: 0%; height: 100%; background-color: #1da1f2; border-radius: 2px; transition: width 0.3s ease; } .progress-text { color: white; font-size: 12px; text-align: center; margin-top: 4px; min-height: 16px; } `; if (!document.querySelector('#control-panel-styles')) { const styleSheet = document.createElement('style'); styleSheet.id = 'control-panel-styles'; styleSheet.textContent = styles; document.head.appendChild(styleSheet); } const panel = document.createElement('div'); panel.className = 'control-panel'; const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'buttons-container'; const pauseButton = document.createElement('button'); pauseButton.className = 'control-button pause-button'; pauseButton.innerHTML = `${pauseResumeIcon}Pause`; pauseButton.onclick = () => { isPaused = !isPaused; pauseButton.innerHTML = `${pauseResumeIcon}${isPaused ? 'Resume' : 'Pause'}`; }; const stopButton = document.createElement('button'); stopButton.className = 'control-button stop-button'; stopButton.innerHTML = `${stopIcon}Stop`; stopButton.onclick = () => { shouldStop = true; }; const counter = document.createElement('div'); counter.className = 'image-counter'; counter.innerHTML = `${imageIcon} 0`; const progressContainer = document.createElement('div'); progressContainer.className = 'progress-container'; progressContainer.innerHTML = ` <div class="progress-bar"> <div class="progress-fill"></div> </div> <div class="progress-text">0%</div> `; buttonsContainer.appendChild(pauseButton); buttonsContainer.appendChild(stopButton); panel.appendChild(buttonsContainer); panel.appendChild(counter); panel.appendChild(progressContainer); document.body.appendChild(panel); requestAnimationFrame(() => { requestAnimationFrame(() => { panel.classList.add('visible'); }); }); return { counter, panel }; } 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=360x360')) { const largeSrc = src.replace('name=360x360', 'name=orig'); if (!extractedUrls.includes(largeSrc)) { extractedUrls.push(largeSrc); newUrlsFound = true; imageCounter.innerHTML = `${imageIcon} ${extractedUrls.length}`; } } }); } }); return newUrlsFound; } async function smoothScroll(distance, duration) { const start = window.pageYOffset; const startTime = 'now' in window.performance ? performance.now() : new Date().getTime(); function scroll() { const now = 'now' in window.performance ? performance.now() : new Date().getTime(); const time = Math.min(1, ((now - startTime) / duration)); window.scrollTo({ top: start + (distance * time), behavior: 'auto' }); if (time < 1) { requestAnimationFrame(scroll); } } scroll(); } async function scrollAndExtract() { if (isScrolling) return; isScrolling = true; const getScrollStep = () => Math.max(window.innerHeight || document.documentElement.clientHeight); const scrollDuration = 1000; const waitTime = 1000; while (!shouldStop) { if (isPaused) { await new Promise(resolve => setTimeout(resolve, 500)); continue; } const currentScrollStep = getScrollStep(); await smoothScroll(currentScrollStep, scrollDuration); await new Promise(resolve => setTimeout(resolve, waitTime)); const newUrlsFound = extractUrls(); if (!newUrlsFound) { await smoothScroll(currentScrollStep * 2, scrollDuration); await new Promise(resolve => setTimeout(resolve, waitTime)); if (!extractUrls()) break; } } isScrolling = false; console.log('Finished extracting. Total unique URLs:', extractedUrls.size); if (shouldStop || !isPaused) { downloadImages(); } } function hideControlPanel() { if (controlPanel?.panel) { controlPanel.panel.classList.remove('visible'); controlPanel.panel.classList.add('hiding'); controlPanel.panel.addEventListener('transitionend', function handler(e) { if (e.propertyName === 'opacity') { controlPanel.panel.removeEventListener('transitionend', handler); controlPanel.panel.remove(); controlPanel = null; } }); } } function resetState() { extractedUrls = []; isScrolling = false; isPaused = false; shouldStop = false; imageCounter = null; if (controlPanel?.panel) { controlPanel.panel.remove(); controlPanel = null; } } 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: 4px; margin-right: 4px; gap: 4px; padding: 0 2px; transition: transform 0.2s, color 0.2s; `; iconDiv.innerHTML = downloadIcon; iconDiv.addEventListener('mouseenter', () => { iconDiv.style.transform = 'scale(1.1)'; iconDiv.style.color = '#1DA1F2'; }); iconDiv.addEventListener('mouseleave', () => { iconDiv.style.transform = 'scale(1)'; iconDiv.style.color = ''; }); iconDiv.addEventListener('click', async (e) => { e.stopPropagation(); const mediaTab = Array.from(document.querySelectorAll('[role="tab"]')) .find(el => el.textContent.includes('Media')); if (mediaTab) { mediaTab.click(); await new Promise(resolve => setTimeout(resolve, 1000)); initializeImageExtractor(); } }); 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); } } }); } function initializeImageExtractor() { const controls = createControlPanel(); controlPanel = controls; imageCounter = controls.counter; scrollAndExtract(); } insertDownloadIcon(); let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; resetState(); setTimeout(insertDownloadIcon, 1000); } else { insertDownloadIcon(); } }).observe(document.body, { childList: true, subtree: true }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址