[J]Douyin Video Downloader

Download videos from Douyin website

目前為 2024-04-10 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         [J]Douyin Video Downloader
// @namespace    http://tampermonkey.net/
// @version      2024-03-31
// @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`;
                downloadDom.download = ((new Date()).getTime())+".mp4";
                document.body.appendChild(downloadDom);
                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.style.display = "none";
            downloadDom.href = sourceUrl;
            downloadDom.target = "_blank";
            //downloadDom.download = `${authorName}_${videoDesc}.mp4`;
            downloadDom.download =((new Date()).getTime())+".mp4";
            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();
    };

})();