您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Batch download all images and videos from a Twitter/X account in original quality.
当前为
// ==UserScript== // @name Twitter/X Media Batch Downloader // @description Batch download all images and videos from a Twitter/X account in original quality. // @icon https://raw.githubusercontent.com/afkarxyz/Twitter-X-Media-Batch-Downloader/refs/heads/main/Archived/icon.svg // @version 1.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 // @connect twitterxapis.vercel.app // @connect pbs.twimg.com // @connect video.twimg.com // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js // ==/UserScript== (() => { const mediaIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="16" height="16"> <path fill="currentColor" d="M256 48c-8.8 0-16 7.2-16 16l0 224c0 8.7 6.9 15.8 15.6 16l69.1-94.2c4.5-6.2 11.7-9.8 19.4-9.8s14.8 3.6 19.4 9.8L380 232.4l56-85.6c4.4-6.8 12-10.9 20.1-10.9s15.7 4.1 20.1 10.9L578.7 303.8c7.6-1.3 13.3-7.9 13.3-15.8l0-224c0-8.8-7.2-16-16-16L256 48zM192 64c0-35.3 28.7-64 64-64L576 0c35.3 0 64 28.7 64 64l0 224c0 35.3-28.7 64-64 64l-320 0c-35.3 0-64-28.7-64-64l0-224zm-56 64l24 0 0 48 0 88 0 112 0 8 0 80 192 0 0-80 48 0 0 80 48 0c8.8 0 16-7.2 16-16l0-64 48 0 0 64c0 35.3-28.7 64-64 64l-48 0-24 0-24 0-192 0-24 0-24 0-48 0c-35.3 0-64-28.7-64-64L0 192c0-35.3 28.7-64 64-64l48 0 24 0zm-24 48l-48 0c-8.8 0-16 7.2-16 16l0 48 64 0 0-64zm0 288l0-64-64 0 0 48c0 8.8 7.2 16 16 16l48 0zM48 352l64 0 0-64-64 0 0 64zM304 80a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/> </svg>`; const imageIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"> <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 videoIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"> <path fill="currentColor" d="M352 432l-192 0 0-112 0-40 192 0 0 40 0 112zm0-200l-192 0 0-40 0-112 192 0 0 112 0 40zM64 80l48 0 0 88-64 0 0-72c0-8.8 7.2-16 16-16zM48 216l64 0 0 80-64 0 0-80zm64 216l-48 0c-8.8 0-16-7.2-16-16l0-72 64 0 0 88zM400 168l0-88 48 0c8.8 0 16 7.2 16 16l0 72-64 0zm0 48l64 0 0 80-64 0 0-80zm0 128l64 0 0 72c0 8.8-7.2 16-16 16l-48 0 0-88zM448 32L64 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-64z"/> </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="18" height="18" 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 loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="vertical-align: middle;"> <path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity="0.25"/> <path fill="currentColor" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"> <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> </path> </svg>`; let controlPanel = null; let imageCounter; let isDownloading = false; async function fetchMetadata(username, url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url || `https://twitterxapis.vercel.app/metadata/${username}`, headers: { Accept: "application/json", }, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.timeline) { data.timeline = data.timeline.map((item, index) => ({ ...item, tweet_id: item.tweet_id || `${index}`, })); } resolve(data); } catch (error) { reject(error); } }, onerror: (error) => { reject(error); }, }); }); } async function downloadFile(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", headers: { Accept: "image/jpeg,image/*,video/*", }, onload: (response) => { resolve(response.response); }, onerror: (error) => { reject(error); }, }); }); } function createCustomMenu(username) { const menuOverlay = document.createElement("div"); menuOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.75); display: flex; justify-content: center; align-items: center; z-index: 10000; `; const menu = document.createElement("div"); menu.style.cssText = ` background-color: rgba(35, 35, 35, 0.75); border-radius: 6px; width: 300px; padding: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; box-sizing: border-box; `; const title = document.createElement("h2"); title.textContent = "Download Options"; title.style.cssText = ` margin-top: 0; margin-bottom: 15px; font-size: 16px; font-weight: bold; color: white; text-align: center; `; const tokenContainer = document.createElement("div"); tokenContainer.style.cssText = ` margin-bottom: 15px; padding: 10px; background-color: rgba(255, 255, 255, 0.1); border-radius: 4px; box-sizing: border-box; width: 100%; `; const tokenLabel = document.createElement("label"); tokenLabel.textContent = "Auth Token"; tokenLabel.style.cssText = ` display: block; margin-bottom: 5px; color: white; text-align: center; font-size: 14px; word-wrap: break-word; `; const tokenInput = document.createElement("input"); tokenInput.type = "text"; tokenInput.placeholder = "Enter Your Auth Token"; tokenInput.value = localStorage.getItem("twitter_auth_token") || ""; tokenInput.style.cssText = ` width: 100%; padding: 8px; border: none; border-radius: 4px; background-color: rgba(35, 35, 35, 0.9); color: white; font-size: 14px; box-sizing: border-box; overflow-wrap: break-word; word-wrap: break-word; word-break: break-all; `; tokenInput.addEventListener("input", (e) => { const value = e.target.value.trim(); if (value === "") { localStorage.removeItem("twitter_auth_token"); } else { localStorage.setItem("twitter_auth_token", value); } }); tokenInput.addEventListener("blur", (e) => { const value = e.target.value.trim(); if (value === "") { localStorage.removeItem("twitter_auth_token"); } else { localStorage.setItem("twitter_auth_token", value); } }); tokenContainer.appendChild(tokenLabel); tokenContainer.appendChild(tokenInput); const options = [ { name: "Media", icon: mediaIcon, getUrl: (token) => `https://twitterxapis.vercel.app/metadata/${username}/${token}`, }, { name: "Image", icon: imageIcon, getUrl: (token) => `https://twitterxapis.vercel.app/metadata/image/${username}/${token}`, }, { name: "Video", icon: videoIcon, getUrl: (token) => `https://twitterxapis.vercel.app/metadata/video/${username}/${token}`, }, ]; options.forEach((option) => { const button = document.createElement("button"); button.innerHTML = `${option.icon} ${option.name}`; button.style.cssText = ` display: flex; align-items: center; gap: 10px; margin-bottom: 10px; padding: 10px; width: 100%; border: none; background-color: rgba(255, 255, 255, 0.1); color: white; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; font-size: 14px; `; button.addEventListener("mouseenter", () => { button.style.backgroundColor = "rgba(255, 255, 255, 0.2)"; }); button.addEventListener("mouseleave", () => { button.style.backgroundColor = "rgba(255, 255, 255, 0.1)"; }); button.addEventListener("click", async () => { const token = tokenInput.value.trim(); if (!token) { alert("Please Enter an Auth Token"); return; } menuOverlay.remove(); try { const iconDiv = document.querySelector(".download-icon"); if (iconDiv) { iconDiv.innerHTML = loadingIcon; } const metadata = await fetchMetadata(username, option.getUrl(token)); if (iconDiv) { iconDiv.innerHTML = downloadIcon; } const controls = createControlPanel(); controlPanel = controls; imageCounter = controls.counter; downloadMedia(metadata, option.icon); } catch (error) { console.error("Error fetching metadata:", error); localStorage.removeItem("twitter_auth_token"); alert( "Failed to fetch media data. Please check your Auth Token and try again." ); const iconDiv = document.querySelector(".download-icon"); if (iconDiv) { iconDiv.innerHTML = downloadIcon; } } }); menu.appendChild(button); }); menu.insertBefore(tokenContainer, menu.firstChild); menu.insertBefore(title, menu.firstChild); menuOverlay.appendChild(menu); document.body.appendChild(menuOverlay); if (!tokenInput.value) { tokenInput.focus(); } menuOverlay.addEventListener("click", (e) => { if (e.target === menuOverlay) { menuOverlay.remove(); } }); } function getFileExtension(url) { if (url.includes("video.twimg.com")) return ".mp4"; return ".jpg"; } 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}`; } async function downloadMedia(metadata, icon) { if (isDownloading || !controlPanel?.panel) return; isDownloading = true; const zip = new JSZip(); const { account_info, timeline, total_urls } = metadata; const { name, nick } = account_info; 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"); if (!progressContainer || !progressFill || !progressText || !imageCounter) { console.error("Required elements not found"); isDownloading = false; return; } buttonsContainer?.style && (buttonsContainer.style.display = "none"); progressContainer.style.display = "block"; imageCounter.innerHTML = `${icon || mediaIcon} ${total_urls}`; let successfulDownloads = 0; const batchSize = 5; const batches = []; const filenameCounts = new Map(); for (let i = 0; i < timeline.length; i += batchSize) { const batch = timeline .slice(i, i + batchSize) .map(async ({ url, date }) => { try { const blob = await downloadFile(url); const fileExt = getFileExtension(url); const formattedDate = formatDate(date); const baseFileName = `${name}_${formattedDate}`; let fileName = baseFileName + fileExt; if (filenameCounts.has(baseFileName)) { const count = filenameCounts.get(baseFileName) + 1; filenameCounts.set(baseFileName, count); fileName = `${baseFileName}_${String(count).padStart( 2, "0" )}${fileExt}`; } else { filenameCounts.set(baseFileName, 0); } zip.file(fileName, blob); successfulDownloads++; const progress = Math.round( (successfulDownloads / total_urls) * 100 ); progressFill.style.width = `${progress}%`; progressText.textContent = `Downloading: (${successfulDownloads}/${total_urls}) ${progress}%`; console.log( `Downloaded: ${fileName} (${successfulDownloads}/${total_urls})` ); return true; } catch (error) { console.error("Error downloading media:", error, url); return false; } }); batches.push(Promise.all(batch)); await new Promise((resolve) => setTimeout(resolve, 100)); } for (const batch of batches) { await batch; } console.log(`Total successful downloads: ${successfulDownloads}`); console.log(`Total expected files: ${total_urls}`); if (successfulDownloads > 0) { imageCounter.innerHTML = `${zipIcon} ${successfulDownloads}`; 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 processedFiles = Math.round( (progress / 100) * successfulDownloads ); progressFill.style.width = `${progress}%`; progressText.textContent = `Creating ZIP: (${processedFiles}/${successfulDownloads}) ${progress}%`; } ); const downloadUrl = URL.createObjectURL(zipBlob); const a = document.createElement("a"); a.href = downloadUrl; a.download = `${name}_(${nick})_${successfulDownloads}`; 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; } .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 counter = document.createElement("div"); counter.className = "image-counter"; counter.innerHTML = `${mediaIcon} 0`; const progressContainer = document.createElement("div"); progressContainer.className = "progress-container"; progressContainer.innerHTML = ` <div class="progress-bar"> <div class="progress-fill"></div> <div class="progress-text">0%</div> `; panel.appendChild(counter); panel.appendChild(progressContainer); document.body.appendChild(panel); requestAnimationFrame(() => { requestAnimationFrame(() => { panel.classList.add("visible"); }); }); return { counter, panel, }; } 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 insertDownloadIcon() { const usernameDivs = document.querySelectorAll('[data-testid="UserName"]'); usernameDivs.forEach((usernameDiv) => { if (!usernameDiv.querySelector(".download-icon")) { 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 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.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", (e) => { e.stopPropagation(); const username = window.location.pathname.split("/")[1]; createCustomMenu(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 ); } } }); } function resetState() { imageCounter = null; if (controlPanel?.panel) { controlPanel.panel.remove(); controlPanel = null; } } 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或关注我们的公众号极客氢云获取最新地址