// ==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.1 // @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/jszip@3.7.1/dist/jszip.min.js // @downloadURL none // ==/UserScript== (function() { 'use strict'; const imageIcon = ``; const zipIcon = ``; const downloadIcon = ``; const pauseResumeIcon = ``; const stopIcon = ``; 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'); 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: 12px; 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; } .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; } .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; } .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; } .progress-container { display: none; margin-top: 8px; } .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; } `; 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 = `