您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
当前为
// ==UserScript== // @name Twitter/X Media Batch Downloader // @description Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality. // @icon https://raw.githubusercontent.com/afkarxyz/Twitter-X-Media-Batch-Downloader/refs/heads/main/Archived/icon.svg // @version 2.6 // @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 // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @connect gallerydl.vercel.app // @connect pbs.twimg.com // @connect video.twimg.com // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js // ==/UserScript== (function() { 'use strict'; const defaultSettings = { authToken: '', batchEnabled: true, batchSize: 100, timelineType: 'media', mediaType: 'all', concurrentDownloads: 25, cacheDuration: 360 }; function getSettings() { return { authToken: GM_getValue('authToken', defaultSettings.authToken), batchEnabled: GM_getValue('batchEnabled', defaultSettings.batchEnabled), batchSize: GM_getValue('batchSize', defaultSettings.batchSize), timelineType: GM_getValue('timelineType', defaultSettings.timelineType), mediaType: GM_getValue('mediaType', defaultSettings.mediaType), concurrentDownloads: GM_getValue('concurrentDownloads', defaultSettings.concurrentDownloads), cacheDuration: GM_getValue('cacheDuration', defaultSettings.cacheDuration) }; } function saveSettings(settings) { GM_setValue('authToken', settings.authToken); GM_setValue('batchEnabled', settings.batchEnabled); GM_setValue('batchSize', settings.batchSize); GM_setValue('timelineType', settings.timelineType); GM_setValue('mediaType', settings.mediaType); GM_setValue('concurrentDownloads', settings.concurrentDownloads); GM_setValue('cacheDuration', settings.cacheDuration); } function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } const cacheManager = { set: function(key, data) { const settings = getSettings(); const cacheItem = { data: data, timestamp: Date.now(), expiry: Date.now() + (settings.cacheDuration * 60 * 1000) }; localStorage.setItem(`twitter_dl_${key}`, JSON.stringify(cacheItem)); }, get: function(key) { const cacheItem = localStorage.getItem(`twitter_dl_${key}`); if (!cacheItem) return null; try { const parsed = JSON.parse(cacheItem); if (Date.now() > parsed.expiry) { localStorage.removeItem(`twitter_dl_${key}`); return null; } return parsed.data; } catch (e) { localStorage.removeItem(`twitter_dl_${key}`); return null; } }, clear: function() { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith('twitter_dl_')) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); } }; function createDownloadIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("viewBox", "0 0 512 512"); svg.setAttribute("width", "18"); svg.setAttribute("height", "18"); svg.style.verticalAlign = "middle"; svg.style.cursor = "pointer"; const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); const style = document.createElementNS("http://www.w3.org/2000/svg", "style"); style.textContent = ".fa-secondary{opacity:.4}"; defs.appendChild(style); svg.appendChild(defs); const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); secondaryPath.setAttribute("class", "fa-secondary"); secondaryPath.setAttribute("fill", "currentColor"); secondaryPath.setAttribute( "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" ); svg.appendChild(secondaryPath); const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); primaryPath.setAttribute("class", "fa-primary"); primaryPath.setAttribute("fill", "currentColor"); primaryPath.setAttribute( "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.appendChild(primaryPath); return svg; } function createGithubIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("width", "24"); svg.setAttribute("height", "24"); svg.style.verticalAlign = "middle"; svg.style.marginRight = "8px"; const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("fill", "currentColor"); path.setAttribute( "d", "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" ); svg.appendChild(path); return svg; } function createConfirmDialog(message, onConfirm, onCancel) { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10001; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background-color: #15202b; color: white; border-radius: 16px; width: 300px; max-width: 90%; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); overflow: hidden; `; const header = document.createElement('div'); header.style.cssText = ` padding: 16px; border-bottom: 1px solid #334155; font-weight: bold; font-size: 16px; text-align: center; `; header.textContent = 'Confirmation'; const content = document.createElement('div'); content.style.cssText = ` padding: 16px; text-align: center; `; content.textContent = message; const buttons = document.createElement('div'); buttons.style.cssText = ` display: flex; padding: 16px; border-top: 1px solid #334155; `; const cancelButton = document.createElement('button'); cancelButton.style.cssText = ` flex: 1; background-color: #64748b; color: white; border: none; border-radius: 9999px; padding: 8px 16px; margin-right: 8px; font-weight: bold; cursor: pointer; text-align: center; transition: background-color 0.2s; `; cancelButton.textContent = 'Cancel'; cancelButton.addEventListener('mouseenter', () => { cancelButton.style.backgroundColor = '#475569'; }); cancelButton.addEventListener('mouseleave', () => { cancelButton.style.backgroundColor = '#64748b'; }); cancelButton.onclick = () => { document.body.removeChild(overlay); if (onCancel) onCancel(); }; const confirmButton = document.createElement('button'); confirmButton.style.cssText = ` flex: 1; background-color: #ef4444; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; text-align: center; transition: background-color 0.2s; `; confirmButton.textContent = 'Confirm'; confirmButton.addEventListener('mouseenter', () => { confirmButton.style.backgroundColor = '#dc2626'; }); confirmButton.addEventListener('mouseleave', () => { confirmButton.style.backgroundColor = '#ef4444'; }); confirmButton.onclick = () => { document.body.removeChild(overlay); if (onConfirm) onConfirm(); }; buttons.appendChild(cancelButton); buttons.appendChild(confirmButton); dialog.appendChild(header); dialog.appendChild(content); dialog.appendChild(buttons); overlay.appendChild(dialog); document.body.appendChild(overlay); } function extractUsername() { const pathParts = window.location.pathname.split('/').filter(part => part); if (pathParts.length > 0) { return pathParts[0]; } return null; } function formatDate(dateString) { const date = new Date(dateString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}${month}${day}_${hours}${minutes}${seconds}`; } function getCurrentTimestamp() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${year}${month}${day}_${hours}${minutes}${seconds}`; } function fetchData(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'json', onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`Request failed with status ${response.status}`)); } }, onerror: function() { reject(new Error('Network error')); } }); }); } function fetchBinary(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`Request failed with status ${response.status}`)); } }, onerror: function() { reject(new Error('Network error')); } }); }); } function getMediaTypeLabel(mediaType) { switch(mediaType) { case 'image': return 'Image'; case 'video': return 'Video'; case 'gif': return 'GIF'; default: return 'Media'; } } function createModal(username) { const existingModal = document.getElementById('media-downloader-modal'); if (existingModal) { existingModal.remove(); } const settings = getSettings(); const modal = document.createElement('div'); modal.id = 'media-downloader-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; `; const modalContent = document.createElement('div'); modalContent.style.cssText = ` background-color: #15202b; color: white; border-radius: 16px; width: 500px; max-width: 90%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); `; const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #334155; `; const title = document.createElement('h2'); title.textContent = `Download Media: ${username}`; title.style.cssText = ` margin: 0; font-size: 18px; font-weight: bold; `; const closeButton = document.createElement('button'); closeButton.innerHTML = '×'; closeButton.style.cssText = ` background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s; `; closeButton.addEventListener('mouseenter', () => { closeButton.style.color = '#0ea5e9'; }); closeButton.addEventListener('mouseleave', () => { closeButton.style.color = 'white'; }); closeButton.onclick = () => modal.remove(); header.appendChild(title); header.appendChild(closeButton); const tabs = document.createElement('div'); tabs.style.cssText = ` display: flex; border-bottom: 1px solid #334155; `; const mainTab = document.createElement('div'); mainTab.textContent = 'Main'; mainTab.className = 'active-tab'; mainTab.style.cssText = ` padding: 12px 16px; cursor: pointer; flex: 1; text-align: center; border-bottom: 2px solid #0ea5e9; `; const settingsTab = document.createElement('div'); settingsTab.textContent = 'Settings'; settingsTab.style.cssText = ` padding: 12px 16px; cursor: pointer; flex: 1; text-align: center; color: #8899a6; `; tabs.appendChild(mainTab); tabs.appendChild(settingsTab); const mainContent = document.createElement('div'); mainContent.style.cssText = ` padding: 16px; `; const settingsContent = document.createElement('div'); settingsContent.style.cssText = ` padding: 16px; display: none; `; const fetchButton = document.createElement('button'); fetchButton.textContent = 'Fetch Media'; fetchButton.style.cssText = ` background-color: #0ea5e9; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-bottom: 16px; width: 100%; text-align: center; transition: background-color 0.2s; `; fetchButton.addEventListener('mouseenter', () => { fetchButton.style.backgroundColor = '#0284c7'; }); fetchButton.addEventListener('mouseleave', () => { fetchButton.style.backgroundColor = '#0ea5e9'; }); const infoContainer = document.createElement('div'); infoContainer.style.cssText = ` background-color: #192734; border-radius: 8px; padding: 12px; margin-bottom: 16px; display: none; `; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 8px; margin-bottom: 16px; `; const downloadCurrentButton = document.createElement('button'); downloadCurrentButton.textContent = 'Download Current Batch'; downloadCurrentButton.style.cssText = ` background-color: #0ea5e9; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; flex: 1; display: none; text-align: center; transition: background-color 0.2s; `; downloadCurrentButton.addEventListener('mouseenter', () => { downloadCurrentButton.style.backgroundColor = '#0284c7'; }); downloadCurrentButton.addEventListener('mouseleave', () => { downloadCurrentButton.style.backgroundColor = '#0ea5e9'; }); const downloadAllButton = document.createElement('button'); downloadAllButton.textContent = 'Download All Batches'; downloadAllButton.style.cssText = ` background-color: #22c55e; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; flex: 1; display: none; text-align: center; transition: background-color 0.2s; `; downloadAllButton.addEventListener('mouseenter', () => { downloadAllButton.style.backgroundColor = '#16a34a'; }); downloadAllButton.addEventListener('mouseleave', () => { downloadAllButton.style.backgroundColor = '#22c55e'; }); buttonContainer.appendChild(downloadCurrentButton); buttonContainer.appendChild(downloadAllButton); const nextBatchButton = document.createElement('button'); nextBatchButton.textContent = 'Next Batch'; nextBatchButton.style.cssText = ` background-color: #0ea5e9; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-bottom: 16px; width: 100%; display: none; text-align: center; transition: background-color 0.2s; `; nextBatchButton.addEventListener('mouseenter', () => { nextBatchButton.style.backgroundColor = '#0284c7'; }); nextBatchButton.addEventListener('mouseleave', () => { nextBatchButton.style.backgroundColor = '#0ea5e9'; }); const autoBatchButton = document.createElement('button'); autoBatchButton.textContent = 'Auto Batch'; autoBatchButton.style.cssText = ` background-color: #6366f1; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-bottom: 16px; width: 100%; display: none; text-align: center; transition: background-color 0.2s; `; autoBatchButton.addEventListener('mouseenter', () => { autoBatchButton.style.backgroundColor = '#4f46e5'; }); autoBatchButton.addEventListener('mouseleave', () => { autoBatchButton.style.backgroundColor = '#6366f1'; }); const stopBatchButton = document.createElement('button'); stopBatchButton.textContent = 'Stop Batch'; stopBatchButton.style.cssText = ` background-color: #ef4444; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-bottom: 16px; width: 100%; display: none; text-align: center; transition: background-color 0.2s; `; stopBatchButton.addEventListener('mouseenter', () => { stopBatchButton.style.backgroundColor = '#dc2626'; }); stopBatchButton.addEventListener('mouseleave', () => { stopBatchButton.style.backgroundColor = '#ef4444'; }); const progressContainer = document.createElement('div'); progressContainer.style.cssText = ` margin-top: 16px; display: none; `; const progressText = document.createElement('div'); progressText.style.cssText = ` margin-bottom: 8px; font-size: 14px; text-align: center; `; progressText.textContent = 'Downloading...'; const progressBar = document.createElement('div'); progressBar.style.cssText = ` width: 100%; height: 8px; background-color: #192734; border-radius: 4px; overflow: hidden; `; const progressFill = document.createElement('div'); progressFill.style.cssText = ` height: 100%; width: 0%; background-color: #0ea5e9; transition: width 0.3s; `; progressBar.appendChild(progressFill); progressContainer.appendChild(progressText); progressContainer.appendChild(progressBar); mainContent.appendChild(fetchButton); mainContent.appendChild(infoContainer); mainContent.appendChild(buttonContainer); mainContent.appendChild(nextBatchButton); mainContent.appendChild(autoBatchButton); mainContent.appendChild(stopBatchButton); mainContent.appendChild(progressContainer); const settingsForm = document.createElement('div'); settingsForm.style.cssText = ` display: flex; flex-direction: column; gap: 16px; `; const tokenGroup = document.createElement('div'); tokenGroup.style.cssText = ` display: flex; flex-direction: column; gap: 8px; `; const tokenLabel = document.createElement('label'); tokenLabel.textContent = 'Auth Token:'; tokenLabel.style.cssText = ` font-size: 14px; font-weight: bold; `; const tokenInputContainer = document.createElement('div'); tokenInputContainer.style.cssText = ` position: relative; display: flex; align-items: center; `; const tokenInput = document.createElement('input'); tokenInput.type = 'text'; tokenInput.value = settings.authToken; tokenInput.style.cssText = ` background-color: #192734; border: 1px solid #334155; border-radius: 4px; padding: 8px 12px; color: white; width: 100%; box-sizing: border-box; `; tokenInput.addEventListener('input', () => { const newSettings = getSettings(); newSettings.authToken = tokenInput.value; saveSettings(newSettings); tokenClearButton.style.display = tokenInput.value ? 'block' : 'none'; }); const tokenClearButton = document.createElement('button'); tokenClearButton.innerHTML = '×'; tokenClearButton.style.cssText = ` position: absolute; right: 8px; background: none; border: none; color: #8899a6; font-size: 18px; cursor: pointer; padding: 0; display: ${settings.authToken ? 'block' : 'none'}; `; tokenClearButton.addEventListener('click', () => { tokenInput.value = ''; const newSettings = getSettings(); newSettings.authToken = ''; saveSettings(newSettings); tokenClearButton.style.display = 'none'; }); tokenInputContainer.appendChild(tokenInput); tokenInputContainer.appendChild(tokenClearButton); tokenGroup.appendChild(tokenLabel); tokenGroup.appendChild(tokenInputContainer); const batchGroup = document.createElement('div'); batchGroup.style.cssText = ` display: flex; align-items: center; gap: 8px; `; const batchLabel = document.createElement('label'); batchLabel.textContent = 'Batch:'; batchLabel.style.cssText = ` font-size: 14px; font-weight: bold; flex: 1; `; const batchToggle = document.createElement('div'); batchToggle.style.cssText = ` position: relative; width: 50px; height: 24px; background-color: ${settings.batchEnabled ? '#0ea5e9' : '#334155'}; border-radius: 12px; cursor: pointer; transition: background-color 0.3s; `; const batchToggleHandle = document.createElement('div'); batchToggleHandle.style.cssText = ` position: absolute; top: 2px; left: ${settings.batchEnabled ? '28px' : '2px'}; width: 20px; height: 20px; background-color: white; border-radius: 50%; transition: left 0.3s; `; batchToggle.appendChild(batchToggleHandle); batchToggle.addEventListener('click', () => { const newSettings = getSettings(); newSettings.batchEnabled = !newSettings.batchEnabled; saveSettings(newSettings); batchToggle.style.backgroundColor = newSettings.batchEnabled ? '#0ea5e9' : '#334155'; batchToggleHandle.style.left = newSettings.batchEnabled ? '28px' : '2px'; batchSizeGroup.style.display = newSettings.batchEnabled ? 'flex' : 'none'; }); batchGroup.appendChild(batchLabel); batchGroup.appendChild(batchToggle); const batchSizeGroup = document.createElement('div'); batchSizeGroup.style.cssText = ` display: ${settings.batchEnabled ? 'flex' : 'none'}; flex-direction: column; gap: 8px; `; const batchSizeLabel = document.createElement('label'); batchSizeLabel.textContent = 'Batch Size:'; batchSizeLabel.style.cssText = ` font-size: 14px; font-weight: bold; `; const batchSizeSelect = document.createElement('select'); batchSizeSelect.style.cssText = ` background-color: #192734; border: 1px solid #334155; border-radius: 4px; padding: 8px 12px; color: white; width: 100%; box-sizing: border-box; `; const batchSizes = [50, 100, 150, 200]; batchSizes.forEach(size => { const option = document.createElement('option'); option.value = size; option.textContent = size; option.selected = size === settings.batchSize; batchSizeSelect.appendChild(option); }); batchSizeSelect.addEventListener('change', () => { const newSettings = getSettings(); newSettings.batchSize = parseInt(batchSizeSelect.value); saveSettings(newSettings); }); batchSizeGroup.appendChild(batchSizeLabel); batchSizeGroup.appendChild(batchSizeSelect); const timelineTypeGroup = document.createElement('div'); timelineTypeGroup.style.cssText = ` display: flex; flex-direction: column; gap: 8px; `; const timelineTypeLabel = document.createElement('label'); timelineTypeLabel.textContent = 'Timeline Type:'; timelineTypeLabel.style.cssText = ` font-size: 14px; font-weight: bold; `; const timelineTypeSelect = document.createElement('select'); timelineTypeSelect.style.cssText = ` background-color: #192734; border: 1px solid #334155; border-radius: 4px; padding: 8px 12px; color: white; width: 100%; box-sizing: border-box; `; const timelineTypes = [ { value: 'media', label: 'Media' }, { value: 'timeline', label: 'Post' }, { value: 'tweets', label: 'Tweets' }, { value: 'with_replies', label: 'Replies' } ]; timelineTypes.forEach(type => { const option = document.createElement('option'); option.value = type.value; option.textContent = type.label; option.selected = type.value === settings.timelineType; timelineTypeSelect.appendChild(option); }); timelineTypeSelect.addEventListener('change', () => { const newSettings = getSettings(); newSettings.timelineType = timelineTypeSelect.value; saveSettings(newSettings); }); timelineTypeGroup.appendChild(timelineTypeLabel); timelineTypeGroup.appendChild(timelineTypeSelect); const mediaTypeGroup = document.createElement('div'); mediaTypeGroup.style.cssText = ` display: flex; flex-direction: column; gap: 8px; `; const mediaTypeLabel = document.createElement('label'); mediaTypeLabel.textContent = 'Media Type:'; mediaTypeLabel.style.cssText = ` font-size: 14px; font-weight: bold; `; const mediaTypeSelect = document.createElement('select'); mediaTypeSelect.style.cssText = ` background-color: #192734; border: 1px solid #334155; border-radius: 4px; padding: 8px 12px; color: white; width: 100%; box-sizing: border-box; `; const mediaTypes = [ { value: 'all', label: 'All' }, { value: 'image', label: 'Image' }, { value: 'video', label: 'Video' }, { value: 'gif', label: 'GIF' } ]; mediaTypes.forEach(type => { const option = document.createElement('option'); option.value = type.value; option.textContent = type.label; option.selected = type.value === settings.mediaType; mediaTypeSelect.appendChild(option); }); mediaTypeSelect.addEventListener('change', () => { const newSettings = getSettings(); newSettings.mediaType = mediaTypeSelect.value; saveSettings(newSettings); }); mediaTypeGroup.appendChild(mediaTypeLabel); mediaTypeGroup.appendChild(mediaTypeSelect); const concurrentGroup = document.createElement('div'); concurrentGroup.style.cssText = ` display: flex; flex-direction: column; gap: 8px; `; const concurrentLabel = document.createElement('label'); concurrentLabel.textContent = 'Batch Download Items:'; concurrentLabel.style.cssText = ` font-size: 14px; font-weight: bold; `; const concurrentSelect = document.createElement('select'); concurrentSelect.style.cssText = ` background-color: #192734; border: 1px solid #334155; border-radius: 4px; padding: 8px 12px; color: white; width: 100%; box-sizing: border-box; `; const concurrentSizes = [5, 10, 20, 25, 50]; concurrentSizes.forEach(size => { const option = document.createElement('option'); option.value = size; option.textContent = size; option.selected = size === settings.concurrentDownloads; concurrentSelect.appendChild(option); }); concurrentSelect.addEventListener('change', () => { const newSettings = getSettings(); newSettings.concurrentDownloads = parseInt(concurrentSelect.value); saveSettings(newSettings); }); concurrentGroup.appendChild(concurrentLabel); concurrentGroup.appendChild(concurrentSelect); const cacheDurationGroup = document.createElement('div'); cacheDurationGroup.style.cssText = ` display: flex; flex-direction: column; gap: 8px; `; const cacheDurationLabel = document.createElement('label'); cacheDurationLabel.textContent = 'Cache Duration (Hour):'; cacheDurationLabel.style.cssText = ` font-size: 14px; font-weight: bold; `; const cacheDurationSelect = document.createElement('select'); cacheDurationSelect.style.cssText = ` background-color: #192734; border: 1px solid #334155; border-radius: 4px; padding: 8px 12px; color: white; width: 100%; box-sizing: border-box; `; for (let i = 1; i <= 24; i++) { const option = document.createElement('option'); option.value = i * 60; option.textContent = `${i} Hour${i > 1 ? 's' : ''}`; option.selected = i === 6 || (settings.cacheDuration === i * 60); cacheDurationSelect.appendChild(option); } cacheDurationSelect.addEventListener('change', () => { const newSettings = getSettings(); newSettings.cacheDuration = parseInt(cacheDurationSelect.value); saveSettings(newSettings); }); cacheDurationGroup.appendChild(cacheDurationLabel); cacheDurationGroup.appendChild(cacheDurationSelect); const clearCacheButton = document.createElement('button'); clearCacheButton.textContent = 'Clear Cache'; clearCacheButton.style.cssText = ` background-color: #ef4444; color: white; border: none; border-radius: 9999px; padding: 8px 16px; font-weight: bold; cursor: pointer; margin-top: 16px; width: 100%; text-align: center; transition: background-color 0.2s; `; clearCacheButton.addEventListener('mouseenter', () => { clearCacheButton.style.backgroundColor = '#dc2626'; }); clearCacheButton.addEventListener('mouseleave', () => { clearCacheButton.style.backgroundColor = '#ef4444'; }); clearCacheButton.addEventListener('click', () => { createConfirmDialog('Are you sure you want to clear the cache?', () => { cacheManager.clear(); const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: #0ea5e9; color: white; padding: 12px 24px; border-radius: 9999px; font-weight: bold; z-index: 10002; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); text-align: center; `; notification.textContent = 'Cache cleared successfully'; document.body.appendChild(notification); setTimeout(() => { document.body.removeChild(notification); }, 3000); }); }); const githubLink = document.createElement('a'); githubLink.href = 'https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader'; githubLink.target = '_blank'; githubLink.style.cssText = ` display: flex; align-items: center; justify-content: center; color: #8899a6; text-decoration: none; margin-top: 16px; padding: 8px; border-radius: 8px; transition: background-color 0.2s, color 0.2s; `; githubLink.innerHTML = createGithubIcon().outerHTML + 'GitHub Repository'; githubLink.addEventListener('mouseenter', () => { githubLink.style.backgroundColor = '#192734'; githubLink.style.color = '#0ea5e9'; }); githubLink.addEventListener('mouseleave', () => { githubLink.style.backgroundColor = 'transparent'; githubLink.style.color = '#8899a6'; }); settingsForm.appendChild(tokenGroup); settingsForm.appendChild(batchGroup); settingsForm.appendChild(batchSizeGroup); settingsForm.appendChild(timelineTypeGroup); settingsForm.appendChild(mediaTypeGroup); settingsForm.appendChild(concurrentGroup); settingsForm.appendChild(cacheDurationGroup); settingsForm.appendChild(clearCacheButton); settingsForm.appendChild(githubLink); settingsContent.appendChild(settingsForm); mainTab.addEventListener('click', () => { mainTab.style.borderBottom = '2px solid #0ea5e9'; mainTab.style.color = 'white'; settingsTab.style.borderBottom = 'none'; settingsTab.style.color = '#8899a6'; mainContent.style.display = 'block'; settingsContent.style.display = 'none'; }); settingsTab.addEventListener('click', () => { settingsTab.style.borderBottom = '2px solid #0ea5e9'; settingsTab.style.color = 'white'; mainTab.style.borderBottom = 'none'; mainTab.style.color = '#8899a6'; settingsContent.style.display = 'block'; mainContent.style.display = 'none'; }); modalContent.appendChild(header); modalContent.appendChild(tabs); modalContent.appendChild(mainContent); modalContent.appendChild(settingsContent); modal.appendChild(modalContent); let mediaData = { username: username, currentPage: 0, mediaItems: [], allMediaItems: [], hasMore: false, downloading: false, totalDownloaded: 0, totalToDownload: 0, totalItems: 0, autoBatchRunning: false }; fetchButton.addEventListener('click', async () => { const settings = getSettings(); if (!settings.authToken) { alert('Please enter your auth token in the Settings tab'); settingsTab.click(); return; } infoContainer.style.display = 'none'; buttonContainer.style.display = 'none'; downloadCurrentButton.style.display = 'none'; downloadAllButton.style.display = 'none'; nextBatchButton.style.display = 'none'; autoBatchButton.style.display = 'none'; stopBatchButton.style.display = 'none'; progressContainer.style.display = 'none'; fetchButton.disabled = true; fetchButton.textContent = 'Fetching...'; try { const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}`; let data = cacheManager.get(cacheKey); if (!data) { let url; if (settings.batchEnabled) { url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}`; } else { url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}`; } data = await fetchData(url); cacheManager.set(cacheKey, data); } if (data.timeline && data.timeline.length > 0) { mediaData.mediaItems = data.timeline; mediaData.hasMore = data.metadata.has_more; mediaData.totalItems = data.total_urls; if (mediaData.currentPage === 0) { mediaData.allMediaItems = [...data.timeline]; } else { mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline]; } const mediaTypeLabel = getMediaTypeLabel(settings.mediaType); if (settings.batchEnabled) { infoContainer.innerHTML = ` <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div> <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div> <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div> <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div> `; } else { const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1; infoContainer.innerHTML = ` <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div> <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div> <div style="margin-top: 8px;"><strong>Part:</strong> ${currentPart}</div> <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div> `; } infoContainer.style.display = 'block'; buttonContainer.style.display = 'flex'; downloadCurrentButton.style.display = 'block'; downloadAllButton.style.display = 'block'; if (settings.batchEnabled && mediaData.hasMore) { nextBatchButton.style.display = 'block'; autoBatchButton.style.display = 'block'; } downloadCurrentButton.onclick = () => downloadMedia(false); downloadAllButton.onclick = () => downloadMedia(true); fetchButton.disabled = false; fetchButton.textContent = 'Fetch Media'; } else { infoContainer.innerHTML = '<div style="color: #ef4444;">No media found or invalid token</div>'; infoContainer.style.display = 'block'; fetchButton.disabled = false; fetchButton.textContent = 'Fetch Media'; } } catch (error) { console.error('Error fetching media:', error); infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`; infoContainer.style.display = 'block'; fetchButton.disabled = false; fetchButton.textContent = 'Fetch Media'; } }); nextBatchButton.addEventListener('click', () => { mediaData.currentPage++; fetchButton.click(); }); autoBatchButton.addEventListener('click', () => { if (mediaData.autoBatchRunning) { return; } mediaData.autoBatchRunning = true; autoBatchButton.style.display = 'none'; stopBatchButton.style.display = 'block'; nextBatchButton.style.display = 'none'; startAutoBatch(); }); stopBatchButton.addEventListener('click', () => { createConfirmDialog('Stop auto batch download?', () => { mediaData.autoBatchRunning = false; stopBatchButton.style.display = 'none'; autoBatchButton.style.display = 'block'; if (mediaData.hasMore) { nextBatchButton.style.display = 'block'; } }); }); async function startAutoBatch() { while (mediaData.hasMore && mediaData.autoBatchRunning) { mediaData.currentPage++; downloadCurrentButton.disabled = true; downloadAllButton.disabled = true; await new Promise(resolve => { const settings = getSettings(); const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}`; let data = cacheManager.get(cacheKey); if (data) { processNextBatch(data); resolve(); } else { let url; if (settings.batchEnabled) { url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}`; } else { url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}`; } fetchData(url).then(data => { cacheManager.set(cacheKey, data); processNextBatch(data); resolve(); }).catch(error => { console.error('Error in auto batch:', error); mediaData.autoBatchRunning = false; stopBatchButton.style.display = 'none'; autoBatchButton.style.display = 'block'; downloadCurrentButton.disabled = false; downloadAllButton.disabled = false; if (mediaData.hasMore) { nextBatchButton.style.display = 'block'; } resolve(); }); } }); await new Promise(resolve => setTimeout(resolve, 1000)); } if (mediaData.autoBatchRunning) { mediaData.autoBatchRunning = false; stopBatchButton.style.display = 'none'; autoBatchButton.style.display = 'none'; } downloadCurrentButton.disabled = false; downloadAllButton.disabled = false; } function processNextBatch(data) { if (data.timeline && data.timeline.length > 0) { mediaData.mediaItems = data.timeline; mediaData.hasMore = data.metadata.has_more; mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline]; const settings = getSettings(); const mediaTypeLabel = getMediaTypeLabel(settings.mediaType); infoContainer.innerHTML = ` <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div> <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div> <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div> <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div> `; if (!mediaData.hasMore) { nextBatchButton.style.display = 'none'; autoBatchButton.style.display = 'none'; stopBatchButton.style.display = 'none'; } } else { mediaData.hasMore = false; nextBatchButton.style.display = 'none'; autoBatchButton.style.display = 'none'; stopBatchButton.style.display = 'none'; } } function chunkMediaItems(items) { const chunks = []; for (let i = 0; i < items.length; i += 500) { chunks.push(items.slice(i, i + 500)); } return chunks; } async function downloadMedia(downloadAll) { if (mediaData.downloading) return; mediaData.downloading = true; const settings = getSettings(); const timestamp = getCurrentTimestamp(); let itemsToDownload; if (downloadAll) { itemsToDownload = mediaData.allMediaItems; } else { itemsToDownload = mediaData.mediaItems; } mediaData.totalToDownload = itemsToDownload.length; mediaData.totalDownloaded = 0; progressText.textContent = `Downloading 0/${formatNumber(mediaData.totalToDownload)}`; progressFill.style.width = '0%'; progressContainer.style.display = 'block'; fetchButton.disabled = true; downloadCurrentButton.disabled = true; downloadAllButton.disabled = true; nextBatchButton.disabled = true; autoBatchButton.disabled = true; stopBatchButton.disabled = true; const chunks = chunkMediaItems(itemsToDownload); for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; const zip = new JSZip(); const hasImages = chunk.some(item => item.type === 'photo'); const hasVideos = chunk.some(item => item.type === 'video'); const hasGifs = chunk.some(item => item.type === 'gif'); let imageFolder, videoFolder, gifFolder; if (settings.mediaType === 'all') { if (hasImages) imageFolder = zip.folder('image'); if (hasVideos) videoFolder = zip.folder('video'); if (hasGifs) gifFolder = zip.folder('gif'); } const filenameMap = {}; const concurrentBatches = []; for (let i = 0; i < chunk.length; i += settings.concurrentDownloads) { concurrentBatches.push(chunk.slice(i, i + settings.concurrentDownloads)); } for (const batch of concurrentBatches) { const downloadPromises = batch.map(async (item) => { try { const formattedDate = formatDate(item.date); let baseFilename = `${username}_${formattedDate}_${item.tweet_id}`; if (filenameMap[baseFilename] !== undefined) { filenameMap[baseFilename]++; baseFilename = `${baseFilename}_${String(filenameMap[baseFilename]).padStart(2, '0')}`; } else { filenameMap[baseFilename] = 0; } const fileExtension = item.type === 'photo' ? 'jpg' : 'mp4'; const filename = `${baseFilename}.${fileExtension}`; const blob = await fetchBinary(item.url); if (settings.mediaType === 'all') { if (item.type === 'photo') { imageFolder.file(filename, blob); } else if (item.type === 'video') { videoFolder.file(filename, blob); } else if (item.type === 'gif') { gifFolder.file(filename, blob); } } else { zip.file(filename, blob); } return true; } catch (error) { console.error(`Error downloading ${item.url}:`, error); return false; } }); await Promise.all(downloadPromises); mediaData.totalDownloaded += batch.length; progressText.textContent = `Downloading ${formatNumber(mediaData.totalDownloaded)}/${formatNumber(mediaData.totalToDownload)}`; progressFill.style.width = `${(mediaData.totalDownloaded / mediaData.totalToDownload) * 100}%`; } progressText.textContent = `Creating ZIP file ${chunkIndex + 1}/${chunks.length}...`; try { const zipBlob = await zip.generateAsync({ type: 'blob' }); let zipFilename; if (chunks.length === 1 && chunk.length < 500) { zipFilename = `${username}_${timestamp}.zip`; } else if (settings.batchEnabled && !downloadAll) { zipFilename = `${username}_${timestamp}_part_${String(mediaData.currentPage + 1).padStart(2, '0')}.zip`; } else { zipFilename = `${username}_${timestamp}_part_${String(chunkIndex + 1).padStart(2, '0')}.zip`; } const downloadLink = document.createElement('a'); downloadLink.href = URL.createObjectURL(zipBlob); downloadLink.download = zipFilename; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); } catch (error) { console.error('Error creating ZIP:', error); progressText.textContent = `Error creating ZIP ${chunkIndex + 1}: ${error.message}`; } } progressText.textContent = 'Download complete!'; progressFill.style.width = '100%'; setTimeout(() => { fetchButton.disabled = false; downloadCurrentButton.disabled = false; downloadAllButton.disabled = false; nextBatchButton.disabled = false; autoBatchButton.disabled = false; stopBatchButton.disabled = false; mediaData.downloading = false; }, 2000); } document.body.appendChild(modal); } function insertDownloadIcon() { const usernameDivs = document.querySelectorAll('[data-testid="UserName"]'); usernameDivs.forEach((usernameDiv) => { if (!usernameDiv.querySelector(".download-icon")) { const username = extractUsername(); if (!username) return; const verifiedButton = usernameDiv .querySelector('[aria-label*="verified"], [aria-label*="Verified"]') ?.closest("button"); const targetElement = verifiedButton ? verifiedButton.parentElement : usernameDiv.querySelector(".css-1jxf684")?.closest("span"); if (targetElement) { const downloadIcon = createDownloadIcon(); 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; margin-right: 6px; gap: 6px; padding: 0 3px; transition: transform 0.2s, color 0.2s; `; iconDiv.appendChild(downloadIcon); iconDiv.addEventListener("mouseenter", () => { iconDiv.style.transform = "scale(1.1)"; iconDiv.style.color = "#0ea5e9"; }); iconDiv.addEventListener("mouseleave", () => { iconDiv.style.transform = "scale(1)"; iconDiv.style.color = ""; }); iconDiv.addEventListener("click", (e) => { e.stopPropagation(); createModal(username); }); 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); } } }); } insertDownloadIcon(); let lastUrl = location.href; const observer = new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; setTimeout(insertDownloadIcon, 1000); } else { insertDownloadIcon(); } }); observer.observe(document.body, { childList: true, subtree: true, }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址