// ==UserScript== // @name 全能下载工具 - Universal Downloader // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 突破网站限制,下载所有类型的媒体资源(视频、音频、图片、文档等),支持质量选择和自定义保存路径 // @author WeiRuan // @match *://*/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-end // @connect * // @downloadURL none // ==/UserScript== (function() { 'use strict'; // ==================== 配置管理 ==================== const CONFIG = { savePath: GM_getValue('savePath', ''), autoDetect: GM_getValue('autoDetect', true), showNotification: GM_getValue('showNotification', true), defaultQuality: GM_getValue('defaultQuality', 'high') }; // ==================== 资源检测器 ==================== class ResourceDetector { constructor() { this.detectedResources = new Map(); this.observers = []; this.init(); } init() { this.detectExistingResources(); this.interceptNetworkRequests(); this.observeDOMChanges(); this.interceptMediaElements(); this.initSiteSpecificHandlers(); } // 初始化网站特定处理 initSiteSpecificHandlers() { const handlers = new SiteHandlers(); handlers.init(this); } // 检测现有资源 detectExistingResources() { // 检测视频元素 document.querySelectorAll('video').forEach(video => { this.addResource({ type: 'video', url: video.src || video.currentSrc, element: video, qualities: this.extractVideoQualities(video) }); }); // 检测音频元素 document.querySelectorAll('audio').forEach(audio => { this.addResource({ type: 'audio', url: audio.src || audio.currentSrc, element: audio }); }); // 检测图片 document.querySelectorAll('img').forEach(img => { if (img.src && !img.src.startsWith('data:')) { this.addResource({ type: 'image', url: img.src, element: img }); } }); // 检测链接资源 document.querySelectorAll('a[href]').forEach(link => { const href = link.href; const ext = this.getFileExtension(href); if (this.isDownloadableFile(ext)) { this.addResource({ type: this.getFileType(ext), url: href, element: link, filename: link.download || this.getFilenameFromUrl(href) }); } }); } // 拦截网络请求 interceptNetworkRequests() { const originalFetch = window.fetch; const originalXHR = window.XMLHttpRequest.prototype.open; const self = this; // 拦截 Fetch window.fetch = function(...args) { const url = args[0]; if (typeof url === 'string') { self.analyzeUrl(url); } return originalFetch.apply(this, args); }; // 拦截 XHR window.XMLHttpRequest.prototype.open = function(method, url) { self.analyzeUrl(url); return originalXHR.apply(this, arguments); }; } // 监听DOM变化 observeDOMChanges() { const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') { this.handleMediaElement(node); } if (node.querySelectorAll) { node.querySelectorAll('video, audio, img').forEach(el => { this.handleMediaElement(el); }); } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); this.observers.push(observer); } // 拦截媒体元素 interceptMediaElements() { const self = this; // 劫持 video 和 audio 的 src 设置 ['HTMLVideoElement', 'HTMLAudioElement'].forEach(elementType => { if (window[elementType]) { const originalSetSrc = Object.getOwnPropertyDescriptor(window[elementType].prototype, 'src').set; Object.defineProperty(window[elementType].prototype, 'src', { set: function(value) { self.addResource({ type: elementType === 'HTMLVideoElement' ? 'video' : 'audio', url: value, element: this }); return originalSetSrc.call(this, value); }, get: function() { return Object.getOwnPropertyDescriptor(window[elementType].prototype, 'src').get.call(this); } }); } }); } // 处理媒体元素 handleMediaElement(element) { const type = element.tagName.toLowerCase(); const url = element.src || element.currentSrc; if (url && !url.startsWith('data:') && !url.startsWith('blob:')) { this.addResource({ type: type, url: url, element: element, qualities: type === 'video' ? this.extractVideoQualities(element) : null }); } // 检查 source 子元素 element.querySelectorAll('source').forEach(source => { if (source.src) { this.addResource({ type: type, url: source.src, element: element, quality: source.dataset.quality || source.label }); } }); } // 分析 URL analyzeUrl(url) { if (!url || url.startsWith('data:')) return; const ext = this.getFileExtension(url); const type = this.getFileType(ext); if (type) { this.addResource({ type: type, url: url, filename: this.getFilenameFromUrl(url) }); } // 检测常见视频平台的 m3u8 和 mpd if (url.includes('.m3u8') || url.includes('.mpd')) { this.addResource({ type: 'video', url: url, format: url.includes('.m3u8') ? 'HLS' : 'DASH' }); } } // 提取视频质量选项 extractVideoQualities(video) { const qualities = []; // 检查 source 元素 video.querySelectorAll('source').forEach(source => { qualities.push({ url: source.src, quality: source.dataset.quality || source.label || 'unknown', type: source.type }); }); // 检查视频本身 if (video.src) { qualities.push({ url: video.src, quality: 'default', type: video.type }); } return qualities.length > 0 ? qualities : null; } // 添加资源 addResource(resource) { if (!resource.url || resource.url.startsWith('data:')) return; const key = resource.url; if (!this.detectedResources.has(key)) { this.detectedResources.set(key, { ...resource, detectedAt: Date.now() }); if (CONFIG.showNotification) { console.log('检测到资源:', resource); } } } // 获取文件扩展名 getFileExtension(url) { try { const urlObj = new URL(url, window.location.href); const pathname = urlObj.pathname; const match = pathname.match(/\.([a-zA-Z0-9]+)(?:\?|$)/); return match ? match[1].toLowerCase() : ''; } catch (e) { return ''; } } // 判断是否为可下载文件 isDownloadableFile(ext) { const downloadableExts = [ // 视频 'mp4', 'webm', 'mkv', 'avi', 'mov', 'flv', 'wmv', 'm4v', // 音频 'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a', 'wma', // 图片 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', // 文档 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', // 压缩文件 'zip', 'rar', '7z', 'tar', 'gz', // 其他 'exe', 'dmg', 'apk', 'iso' ]; return downloadableExts.includes(ext); } // 获取文件类型 getFileType(ext) { const typeMap = { video: ['mp4', 'webm', 'mkv', 'avi', 'mov', 'flv', 'wmv', 'm4v'], audio: ['mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a', 'wma'], image: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'], document: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'], archive: ['zip', 'rar', '7z', 'tar', 'gz'], other: ['exe', 'dmg', 'apk', 'iso'] }; for (const [type, exts] of Object.entries(typeMap)) { if (exts.includes(ext)) { return type; } } return null; } // 从 URL 获取文件名 getFilenameFromUrl(url) { try { const urlObj = new URL(url, window.location.href); const pathname = urlObj.pathname; return pathname.split('/').pop() || 'download'; } catch (e) { return 'download'; } } // 获取所有检测到的资源 getAllResources() { return Array.from(this.detectedResources.values()); } // 清理 destroy() { this.observers.forEach(observer => observer.disconnect()); this.detectedResources.clear(); } } // ==================== 下载管理器 ==================== class DownloadManager { constructor() { this.downloads = []; } // 下载资源 async download(resource, options = {}) { const { quality = CONFIG.defaultQuality, filename = resource.filename || this.generateFilename(resource), onProgress = null, onComplete = null, onError = null } = options; try { // 选择质量 let downloadUrl = resource.url; if (resource.qualities && resource.qualities.length > 0) { downloadUrl = this.selectQuality(resource.qualities, quality); } // 处理特殊格式 if (resource.format === 'HLS' || resource.format === 'DASH') { this.notify('提示', 'HLS/DASH 格式需要特殊处理,将尝试下载'); } // 使用 GM_xmlhttpRequest 下载 await this.downloadWithGM(downloadUrl, filename, { onProgress, onComplete, onError }); } catch (error) { console.error('下载失败:', error); if (onError) onError(error); this.notify('下载失败', error.message); } } // 使用 GM_xmlhttpRequest 下载 downloadWithGM(url, filename, callbacks = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onprogress: (progress) => { if (callbacks.onProgress && progress.lengthComputable) { const percent = (progress.loaded / progress.total) * 100; callbacks.onProgress(percent); } }, onload: (response) => { try { const blob = response.response; const downloadUrl = URL.createObjectURL(blob); // 创建下载链接 const a = document.createElement('a'); a.href = downloadUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 清理 setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); this.notify('下载完成', filename); if (callbacks.onComplete) callbacks.onComplete(); resolve(); } catch (error) { reject(error); } }, onerror: (error) => { if (callbacks.onError) callbacks.onError(error); reject(error); } }); }); } // 选择质量 selectQuality(qualities, preferredQuality) { if (qualities.length === 1) { return qualities[0].url; } // 质量优先级 const qualityMap = { 'high': ['1080p', '1080', '720p', '720', '480p', '480', '360p', '360'], 'medium': ['720p', '720', '480p', '480', '360p', '360', '1080p', '1080'], 'low': ['360p', '360', '480p', '480', '720p', '720'] }; const priorities = qualityMap[preferredQuality] || qualityMap['high']; for (const priority of priorities) { const found = qualities.find(q => q.quality && q.quality.toLowerCase().includes(priority) ); if (found) return found.url; } return qualities[0].url; } // 生成文件名 generateFilename(resource) { const timestamp = new Date().getTime(); const ext = this.getExtFromUrl(resource.url) || this.getExtFromType(resource.type); const title = document.title.slice(0, 50).replace(/[\/\\:*?"<>|]/g, '-'); return `${title}_${timestamp}.${ext}`; } // 从 URL 获取扩展名 getExtFromUrl(url) { try { const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|#|$)/); return match ? match[1] : null; } catch (e) { return null; } } // 从类型获取扩展名 getExtFromType(type) { const extMap = { video: 'mp4', audio: 'mp3', image: 'jpg', document: 'pdf', archive: 'zip', other: 'bin' }; return extMap[type] || 'bin'; } // 通知 notify(title, message) { if (CONFIG.showNotification && typeof GM_notification !== 'undefined') { GM_notification({ title: title, text: message, timeout: 3000 }); } } } // ==================== 用户界面 ==================== class DownloaderUI { constructor(detector, downloader) { this.detector = detector; this.downloader = downloader; this.panel = null; this.init(); } init() { this.createStyles(); this.createPanel(); this.createFloatingButton(); this.registerMenuCommands(); } // 创建样式 createStyles() { const style = document.createElement('style'); style.textContent = ` .ud-floating-btn { position: fixed; right: 20px; bottom: 20px; width: 60px; height: 60px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: pointer; z-index: 999999; display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; transition: transform 0.3s, box-shadow 0.3s; } .ud-floating-btn:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(0,0,0,0.4); } .ud-panel { position: fixed; right: -400px; top: 0; width: 400px; height: 100%; background: white; box-shadow: -2px 0 10px rgba(0,0,0,0.3); z-index: 1000000; transition: right 0.3s; overflow: hidden; display: flex; flex-direction: column; } .ud-panel.show { right: 0; } .ud-panel-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; font-size: 18px; font-weight: bold; } .ud-panel-close { float: right; cursor: pointer; font-size: 24px; } .ud-panel-body { flex: 1; overflow-y: auto; padding: 15px; } .ud-resource-item { background: #f5f5f5; border-radius: 8px; padding: 12px; margin-bottom: 10px; transition: background 0.2s; } .ud-resource-item:hover { background: #e8e8e8; } .ud-resource-type { display: inline-block; background: #667eea; color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px; margin-bottom: 8px; } .ud-resource-url { font-size: 12px; color: #666; margin-bottom: 8px; word-break: break-all; max-height: 40px; overflow: hidden; } .ud-resource-actions { display: flex; gap: 8px; } .ud-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; transition: opacity 0.2s; } .ud-btn:hover { opacity: 0.8; } .ud-btn-primary { background: #667eea; color: white; } .ud-btn-secondary { background: #ddd; color: #333; } .ud-quality-selector { margin-top: 8px; } .ud-quality-selector select { width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; } .ud-settings { padding: 15px; border-top: 1px solid #ddd; } .ud-setting-item { margin-bottom: 12px; } .ud-setting-label { font-size: 13px; color: #333; margin-bottom: 6px; } .ud-setting-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; } .ud-empty { text-align: center; color: #999; padding: 40px 20px; } `; document.head.appendChild(style); } // 创建浮动按钮 createFloatingButton() { const btn = document.createElement('div'); btn.className = 'ud-floating-btn'; btn.innerHTML = '⬇'; btn.title = '全能下载工具'; btn.onclick = () => this.togglePanel(); document.body.appendChild(btn); } // 创建面板 createPanel() { const panel = document.createElement('div'); panel.className = 'ud-panel'; panel.innerHTML = `