// ==UserScript==
// @name [J]Douyin Video Downloader
// @namespace http://tampermonkey.net/
// @version 20240411
// @description Download videos from Douyin website
// @author jeffc
// @match *.douyin.com/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
function Video(authorName, desc, id) {
this.authorName = authorName || "";
this.videoDesc = desc || "";
this.videoId = id || "";
this.videoUrl = "";
}
Video.prototype = {
constructor: Video,
download: function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', `https://www.douyin.com/aweme/v1/web/aweme/detail/?device_platform=webapp&aweme_id=${this.videoId}&screen_width=1920&screen_height=1080`, true);
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status === 200) {
var responseData = xhr.response;
this.videoUrl = responseData.aweme_detail.video.bit_rate[0].play_addr.url_list[0];
this.save();
} else {
console.error('request error:', xhr.statusText);
}
};
xhr.onerror = function() {
console.error('request error:', xhr.statusText);
};
xhr.send();
},
clear: function() {
this.authorName = "";
this.videoDesc = "";
this.videoId = "";
this.videoUrl = "";
},
save: function() {
if (this.videoUrl.length > 0) {
let downloadDom = document.createElement('a');
downloadDom.href = this.videoUrl;
downloadDom.target = "_blank";
// downloadDom.download = `${this.authorName}_${this.videoDesc}.mp4`;
document.body.appendChild(downloadDom);
downloadDom.setAttribute("download",((new Date()).getTime())+".mp4")
downloadDom.click();
document.body.removeChild(downloadDom);
this.clear();
} else {
alert("无法解析视频");
}
}
};
const _historyWrap = function(type) {
const orig = history[type];
const e = new Event(type);
return function() {
const rv = orig.apply(this, arguments);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
}
history.pushState = _historyWrap('pushState');
history.replaceState = _historyWrap('replaceState');
window.addEventListener('pushState', function(e) {
console.log('page change ');
dynamicMonitoring();
});
window.addEventListener('replaceState', function(e) {
console.log('page change ');
dynamicMonitoring();
});
const PageType = {
Detail: 'Detail', // 详情页
Normal: 'Normal', // 作者主页
Live: 'Live', // 直播
};
let currentPageType = PageType.Normal;
// 动态监测函数
function dynamicMonitoring() {
if (/www.douyin.com\/video\/[0-9]{9,}/.test(window.location.href)) {
currentPageType = PageType.Detail;
var initialTargetNode = document.querySelector('xg-video-container');
var mobserver = new MutationObserver(function(mutationsList, observer) {
mutationsList.forEach(function(mutation) {
refreshDownloadDom(initialTargetNode);
});
});
var mconfig = {
childList:true,
subtree:true
};
let listener = setInterval(function() {
if (!initialTargetNode ) {
initialTargetNode = document.querySelector('xg-video-container');
}else{
mobserver.observe(initialTargetNode, mconfig);
refreshDownloadDom(initialTargetNode);
clearInterval(listener);
}
}, 500);
} else {
// 通用页
currentPageType = PageType.Normal;
monitoringSilder();
var observeTargetNode;
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
switchObserverTarget();
});
});
var config = {
attributes: true,
attributeFilter: ['data-e2e']
};
let listener = setInterval(function() {
if (!observeTargetNode ) {
observeTargetNode = document.querySelector('div[data-e2e="feed-active-video"]');
}else{
observer.observe(observeTargetNode, config);
refreshDownloadDom(observeTargetNode);
clearInterval(listener);
}
}, 500);
// 切换观察目标节点的函数
function switchObserverTarget() {
// 重新获取目标节点
var newTargetNode = document.querySelector('div[data-e2e="feed-active-video"]');
let listener = setInterval(function() {
if (!newTargetNode ) {
newTargetNode = document.querySelector('div[data-e2e="feed-active-video"]');
}else{
if (newTargetNode && newTargetNode !== observeTargetNode) {
observer.unobserve(observeTargetNode);
observer.observe(newTargetNode, config);
observeTargetNode = newTargetNode;
refreshDownloadDom(newTargetNode);
}
clearInterval(listener);
}
}, 500);
}
}
}
// 更新通用页的下载按钮
function refreshDownloadDom(activeVideoDom) {
var downloadBtn = activeVideoDom.querySelector('#douyin_download_by_jeffc');
if (!downloadBtn) {
downloadBtn = document.createElement('div');
downloadBtn.setAttribute("data-index", "10");
downloadBtn.id = "douyin_download_by_jeffc";
downloadBtn.innerHTML = "DOWNLOAD";
downloadBtn.style = 'cursor:pointer;width: 60px;text-align: center;font-size: 14px;color: rgba(255, 255, 255,0.75);line-height:20px;margin-right: 30px;';
downloadBtn.addEventListener('click', function() {
downloadVideo(activeVideoDom);
});
let listenerActiveVideo = setInterval(function() {
let targetNode = currentPageType == currentPageType.Normal ? activeVideoDom.querySelector("xg-right-grid") : activeVideoDom.parentNode.querySelector("xg-right-grid") ;
if (targetNode ) {
if(!targetNode.querySelector("#douyin_download_by_jeffc"))
{
targetNode.appendChild(downloadBtn);
}
clearInterval(listenerActiveVideo);
}
}, 500);
}
}
// 下载视频的函数
function downloadVideo(activeVideoDom) {
var authorName = activeVideoDom.querySelector('[data-e2e="feed-video-nickname"]')?.innerText.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '');
var videoDesc = activeVideoDom.querySelector('[data-e2e="video-desc"]')?.innerText.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '');
var videoDom = activeVideoDom.querySelector("video");
var sourceUrl;
if (videoDom) {
if (videoDom.childNodes.length > 0) {
sourceUrl = videoDom.querySelector("source")?.src;
}
} else {
sourceUrl = activeVideoDom.querySelector("xg-video")?.src;
}
if (sourceUrl) {
// 直接下载
var downloadDom = document.createElement('a');
downloadDom.href = sourceUrl;
downloadDom.target = "_blank";
downloadDom.setAttribute("download","");
document.body.appendChild(downloadDom);
downloadDom.click();
document.body.removeChild(downloadDom);
} else {
// 解析视频并下载
var vid = activeVideoDom.getAttribute("data-e2e-vid");
(new Video(authorName, videoDesc, vid)).download();
}
}
function monitoringSilder()
{
const mutationObserver = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-e2e-vid') {
const targetNode = mutation.target;
console.log(targetNode);
dealwith(targetNode);
}
});
});
let listener = setInterval(function() {
if (document.querySelector('#slidelist')) {
mutationObserver.observe(document.querySelector('#slidelist'), { childList: true, subtree: true,attributes:true});
clearInterval(listener);
}
}, 500);
function dealwith(activeVideoDom){
var downloadBtn = activeVideoDom.querySelector('#douyin_download_by_jeffc');
if (!downloadBtn) {
downloadBtn = document.createElement('div');
downloadBtn.setAttribute("data-index", "10");
downloadBtn.id = "douyin_download_by_jeffc";
downloadBtn.innerHTML = "DOWNLOAD";
downloadBtn.style = 'cursor:pointer;width: 60px;text-align: center;font-size: 14px;color: rgba(255, 255, 255,0.75);line-height:20px;margin-right: 30px;';
downloadBtn.addEventListener('click', function() {
downloadVideo(activeVideoDom);
});
let listenerActiveVideo = setInterval(function() {
let targetNode = currentPageType == currentPageType.Normal ? activeVideoDom.querySelector("xg-right-grid") : activeVideoDom.parentNode.querySelector("xg-right-grid") ;
if (targetNode ) {
if(!targetNode.querySelector("#douyin_download_by_jeffc"))
{
targetNode.appendChild(downloadBtn);
}
clearInterval(listenerActiveVideo);
}
}, 500);
}
}
}
// 页面加载完成后开始动态监测
window.onload = function() {
dynamicMonitoring();
};
// 监听键盘按下事件
document.addEventListener("keydown", function(event) {
// 获取按下的键的键码
var keyCode = event.keyCode;
if(keyCode == 90)
{
var target_dom = document.querySelectorAll("#douyin_download_by_jeffc");
if(target_dom)
{
target_dom[target_dom.length-1].click();
}
}
});
})();