// ==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();
}
})();