您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download all media images in original quality.
当前为
// ==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(); })();