[J]Douyin Video Downloader

Download videos from Douyin website

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        [J]Douyin Video Downloader
// @namespace    http://tampermonkey.net/
// @version         2024120821
// @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;

    let liveExecuStatus = false;

    // 动态监测函数
    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 if(!liveExecuStatus && ( /live.douyin.com\/[0-9]{9,}/.test(window.location.href) || /www.douyin.com\/follow\/live\/[0-9]{9,}/.test(window.location.href))){
            liveExecuStatus = true;
             var targetUrl="";
             const originalFetch = window.fetch;
             window.fetch = function(url, options) {
                     if (url.includes('.mp4') || url.includes('.m3u8') || url.includes('.ts') || url.includes('.flv')) {
                            targetUrl = url;
                     }
                     return originalFetch.apply(this, [url, options]);
              };

            var findlive = setInterval(function(){
                if(targetUrl != "" && document.querySelector(".__leftContainer"))
                {
                    console.log("=============");
                    addcss();
                    var savebtn = document.createElement("button");
                    savebtn.textContent="直播录制";
                    savebtn.className="jbtn";
                    savebtn.addEventListener("click",function(){
                        var ddd = document.createElement('a');
                        ddd.href = targetUrl;
                        ddd.target = "_blank";
                        ddd.setAttribute("download","");
                        document.body.appendChild(ddd);
                        ddd.click();
                        document.body.removeChild(ddd);
                    });
                    document.querySelector(".__leftContainer").appendChild(savebtn);
                    clearInterval(findlive);
                    savebtn.previousSibling.style="margin-right:10px";
                }
            },200);
        } 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 addcss()
    {
               const style = document.createElement('style');
    style.innerHTML = `
            .jbtn {
            magin-left:20px;
             padding: 5px 25px;
             font-size: 14px;
              border: none;
              outline: none;
              color: rgb(255, 255, 255);
              background: #111;
              cursor: pointer;
              position: relative;
              z-index: 0;
              border-radius: 10px;
              user-select: none;
              -webkit-user-select: none;
              touch-action: manipulation;
            }

            .jbtn:before {
              content: "";
              background: linear-gradient(
                45deg,
                #ff0000,
                #ff7300,
                #fffb00,
                #48ff00,
                #00ffd5,
                #002bff,
                #7a00ff,
                #ff00c8,
                #ff0000
              );
              position: absolute;
              top: -2px;
              left: -2px;
              background-size: 400%;
              z-index: -1;
              filter: blur(5px);
              -webkit-filter: blur(5px);
              width: calc(100% + 4px);
              height: calc(100% + 4px);
              animation: glowing-jbtn 20s linear infinite;
              transition: opacity 0.3s ease-in-out;
              border-radius: 10px;
            }

            @keyframes glowing-jbtn {
              0% {
                background-position: 0 0;
              }
              50% {
                background-position: 400% 0;
              }
              100% {
                background-position: 0 0;
              }
            }

            .jbtn:after {
              z-index: -1;
              content: "";
              position: absolute;
              width: 100%;
              height: 100%;
              background: #222;
              left: 0;
              top: 0;
              border-radius: 10px;
            }
    `;
    document.head.appendChild(style);
    }

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

})();