// ==UserScript== // @name 🚀 🎬 Enhanced Twitter Media Downloader | 推特媒体下载器 // @name:en 🚀 🎬 Enhanced Twitter Media Downloader | HD Video&Image Download | Multi-position Integration // @name:ja 🚀 🎬 Enhanced Twitter メディアダウンローダー | HD動画・画像ダウンロード | マルチポジション統合 // @name:es 🚀 🎬 Descargador Mejorado de Twitter | Descarga HD de Video e Imagen | Integración Multi-posición // @name:pt 🚀 Baixador Aprimorado do Twitter | Download HD de Vídeo e Imagem | Integração Multi-posição // @namespace http://tampermonkey.net/ // @version 1.1 // @description:en 🎬 Professional Twitter/X media downloader! 📱 One-click download HD videos and images with download buttons in tweet timeline, video player, and image modal 🔥 Auto-fetch highest quality, batch download support, completely free & no ads ✨ // @description:ja 🎬 プロフェッショナルなTwitter/Xメディアダウンローダー!📱 ツイートタイムライン、動画プレーヤー、画像モーダルにダウンロードボタンでHD動画・画像をワンクリックダウンロード 🔥 最高画質自動取得、一括ダウンロード対応、完全無料・広告なし ✨ // @description:es 🎬 ¡Descargador profesional de medios de Twitter/X! 📱 Descarga con un clic videos HD e imágenes con botones de descarga en línea de tiempo, reproductor de video y modal de imagen 🔥 Obtención automática de máxima calidad, soporte de descarga por lotes, completamente gratis y sin anuncios ✨ // @description:pt 🎬 Baixador profissional de mídia do Twitter/X! 📱 Download com um clique de vídeos HD e imagens com botões de download na timeline, player de vídeo e modal de imagem 🔥 Obtenção automática da qualidade máxima, suporte a download em lote, totalmente gratuito e sem anúncios ✨ // @description 🎬 Twitter/X平台专业媒体下载器!📱 一键下载高清视频和图片,支持推文时间线、视频播放器、图片模态框多位置下载按钮 🔥 自动获取最高画质,批量下载,完全免费无广告 ✨ // @author YouhouLab // @license MIT // @match *://*.twitter.com/* // @match *://*.x.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com // @grant none // @run-at document-start // @supportURL https://github.com/youhoulab/enhanced-twitter-downloader // @homepageURL https://github.com/youhoulab/enhanced-twitter-downloader // @compatible chrome // @compatible firefox // @compatible opera // @compatible edge // @compatible safari // @keywords twitter, x, download, video, image, media, hd, 1080p, 2k, 4k, free, no ads, batch download, enhanced, 推特, 媒体, 下载, 视频, 图片, 高清, 免费, 无广告, 批量下载, ツイッター, メディア, ダウンロード, 動画, 画像, 高画質, 無料, 広告なし, descarga, video, imagen, gratis, sin anuncios, baixar, vídeo, imagem, gratuito, sem anúncios, 🚀, 🎬, 📱, 🔥, ✨, 📺, 🖼️, 💾, ⬇️, 🎯 // @downloadURL none // ==/UserScript== /** * Enhanced Twitter媒体下载器 - 完整版 🚀 * 基于原有脚本增强,支持多位置下载按钮集成 */ class EnhancedTwitterMediaDownloader { constructor() { this.mediaStore = []; // 存储从API中提取的媒体信息 this.init(); } /** * 初始化下载器 */ init() { this.interceptXMLHttpRequest(); this.setupAllObservers(); this.injectStyles(); } /** * 设置所有观察器 */ setupAllObservers() { this.observeTweetElements(); // 推特动态按钮 this.observeVideoElements(); // 视频控制按钮 this.observeImageModal(); // 图片放大水印按钮 } /** * 拦截XMLHttpRequest,监听Twitter API响应 */ interceptXMLHttpRequest() { const originalOpen = XMLHttpRequest.prototype.open; const self = this; XMLHttpRequest.prototype.open = function(method, url) { // 检查是否是Twitter API请求 if (/(api\.)?(twitter|x)\.com\/(i\/api\/)?(2|graphql|1\.1)\//i.test(url)) { const originalSend = this.send; this.send = function() { const originalOnReadyStateChange = this.onreadystatechange; this.onreadystatechange = function() { // 请求完成时解析响应 if (this.readyState === XMLHttpRequest.DONE && this.responseText) { try { const responseData = JSON.parse(this.responseText); self.extractMediaFromResponse(responseData); } catch (error) { console.error('解析API响应失败:', error); } } // 调用原始回调 if (originalOnReadyStateChange) { return originalOnReadyStateChange.apply(this, arguments); } }; return originalSend.apply(this, arguments); }; } return originalOpen.apply(this, arguments); }; } /** * 从API响应中提取媒体信息 */ extractMediaFromResponse(data) { try { // 查找所有包含扩展实体的对象 const entitiesWithMedia = this.findObjectsWithProperty(data, 'extended_entities'); // 查找包含字符串值的对象(可能包含媒体信息) const stringValues = this.findObjectsWithProperty(data, 'string_value'); // 解析字符串值中的媒体信息 const parsedStringMedia = stringValues .map(obj => this.parseStringValueMedia(obj.string_value)) .filter(Boolean); // 合并所有媒体源 const allMediaSources = [...entitiesWithMedia, ...parsedStringMedia]; // 提取媒体信息 const mediaItems = this.extractMediaItems(allMediaSources); if (mediaItems.length > 0) { this.mediaStore.push(...mediaItems); console.log('提取到媒体信息:', mediaItems); } } catch (error) { console.error('提取媒体信息失败:', error); } } /** * 递归查找包含指定属性的对象 */ findObjectsWithProperty(obj, property, results = []) { if (!obj || typeof obj !== 'object') return results; if (obj.hasOwnProperty(property)) { results.push(obj); } else { Object.values(obj).forEach(value => { results.push(...this.findObjectsWithProperty(value, property)); }); } return results; } /** * 解析字符串值中的媒体信息 */ parseStringValueMedia(stringValue) { try { const parsed = JSON.parse(stringValue); const mediaEntities = Object.values(parsed.media_entities || {}); const mediaEntity = mediaEntities.find(entity => this.isValidMediaType(entity)); if (mediaEntity) { return { extended_entities: { media: [mediaEntity] }, id_str: mediaEntity.id_str }; } } catch (error) { // 解析失败,忽略此项 } return null; } /** * 检查是否是有效的媒体类型 */ isValidMediaType(entity) { return entity && ['video', 'animated_gif', 'photo'].includes(entity.type); } /** * 从媒体源中提取媒体项 */ extractMediaItems(mediaSources) { return mediaSources .filter(source => { const mediaArray = source.extended_entities?.media || []; return mediaArray.some(media => this.isValidMediaType(media)); }) .flatMap(source => { const entityId = source.id_str || source.conversation_id_str; const mediaArray = source.extended_entities.media.filter(media => this.isValidMediaType(media)); return mediaArray.map(media => ({ id: media.id_str, entityId: entityId, thumbnail: this.getThumbnailUrl(media), photo: this.getPhotoUrl(media), video: this.getVideoUrl(media), text: this.generateDisplayText(source) })); }) .filter((item, index, array) => { // 去重 return array.findIndex(other => other.id === item.id) === index; }); } /** * 获取缩略图URL */ getThumbnailUrl(media) { const url = media.media_url_https; return url.substr(0, url.lastIndexOf('.')); } /** * 获取图片URL */ getPhotoUrl(media) { const url = media.media_url_https; const hasLargeSize = media.sizes?.large; return url + (hasLargeSize ? ':large' : ''); } /** * 获取视频URL(最高质量) */ getVideoUrl(media) { const variants = media.video_info?.variants; if (!variants) return null; const mp4Variants = variants .filter(variant => variant.content_type === 'video/mp4') .sort((a, b) => b.bitrate - a.bitrate); return mp4Variants[0]?.url || null; } /** * 生成显示文本 */ generateDisplayText(source) { const id = source.id_str || source.conversation_id_str; if (!source.full_text) return id; let text = source.full_text .split('https://t.co')[0] .trim() .replace(/(\r\n|\n|\r)/gm, '') .substr(0, 50); return text || id; } // ================================= // 推特动态按钮集成 // ================================= observeTweetElements() { const tweetObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node instanceof HTMLElement) { // 检查推特动态 const articles = node.querySelectorAll('article[role="article"]'); articles.forEach(article => this.handleTweetElement(article)); // 如果节点本身是article if (node.matches('article[role="article"]')) { this.handleTweetElement(node); } } }); }); }); tweetObserver.observe(document, { childList: true, subtree: true }); } handleTweetElement(articleElement) { // 延迟处理,等待元素完全渲染 setTimeout(() => { // 检查是否包含媒体 const mediaElements = articleElement.querySelectorAll('img[src*="pbs.twimg.com"], video'); if (mediaElements.length === 0) return; // 查找互动按钮区域 - 使用XPath提供的精确路径逻辑 const buttonGroups = articleElement.querySelectorAll('div[role="group"]'); const actionGroup = buttonGroups[buttonGroups.length - 1]; if (!actionGroup) return; // 检查是否已添加 if (actionGroup.querySelector('.tweet-download-btn')) return; // 查找对应的媒体数据 const mediaItem = this.findMediaForTweet(articleElement); if (!mediaItem) return; // 添加下载按钮 this.addTweetDownloadButton(actionGroup, mediaItem); }, 1000); } addTweetDownloadButton(actionGroup, mediaItem) { const downloadButton = document.createElement('div'); downloadButton.className = 'tweet-download-btn'; downloadButton.setAttribute('role', 'button'); downloadButton.setAttribute('aria-label', '下载媒体'); downloadButton.setAttribute('tabindex', '0'); // 获取其他按钮的样式参考 const otherButtons = actionGroup.querySelectorAll('div[role="button"]'); const referenceButton = otherButtons[0]; // 设置圆形按钮样式 - CSS类会覆盖这些基础样式 downloadButton.innerHTML = `
`; // 悬停效果通过CSS处理,这里移除JavaScript处理 downloadButton.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); // 立即移除焦点,避免圆环残留 downloadButton.blur(); await this.handleDownload(e, downloadButton, mediaItem); }); actionGroup.appendChild(downloadButton); } // ================================= // 视频控制按钮集成 // ================================= observeVideoElements() { const videoObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node instanceof HTMLElement) { // 检查视频元素 const videos = node.querySelectorAll('video'); videos.forEach(video => this.handleVideoElement(video)); // 如果节点本身是视频 if (node.tagName === 'VIDEO') { this.handleVideoElement(node); } } }); }); }); videoObserver.observe(document, { childList: true, subtree: true }); } handleVideoElement(videoElement) { // 等待视频加载完成 const addButton = () => { setTimeout(() => { this.addVideoDownloadButton(videoElement); }, 2000); // 等待控制栏渲染 }; if (videoElement.readyState >= 1) { addButton(); } else { videoElement.addEventListener('loadedmetadata', addButton); } // 也监听用户交互后的控制栏显示 videoElement.addEventListener('play', addButton); videoElement.addEventListener('pause', addButton); } addVideoDownloadButton(videoElement) { const controlBar = this.findVideoControlBar(videoElement); if (!controlBar) return; // 检查是否已添加 if (controlBar.querySelector('.video-download-btn')) return; // 查找对应的媒体数据 const mediaItem = this.findMediaForVideo(videoElement); if (!mediaItem || !mediaItem.video) return; // 创建下载按钮 const downloadButton = this.createVideoDownloadButton(mediaItem); // 查找设置按钮并插入到其旁边 - 基于提供的XPath逻辑 const settingsButton = controlBar.querySelector('button[aria-label*="设置"], button[aria-label*="Settings"], button[aria-label*="更多"]'); if (settingsButton && settingsButton.parentElement) { settingsButton.parentElement.insertBefore(downloadButton, settingsButton.nextSibling); } else { // 如果找不到设置按钮,添加到控制栏末尾 controlBar.appendChild(downloadButton); } } findVideoControlBar(videoElement) { // 向上查找视频容器 let container = videoElement.parentElement; while (container && !container.matches('div[data-testid="videoComponent"], div[data-testid="videoPlayer"], div[role="group"]')) { container = container.parentElement; if (container === document.body) return null; } if (!container) return null; // 在容器中查找控制栏 - 通常是包含按钮的div const controlBars = container.querySelectorAll('div[role="group"], div:has(button)'); // 选择包含最多按钮的div作为控制栏 let bestCandidate = null; let maxButtons = 0; controlBars.forEach(bar => { const buttonCount = bar.querySelectorAll('button').length; if (buttonCount > maxButtons) { maxButtons = buttonCount; bestCandidate = bar; } }); return bestCandidate; } createVideoDownloadButton(mediaItem) { const button = document.createElement('button'); button.className = 'video-download-btn'; button.setAttribute('aria-label', '下载视频'); // 基础样式,圆形样式由CSS类处理 button.style.cssText = ` background: none; border: none; cursor: pointer; color: white; `; button.innerHTML = `
`; // 悬停效果通过CSS处理,移除JavaScript处理 button.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); // 立即移除焦点,避免圆环残留 button.blur(); await this.handleDownload(e, button, mediaItem); }); return button; } // ================================= // 图片放大水印按钮集成 // ================================= observeImageModal() { const modalObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node instanceof HTMLElement) { // 检查模态框 if (node.matches('div[aria-modal="true"]')) { this.handleImageModal(node); } // 检查子元素中的模态框 const modals = node.querySelectorAll('div[aria-modal="true"]'); modals.forEach(modal => this.handleImageModal(modal)); } }); }); }); modalObserver.observe(document, { childList: true, subtree: true }); } handleImageModal(modalElement) { setTimeout(() => { const images = modalElement.querySelectorAll('img[src*="pbs.twimg.com"]'); images.forEach(img => { this.addImageWatermarkButton(img); }); }, 500); // 等待图片渲染完成 } addImageWatermarkButton(imageElement) { // 检查是否已添加 const container = imageElement.closest('div'); if (!container || container.querySelector('.image-watermark-download')) return; // 查找对应的媒体数据 const mediaItem = this.mediaStore.find(item => imageElement.src.indexOf(item.thumbnail) > -1 || imageElement.src.indexOf(item.photo) > -1 ); if (!mediaItem) return; // 创建水印式下载按钮 const watermarkButton = document.createElement('div'); watermarkButton.className = 'image-watermark-download'; watermarkButton.setAttribute('role', 'button'); watermarkButton.setAttribute('aria-label', '下载图片'); watermarkButton.setAttribute('tabindex', '0'); watermarkButton.innerHTML = `
`; // 样式设置 - 基础样式,详细圆形样式由CSS类处理 Object.assign(watermarkButton.style, { position: 'absolute', top: '16px', right: '16px', cursor: 'pointer', zIndex: '1000', opacity: '0.8', transition: 'opacity 0.2s' }); // 悬停效果通过CSS处理 // 点击下载 watermarkButton.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); // 立即移除焦点,避免圆环残留 watermarkButton.blur(); await this.handleDownload(e, watermarkButton, mediaItem); }); // 确保图片容器有相对定位 const imageContainer = imageElement.closest('div'); if (imageContainer) { const computedStyle = getComputedStyle(imageContainer); if (computedStyle.position === 'static') { imageContainer.style.position = 'relative'; } imageContainer.appendChild(watermarkButton); } } // ================================= // 通用辅助方法 // ================================= findMediaForVideo(videoElement) { // 通过视频URL或容器查找对应的媒体数据 const videoSrc = videoElement.src || videoElement.currentSrc; if (!videoSrc) return null; return this.mediaStore.find(item => { if (!item.video) return false; // 提取视频ID用于匹配 const videoId = videoSrc.match(/\/(\d+)\//)?.[1]; const itemVideoId = item.video.match(/\/(\d+)\//)?.[1]; return videoId && itemVideoId && videoId === itemVideoId; }); } findMediaForTweet(articleElement) { // 通过推文中的图片找到对应的媒体数据 const images = articleElement.querySelectorAll('img[src*="pbs.twimg.com"]'); for (const img of images) { const mediaItem = this.mediaStore.find(item => img.src.indexOf(item.thumbnail) > -1 ); if (mediaItem) return mediaItem; } return null; } /** * 处理下载事件 */ async handleDownload(event, button, mediaItem) { event.preventDefault(); event.stopImmediatePropagation(); button.disabled = true; this.setButtonState(button, 'loading'); try { // 获取同一条推文的所有媒体 const relatedMedia = this.mediaStore.filter(item => item.entityId === mediaItem.entityId ); const downloadedUrls = new Set(); // 下载所有相关媒体 for (const media of relatedMedia) { const downloadUrl = media.video || media.photo; if (downloadUrl && !downloadedUrls.has(downloadUrl)) { await this.downloadFile(downloadUrl, media.text); downloadedUrls.add(downloadUrl); } } this.setButtonState(button, 'success'); } catch (error) { console.error('下载失败:', error); this.setButtonState(button, 'error'); } } /** * 设置按钮状态 */ setButtonState(button, state) { const downloadIcon = button.querySelector('.download-icon'); const loadingIcon = button.querySelector('.loading-icon'); const successIcon = button.querySelector('.success-icon'); // 隐藏所有图标 [downloadIcon, loadingIcon, successIcon].forEach(icon => { if (icon) icon.style.display = 'none'; }); // 显示对应状态的图标 switch (state) { case 'loading': if (loadingIcon) loadingIcon.style.display = 'block'; break; case 'success': if (successIcon) successIcon.style.display = 'block'; setTimeout(() => { button.disabled = false; if (downloadIcon) downloadIcon.style.display = 'block'; if (successIcon) successIcon.style.display = 'none'; }, 2000); break; case 'error': default: if (downloadIcon) downloadIcon.style.display = 'block'; button.disabled = false; break; } } /** * 下载文件 */ async downloadFile(url, filename) { try { const response = await fetch(url); const blob = await response.blob(); const contentType = response.headers.get('Content-Type'); // 确定文件扩展名 const extension = this.getFileExtension(contentType); // 创建下载链接 const downloadLink = document.createElement('a'); downloadLink.style.display = 'none'; downloadLink.href = window.URL.createObjectURL(blob); downloadLink.setAttribute('target', '_blank'); downloadLink.setAttribute('download', `${filename}.${extension}`); // 触发下载 document.body.appendChild(downloadLink); downloadLink.click(); // 清理 window.URL.revokeObjectURL(downloadLink.href); document.body.removeChild(downloadLink); } catch (error) { console.error('文件下载失败:', error); throw error; } } /** * 根据MIME类型获取文件扩展名 */ getFileExtension(contentType) { switch (contentType) { case 'image/jpeg': return 'jpg'; case 'image/png': return 'png'; case 'video/mp4': return 'mp4'; default: return 'mp4'; // 默认为mp4 } } /** * 注入样式 */ injectStyles() { const style = document.createElement('style'); style.textContent = ` /* 推特动态下载按钮样式 - 圆形设计 */ .tweet-download-btn { color: rgb(83, 100, 113); transition: all 0.2s ease-in-out; border-radius: 50% !important; width: 36px !important; height: 36px !important; min-width: 36px !important; min-height: 36px !important; display: flex !important; align-items: center !important; justify-content: center !important; background-color: transparent !important; border: none !important; cursor: pointer !important; outline: none !important; } .tweet-download-btn:hover { color: rgb(29, 161, 242); background-color: rgba(29, 161, 242, 0.1) !important; transform: scale(1.05); } /* 视频下载按钮样式 - 圆形设计 */ .video-download-btn { border-radius: 50% !important; width: 35px !important; height: 35px !important; min-width: 35px !important; min-height: 35px !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s ease-in-out !important; outline: none !important; } .video-download-btn:hover { background-color: rgba(255, 255, 255, 0.15) !important; transform: scale(1.1) !important; } /* 图片水印下载按钮样式 - 圆形设计 */ .image-watermark-download { user-select: none; border-radius: 50% !important; width: 35px !important; height: 35px !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s ease-in-out !important; outline: none !important; } .image-watermark-download:hover { transform: scale(1.1) !important; opacity: 1 !important; } .image-watermark-download svg { border-radius: 50% !important; } /* 通用样式 */ .tweet-download-btn:disabled, .video-download-btn:disabled, .image-watermark-download:disabled { cursor: not-allowed; opacity: 0.7; transform: none !important; } /* 按钮内图标居中 */ .tweet-download-btn > div, .video-download-btn > div, .image-watermark-download > div { display: flex !important; align-items: center !important; justify-content: center !important; width: 100% !important; height: 100% !important; padding: 0 !important; } /* 加载动画 */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-icon { animation: spin 1s linear infinite; } /* 移除所有焦点和点击时的圆环效果 */ .tweet-download-btn:focus, .tweet-download-btn:active, .tweet-download-btn:focus-visible, .tweet-download-btn:focus-within, .video-download-btn:focus, .video-download-btn:active, .video-download-btn:focus-visible, .video-download-btn:focus-within, .image-watermark-download:focus, .image-watermark-download:active, .image-watermark-download:focus-visible, .image-watermark-download:focus-within { outline: none !important; border: none !important; box-shadow: none !important; -webkit-appearance: none !important; -moz-appearance: none !important; appearance: none !important; } /* 强制移除所有浏览器默认焦点样式 */ .tweet-download-btn *:focus, .tweet-download-btn *:active, .tweet-download-btn *:focus-visible, .video-download-btn *:focus, .video-download-btn *:active, .video-download-btn *:focus-visible, .image-watermark-download *:focus, .image-watermark-download *:active, .image-watermark-download *:focus-visible { outline: none !important; border: none !important; box-shadow: none !important; } `; document.head.appendChild(style); } } const enhancedDownloader = new EnhancedTwitterMediaDownloader();