支持单视频以及主页所有视频下载,HD画质访问用户主页时,右下角会出现下载按钮,访问单视频时,播放界面会显示下载按钮,自行复制代码到油猴中新建脚本保存即可。

// ==UserScript==
//               Tiktok视频下载
//          tiktok_video_download
//            1.0
// @description       解析和下载Tiktok视频(无水印)
//             Webrobot
//               
//              *://*.tiktok.com/*
//              GM_openInTab
// @grant             GM_xmlhttpRequest
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_addStyle
// @run-at            document-idle
// ==/UserScript==

(function () {
    'use strict';

    // 通用方法定义
    const commonFunctionObject = {
        GMopenInTab: function (url, options = { active: true, insert: true, setParent: true }) {
            if (typeof GM_openInTab === "function") {
                GM_openInTab(url, options);
            }
        },
        GMaddStyle: function (css) {
            const style = document.createElement('style');
            style.textContent = css;
            document.head.appendChild(style);
        },
        webToast: function (params) {
            const { message, background } = params;
            const toast = document.createElement('div');
            toast.style.position = 'fixed';
            toast.style.bottom = '20px';
            toast.style.left = '50%';
            toast.style.transform = 'translateX(-50%)';
            toast.style.background = background || 'rgba(0, 0, 0, 0.7)';
            toast.style.color = '#fff';
            toast.style.padding = '10px';
            toast.style.borderRadius = '5px';
            toast.style.zIndex = '99999';
            toast.innerText = message;
            document.body.appendChild(toast);
            setTimeout(() => document.body.removeChild(toast), 1500);
        },
        request: function (method, url, data, headers = { "Content-Type": "application/json;charset=UTF-8" }) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    url: url,
                    method: method,
                    data: data,
                    headers: headers,
                    onload: function (response) {
                        if (response.status === 200) {
                            resolve({ result: 'success', data: response.responseText });
                        } else {
                            reject({ result: 'error', data: null });
                        }
                    }
                });
            });
        }
    };

    function Tiktok() {
        this.extractHref = function (html) {
            const regex = /<a\s+(?:[^>]*?\s+)?href=(['"])(.*?)\1/gi;
            const hrefs = [];
            let match;
            while ((match = regex.exec(html)) !== null) {
                hrefs.push(match[2]);
            }
            return hrefs.filter((href) => href.indexOf("snapcdn.app") != -1);
        };

        this.extractAllVideoLinks = function () {
            const videoLinks = [];
            const anchorTags = document.querySelectorAll('a');
            anchorTags.forEach(anchor => {
                const href = anchor.getAttribute('href');
                if (href && /^https:\/\/www\.tiktok\.com\/@[^/]+\/video\/\d+/.test(href)) {
                    videoLinks.push(href);
                }
            });
            return videoLinks;
        };

        this.scrollToBottom = async function () {
            return new Promise((resolve) => {
                let previousHeight = 0;
                const scrollInterval = setInterval(() => {
                    window.scrollTo(0, document.body.scrollHeight);
                    if (document.body.scrollHeight !== previousHeight) {
                        previousHeight = document.body.scrollHeight;
                    } else {
                        clearInterval(scrollInterval);
                        resolve();
                    }
                }, 1000);
            });
        };

        this.downloadSingleVideo = async function (url, element) {
            commonFunctionObject.webToast({ "message": "正在下载中.", "background": "#000" });
            element.classList.add("download-loadding");

            const data = await commonFunctionObject.request("POST", "https://tikdownloader.io/api/ajaxSearch",
                "q=" + url + "&lang=en", { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" });
            if (data.result === "success") {
                const result = JSON.parse(data.data);
                if (result.status == "ok" && result.hasOwnProperty("data")) {
                    const data = result.data;
                    const downloadUrls = this.extractHref(data);
                    if (downloadUrls.length >= 2) {
                        commonFunctionObject.GMopenInTab(downloadUrls.at(-2));
                    }
                }
            }

            element.classList.remove("download-loadding");
        };

        this.downloadAllVideos = async function (element) {
            commonFunctionObject.webToast({ "message": "正在加载所有视频,请稍候...", "background": "#000" });
            element.classList.add("download-loadding");

            await this.scrollToBottom();  // 先滚动到页面底部,加载所有视频

            const videoLinks = this.extractAllVideoLinks();
            if (videoLinks.length === 0) {
                commonFunctionObject.webToast({ "message": "未找到任何视频链接", "background": "#000" });
                element.classList.remove("download-loadding");
                return;
            }

            commonFunctionObject.webToast({ "message": "开始下载所有视频中...", "background": "#000" });
            for (const videoLink of videoLinks) {
                const data = await commonFunctionObject.request("POST", "https://tikdownloader.io/api/ajaxSearch",
                    "q=" + videoLink + "&lang=en", { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" });
                if (data.result === "success") {
                    const result = JSON.parse(data.data);
                    if (result.status == "ok" && result.hasOwnProperty("data")) {
                        const data = result.data;
                        const downloadUrls = this.extractHref(data);
                        if (downloadUrls.length >= 2) {
                            commonFunctionObject.GMopenInTab(downloadUrls.at(-2));
                        }
                    }
                }
            }

            element.classList.remove("download-loadding");
        };

        this.start = async function () {
            this.removeButtons();
            const currentUrl = window.location.href;
            if (/www\.tiktok\.com\/@[^/]+$/.test(currentUrl)) {
                this.addBatchDownloadButton();
            } else if (/www\.tiktok\.com\/@[^/]+\/video\/\d+/.test(currentUrl)) {
                this.addSingleDownloadButton();
            }
        };

        this.addBatchDownloadButton = function () {
            commonFunctionObject.GMaddStyle(`
                @keyframes scriptspin {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}}
                .download-loadding{
                    animation: scriptspin 1s linear infinite;
                }
                #tiktok-download-990i {
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    background: #ff2d55;
                    border-radius: 50%;
                    padding: 10px;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
                    cursor: pointer;
                    z-index: 9999;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                }
                #tiktok-download-990i svg {
                    fill: #ffffff;
                }
            `);

            if (!document.querySelector("#tiktok-download-990i")) {
                const container = document.querySelector('body');
                if (!container) {
                    return;
                }

                let downloadButton = document.createElement('div');
                downloadButton.id = "tiktok-download-990i";
                downloadButton.innerHTML = `<svg t="1724300009050" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5307" width="35" height="35"><path d="M298.666667 554.666667v85.333333H256v128h512v-128h-42.666667v-85.333333h128v213.333333a85.333333 85.333333 0 0 1-78.933333 85.077333L768 853.333333H256a85.333333 85.333333 0 0 1-85.12-78.933333L170.666667 768v-213.333333h128z" fill="#ffffff" p-id="5308"></path><path d="M512 627.498667l219.477333-219.477334h-120.704L512 506.88 413.141333 408.021333H292.522667L512 627.498667z" fill="#ffffff" p-id="5309"></path><path d="M554.666667 528V167.978667h-85.333334v360.021333h85.333334z" fill="#ffffff" p-id="5310"></path></svg>`;
                downloadButton.title = "点击下载所有视频(高清无水印)";
                downloadButton.addEventListener("click", () => {
                    this.downloadAllVideos(downloadButton);
                });

                container.appendChild(downloadButton);
            }
        };

        this.addSingleDownloadButton = function () {
            commonFunctionObject.GMaddStyle(`
                @keyframes scriptspin {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}}
                .download-loadding{
                    animation: scriptspin 1s linear infinite;
                }
            `);

            if (!document.querySelector("#tiktok-download-single-990i")) {
                const container = document.querySelector('#main-content-video_detail') || document.body;
                if (!container) {
                    return;
                }

                const divs = container.querySelectorAll("div");
                const regex = /-DivRightControlsWrapper|-DivMiniPlayerContainer/;
                const matchedDiv = Array.from(divs).find(div => {
                    return div.classList.value.split(' ').some(className => {
                        return regex.test(className);
                    });
                });

                if (matchedDiv) {
                    let cloneNode = null;
                    let isDetail = matchedDiv.children.length != 1;
                    if (isDetail) {
                        cloneNode = matchedDiv.children[0].cloneNode(true);
                    } else {
                        cloneNode = matchedDiv.cloneNode(true);
                    }
                    cloneNode.id = "tiktok-download-single-990i";
                    cloneNode.querySelector("div").innerHTML = `<svg t="1724300009050" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5307" width="35" height="35"><path d="M298.666667 554.666667v85.333333H256v128h512v-128h-42.666667v-85.333333h128v213.333333a85.333333 85.333333 0 0 1-78.933333 85.077333L768 853.333333H256a85.333333 85.333333 0 0 1-85.12-78.933333L170.666667 768v-213.333333h128z" fill="#ffffff" p-id="5308"></path><path d="M512 627.498667l219.477333-219.477334h-120.704L512 506.88 413.141333 408.021333H292.522667L512 627.498667z" fill="#ffffff" p-id="5309"></path><path d="M554.666667 528V167.978667h-85.333334v360.021333h85.333334z" fill="#ffffff" p-id="5310"></path></svg>`;
                    if (isDetail) {
                        matchedDiv.insertBefore(cloneNode, matchedDiv.children[0]);
                    } else {
                        cloneNode.style.right = (166) + "px";
                        matchedDiv.parentNode.insertBefore(cloneNode, matchedDiv);
                    }
                    cloneNode.title = "点击下载视频(高清无水印)";
                    cloneNode.addEventListener("click", () => {
                        this.downloadSingleVideo(window.location.href, cloneNode);
                    });
                }
            }
        };

        this.removeButtons = function () {
            const batchButton = document.querySelector("#tiktok-download-990i");
            if (batchButton) {
                batchButton.remove();
            }

            const singleButton = document.querySelector("#tiktok-download-single-990i");
            if (singleButton) {
                singleButton.remove();
            }
        };

        this.observeUrlChange = function () {
            let previousUrl = window.location.href;
            const observer = new MutationObserver(() => {
                if (previousUrl !== window.location.href) {
                    previousUrl = window.location.href;
                    this.start();
                }
            });
            const config = { subtree: true, childList: true };
            observer.observe(document, config);
        };
    }

    try {
        const tiktok = new Tiktok();
        tiktok.start();
        tiktok.observeUrlChange();
    } catch (e) {
        console.log("Tiktok视频下载脚本出错:" + e);
    }
})();