您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键下载抖音视频,支持多种清晰度选择,无水印下载
// ==UserScript== // @name Dou音视频下载器 // @namespace https://greasyfork.org/zh-CN/scripts/538824-dou%E9%9F%B3%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD%E5%99%A8 // @version 1.0 // @author moyu001 // @description 一键下载抖音视频,支持多种清晰度选择,无水印下载 // @license MIT // @match *://*.douyin.com/* // @match *://*.iesdouyin.com/* // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function() { 'use strict'; // 工具函数 const utils = { // 格式化文件大小 formatByteToSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, // 阻止事件 preventEvent(event) { event.preventDefault(); event.stopPropagation(); }, // 获取React对象 getReactObj(element) { for (let key in element) { if (key.startsWith('__reactInternalInstance') || key.startsWith('__reactFiber')) { return element[key]; } } return null; }, // 显示消息 showMessage(message, type = 'info') { const colors = { info: '#2196F3', success: '#4CAF50', error: '#F44336', warning: '#FF9800' }; const messageEl = document.createElement('div'); messageEl.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${colors[type]}; color: white; padding: 12px 20px; border-radius: 4px; z-index: 10000; font-size: 14px; max-width: 300px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); `; messageEl.textContent = message; document.body.appendChild(messageEl); setTimeout(() => { if (messageEl.parentNode) { messageEl.parentNode.removeChild(messageEl); } }, 3000); } }; // 下载管理器 class DownloadManager { constructor() { this.downloads = new Map(); } // 显示视频解析对话框 showParseDialog(downloadFileName, downloadUrlInfoList) { let contentHTML = ''; downloadUrlInfoList.forEach((downloadInfo) => { const videoQualityInfo = `${downloadInfo.width}x${downloadInfo.height} @${downloadInfo.fps}fps`; contentHTML += ` <div class="dy-video-item" style="margin: 15px 0; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px;"> <div class="dy-video-quality" style="margin-bottom: 8px;"> <span style="font-weight: bold;">清晰度:</span> <span>${videoQualityInfo}</span> </div> <div class="dy-video-size" style="margin-bottom: 8px;"> <span style="font-weight: bold;">文件大小:</span> <span>${utils.formatByteToSize(downloadInfo.dataSize)}</span> </div> <div class="dy-video-actions"> <button class="download-btn" data-url="${downloadInfo.url}" data-filename="${downloadFileName} - ${videoQualityInfo}.${downloadInfo.format}" style="background: #fe2c55; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-right: 10px;"> 立即下载 </button> <a href="${downloadInfo.url}" target="_blank" style="color: #1890ff; text-decoration: none;"> 在新窗口打开 </a> </div> ${downloadInfo.backUrl.length ? ` <div class="dy-video-backup" style="margin-top: 8px;"> <span style="font-weight: bold;">备用地址:</span> ${downloadInfo.backUrl.map((url, index) => `<a href="${url}" target="_blank" style="color: #1890ff; margin-right: 10px;">地址${index + 1}</a>` ).join('')} </div> ` : ''} </div> `; }); // 创建对话框 const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 10000; max-width: 600px; max-height: 80vh; overflow: hidden; display: flex; flex-direction: column; `; const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; `; dialog.innerHTML = ` <div style="padding: 20px; border-bottom: 1px solid #e0e0e0;"> <h3 style="margin: 0; color: #333;">视频下载</h3> <button id="close-dialog" style="position: absolute; top: 15px; right: 15px; background: none; border: none; font-size: 20px; cursor: pointer;">×</button> </div> <div style="padding: 20px; overflow-y: auto; flex: 1;"> ${contentHTML} </div> `; document.body.appendChild(overlay); document.body.appendChild(dialog); // 绑定事件 dialog.querySelector('#close-dialog').onclick = () => { document.body.removeChild(overlay); document.body.removeChild(dialog); }; overlay.onclick = () => { document.body.removeChild(overlay); document.body.removeChild(dialog); }; // 绑定下载按钮事件 dialog.querySelectorAll('.download-btn').forEach(btn => { btn.onclick = (e) => { const url = e.target.getAttribute('data-url'); const filename = e.target.getAttribute('data-filename'); this.downloadVideo(url, filename); }; }); } // 下载视频 downloadVideo(url, filename) { if (typeof GM_download !== 'function') { utils.showMessage('当前脚本环境不支持下载功能', 'error'); window.open(url, '_blank'); return; } utils.showMessage('开始下载视频...', 'info'); let downloadId = Date.now(); let progressElement = this.createProgressElement(filename, downloadId); const downloadOptions = { url: url, name: filename, headers: { 'Referer': window.location.href, 'User-Agent': navigator.userAgent }, onload: () => { this.removeProgressElement(downloadId); utils.showMessage(`下载完成:${filename}`, 'success'); }, onprogress: (details) => { if (details.loaded && details.total) { const progress = Math.round((details.loaded / details.total) * 100); this.updateProgress(downloadId, progress); } }, onerror: (error) => { this.removeProgressElement(downloadId); utils.showMessage(`下载失败:${error.error || '未知错误'}`, 'error'); }, ontimeout: () => { this.removeProgressElement(downloadId); utils.showMessage('下载超时', 'error'); } }; try { const result = GM_download(downloadOptions); this.downloads.set(downloadId, result); } catch (error) { this.removeProgressElement(downloadId); utils.showMessage('下载启动失败', 'error'); } } // 创建进度元素 createProgressElement(filename, downloadId) { const progressEl = document.createElement('div'); progressEl.id = `download-progress-${downloadId}`; progressEl.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; z-index: 10000; max-width: 300px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); `; progressEl.innerHTML = ` <div style="font-size: 14px; margin-bottom: 8px; color: #333;">${filename}</div> <div style="background: #f0f0f0; border-radius: 4px; overflow: hidden;"> <div id="progress-bar-${downloadId}" style="background: #fe2c55; height: 8px; width: 0%; transition: width 0.3s;"></div> </div> <div id="progress-text-${downloadId}" style="font-size: 12px; color: #666; margin-top: 4px;">0%</div> `; document.body.appendChild(progressEl); return progressEl; } // 更新进度 updateProgress(downloadId, progress) { const progressBar = document.getElementById(`progress-bar-${downloadId}`); const progressText = document.getElementById(`progress-text-${downloadId}`); if (progressBar && progressText) { progressBar.style.width = `${progress}%`; progressText.textContent = `${progress}%`; } } // 移除进度元素 removeProgressElement(downloadId) { const progressEl = document.getElementById(`download-progress-${downloadId}`); if (progressEl && progressEl.parentNode) { progressEl.parentNode.removeChild(progressEl); } this.downloads.delete(downloadId); } } // 视频解析器 class VideoParser { constructor(downloadManager) { this.downloadManager = downloadManager; } // 解析视频信息 parseVideoInfo(element) { try { const reactObj = utils.getReactObj(element); if (!reactObj) { throw new Error('无法获取React对象'); } // 查找awemeInfo let awemeInfo = null; let currentFiber = reactObj; // 向上遍历fiber树查找awemeInfo while (currentFiber && !awemeInfo) { if (currentFiber.memoizedProps && currentFiber.memoizedProps.awemeInfo) { awemeInfo = currentFiber.memoizedProps.awemeInfo; break; } if (currentFiber.return && currentFiber.return.memoizedProps && currentFiber.return.memoizedProps.awemeInfo) { awemeInfo = currentFiber.return.memoizedProps.awemeInfo; break; } currentFiber = currentFiber.return; } if (!awemeInfo) { throw new Error('无法获取视频信息'); } return this.extractVideoUrls(awemeInfo); } catch (error) { console.error('解析视频信息失败:', error); utils.showMessage('解析视频信息失败', 'error'); return null; } } // 提取视频URL extractVideoUrls(awemeInfo) { const videoDownloadUrlList = []; const bitRateList = awemeInfo?.video?.bitRateList; if (bitRateList && Array.isArray(bitRateList)) { bitRateList.forEach(item => { const videoInfo = { url: item.playApi || item.playAddr?.[0]?.src, width: item.width, height: item.height, format: item.format || 'mp4', fps: item.fps || 30, dataSize: item.dataSize || 0, backUrl: [] }; // 添加备用URL if (Array.isArray(item.playAddr)) { videoInfo.backUrl = item.playAddr.map(addr => addr.src).filter(src => src !== videoInfo.url); } // 处理URL协议 if (videoInfo.url) { if (videoInfo.url.startsWith('http:')) { videoInfo.url = videoInfo.url.replace('http:', 'https:'); } videoDownloadUrlList.push(videoInfo); } }); } if (videoDownloadUrlList.length === 0) { throw new Error('未找到有效的视频链接'); } // 去重并排序 const uniqueVideos = this.removeDuplicates(videoDownloadUrlList); uniqueVideos.sort((a, b) => b.width - a.width); // 按清晰度降序排列 // 生成文件名 const authorName = awemeInfo?.authorInfo?.nickname || '未知作者'; const videoDesc = awemeInfo?.desc || '未知视频'; const filename = `${authorName} - ${videoDesc}`.replace(/[<>:"/\\|?*]/g, '_'); return { filename, videos: uniqueVideos }; } // 去除重复视频 removeDuplicates(videoList) { const unique = []; const seen = new Set(); videoList.forEach(video => { const key = `${video.width}x${video.height}@${video.fps}`; if (!seen.has(key)) { seen.add(key); unique.push(video); } else { // 如果已存在相同规格,选择文件大小更大的 const existingIndex = unique.findIndex(v => v.width === video.width && v.height === video.height && v.fps === video.fps ); if (existingIndex !== -1 && video.dataSize > unique[existingIndex].dataSize) { unique[existingIndex] = video; } } }); return unique; } } // 主应用类 class DouyinDownloader { constructor() { this.downloadManager = new DownloadManager(); this.videoParser = new VideoParser(this.downloadManager); this.init(); } init() { this.bindEvents(); } // 绑定事件 bindEvents() { // 监听分享按钮点击(原有的下载按钮) document.addEventListener('click', (event) => { const shareContainer = event.target.closest('[data-e2e="video-share-container"]'); if (shareContainer) { const downloadBtn = event.target.closest('div[data-inuser="false"] button + div'); if (downloadBtn) { utils.preventEvent(event); this.handleVideoDownload(event.target); } } }, true); } // 处理视频下载 handleVideoDownload(element) { try { const result = this.videoParser.parseVideoInfo(element); if (result) { this.downloadManager.showParseDialog(result.filename, result.videos); } } catch (error) { console.error('下载处理失败:', error); utils.showMessage('下载处理失败', 'error'); } } } // 启动应用 function startApp() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new DouyinDownloader(); }); } else { new DouyinDownloader(); } } // 检查页面是否为抖音相关页面 if (window.location.hostname.includes('douyin.com') || window.location.hostname.includes('iesdouyin.com')) { startApp(); } })();