M3U8 Video Detector and Downloader

Automatically detect the m3u8 video of the page and download it completely. Fixed URL encoding issue to ensure PotPlayer works correctly.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         m3u8视频侦测下载,调用本地potplayer播放器,调用以后就可以实现VR格式的视频直接3D转2D
// @name:zh-CN   m3u8视频侦测下载器
// @name:zh-TW   m3u8視頻偵測下載器
// @name:en      M3U8 Video Detector and Downloader
// @version      1.5.2
// @description  自动检测页面m3u8视频并进行完整下载。修复了URL编码问题,确保PotPlayer正确调用。
// @description:zh-CN  自动检测页面m3u8视频并进行完整下载。修复了URL编码问题,确保PotPlayer正确调用。
// @description:zh-TW  自動檢測頁面m3u8視頻並進行完整下載。修復了URL編碼問題,確保PotPlayer正確調用。
// @description:en  Automatically detect the m3u8 video of the page and download it completely. Fixed URL encoding issue to ensure PotPlayer works correctly.
// @icon         https://tools.thatwind.com/favicon.png
// @author       allFull
// @namespace    https://tools.thatwind.com/
// @homepage     https://tools.thatwind.com/tool/m3u8downloader
// @match        *://*/*
// @exclude      *://www.diancigaoshou.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/m3u8-parser.min.js
// @connect      *
// @grant        unsafeWindow
// @grant        GM_openInTab
// @grant        GM.openInTab
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @grant        GM_deleteValue
// @grant        GM.deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const mgmapi = {
        addStyle(s) {
            let style = document.createElement("style");
            style.innerHTML = s;
            document.documentElement.appendChild(style);
        },
        async getValue(name, defaultVal) {
            return await ((typeof GM_getValue === "function") ? GM_getValue : GM.getValue)(name, defaultVal);
        },
        async setValue(name, value) {
            return await ((typeof GM_setValue === "function") ? GM_setValue : GM.setValue)(name, value);
        },
        async deleteValue(name) {
            return await ((typeof GM_deleteValue === "function") ? GM_deleteValue : GM.deleteValue)(name);
        },
        openInTab(url, open_in_background = false) {
            return ((typeof GM_openInTab === "function") ? GM_openInTab : GM.openInTab)(url, open_in_background);
        },
        xmlHttpRequest(details) {
            return ((typeof GM_xmlhttpRequest === "function") ? GM_xmlhttpRequest : GM.xmlHttpRequest)(details);
        },
        download(details) {
            return this.openInTab(details.url);
        },
        copyText(text) {
            copyTextToClipboard(text);
            function copyTextToClipboard(text) {
                var copyFrom = document.createElement("textarea");
                copyFrom.textContent = text;
                document.body.appendChild(copyFrom);
                copyFrom.select();
                document.execCommand('copy');
                copyFrom.blur();
                document.body.removeChild(copyFrom);
            }
        },
        message(text, disappearTime = 5000) {
            const id = "f8243rd238-gm-message-panel";
            let p = document.querySelector(`#${id}`);
            if (!p) {
                p = document.createElement("div");
                p.id = id;
                p.style = `
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    display: flex;
                    flex-direction: column;
                    align-items: end;
                    z-index: 999999999999999;
                `;
                (document.body || document.documentElement).appendChild(p);
            }
            let mdiv = document.createElement("div");
            mdiv.innerText = text;
            mdiv.style = `
                padding: 3px 8px;
                border-radius: 5px;
                background: black;
                box-shadow: #000 1px 2px 5px;
                margin-top: 10px;
                font-size: small;
                color: #fff;
                text-align: right;
            `;
            p.appendChild(mdiv);
            setTimeout(() => {
                p.removeChild(mdiv);
            }, disappearTime);
        }
    };

    // 播放器配置 - 修复URL编码问题
    const players = {
        "vlc": {
            name: "VLC Player",
            protocol: "vlc://",
            // VLC需要编码的URL
            format: (url) => `vlc://${encodeURIComponent(url)}`
        },
        "potplayer": {
            name: "PotPlayer",
            protocol: "potplayer://",
            // PotPlayer需要未编码的原始URL
            format: (url) => `potplayer://${url}`
        },
        "mpc-hc": {
            name: "MPC-HC",
            protocol: "mpc-hc://",
            format: (url) => `mpc-hc://open/file?url=${encodeURIComponent(url)}`
        },
        "mpv": {
            name: "MPV",
            protocol: "mpv://",
            format: (url) => `mpv://${url}`
        }
    };

    // 获取默认播放器 - 默认使用PotPlayer
    async function getDefaultPlayer() {
        const defaultPlayer = await mgmapi.getValue("defaultPlayer", "potplayer");
        return players[defaultPlayer] ? defaultPlayer : "potplayer";
    }

    // 设置默认播放器
    async function setDefaultPlayer(playerId) {
        if (players[playerId]) {
            await mgmapi.setValue("defaultPlayer", playerId);
            return true;
        }
        return false;
    }

    // 调用本地播放器 - 确保URL正确处理
    function launchLocalPlayer(url) {
        // 确保URL是解码后的原始格式
        let decodedUrl = url;
        try {
            // 检查是否已编码,如果是则解码
            if (decodedUrl !== decodeURIComponent(decodedUrl)) {
                decodedUrl = decodeURIComponent(decodedUrl);
            }
        } catch (e) {
            console.log("URL无需解码或解码失败,使用原始URL:", e);
        }

        getDefaultPlayer().then(defaultPlayer => {
            try {
                const player = players[defaultPlayer];
                const playerUrl = player.format(decodedUrl);

                // 创建一个隐藏的链接并点击它来调用本地播放器
                const link = document.createElement('a');
                link.href = playerUrl;
                link.style.display = 'none';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);

                mgmapi.message(`正在使用${player.name}播放...\nPlaying with ${player.name}...`, 3000);
            } catch (e) {
                mgmapi.message(`调用播放器失败,请确保已安装${players[defaultPlayer].name}\nFailed to launch player. Please ensure ${players[defaultPlayer].name} is installed.`, 5000);
                console.error("播放器调用失败:", e);
            }
        });
    }

    // 添加播放器设置按钮
    function addPlayerSettings() {
        const settingsDiv = document.createElement("div");
        settingsDiv.className = "player-settings";
        settingsDiv.style = `
            color: white;
            padding: 5px 10px;
            background: #333;
            border-radius: 3px;
            font-size: 12px;
            margin-bottom: 5px;
        `;

        let settingsHtml = "<span>默认播放器 (Default Player): </span>";
        Object.keys(players).forEach(playerId => {
            settingsHtml += `
                <span
                    class="player-option ${playerId}"
                    style="margin: 0 3px; cursor: pointer; padding: 2px 5px; border-radius: 2px;"
                    data-player="${playerId}"
                >
                    ${players[playerId].name}
                </span>
            `;
        });

        settingsDiv.innerHTML = settingsHtml;
        wrapper.appendChild(settingsDiv);

        // 设置默认选中状态
        getDefaultPlayer().then(defaultPlayer => {
            document.querySelector(`.player-option.${defaultPlayer}`).style.background = "#666";
        });

        // 绑定播放器选择事件
        document.querySelectorAll(".player-option").forEach(option => {
            option.addEventListener("click", function() {
                const playerId = this.getAttribute("data-player");
                setDefaultPlayer(playerId).then(success => {
                    if (success) {
                        document.querySelectorAll(".player-option").forEach(opt => {
                            opt.style.background = "";
                        });
                        this.style.background = "#666";
                        mgmapi.message(`默认播放器已设置为${players[playerId].name}\nDefault player set to ${players[playerId].name}`, 2000);
                    }
                });
            });
        });
    }


    if (location.host === "tools.thatwind.com" || location.host === "localhost:3000") {
        mgmapi.addStyle("#userscript-tip{display:none !important;}");

        // 对请求做代理
        const _fetch = unsafeWindow.fetch;
        unsafeWindow.fetch = async function (...args) {
            try {
                let response = await _fetch(...args);
                if (response.status !== 200) throw new Error(response.status);
                return response;
            } catch (e) {
                // 失败请求使用代理
                if (args.length == 1) {
                    console.log(`请求代理:${args[0]}`);
                    return await new Promise((resolve, reject) => {
                        let referer = new URLSearchParams(location.hash.slice(1)).get("referer");
                        let headers = {};
                        if (referer) {
                            referer = new URL(referer);
                            headers = {
                                "origin": referer.origin,
                                "referer": referer.href
                            };
                        }
                        mgmapi.xmlHttpRequest({
                            method: "GET",
                            url: args[0],
                            responseType: 'arraybuffer',
                            headers,
                            onload(r) {
                                resolve({
                                    status: r.status,
                                    headers: new Headers(r.responseHeaders.split("\n").filter(n => n).map(s => s.split(/:\s*/)).reduce((all, [a, b]) => { all[a] = b; return all; }, {})),
                                    async text() {
                                        return r.responseText;
                                    },
                                    async arrayBuffer() {
                                        return r.response;
                                    }
                                });
                            },
                            onerror() {
                                reject(new Error());
                            }
                        });
                    });
                } else {
                    throw e;
                }
            }
        }

        return;
    }


    // iframe 信息交流
    window.addEventListener("message", async (e) => {
        if (e.data === "3j4t9uj349-gm-get-title") {
            let name = `top-title-${Date.now()}`;
            await mgmapi.setValue(name, document.title);
            e.source.postMessage(`3j4t9uj349-gm-top-title-name:${name}`, "*");
        }
    });

    function getTopTitle() {
        return new Promise(resolve => {
            window.addEventListener("message", async function l(e) {
                if (typeof e.data === "string") {
                    if (e.data.startsWith("3j4t9uj349-gm-top-title-name:")) {
                        let name = e.data.slice("3j4t9uj349-gm-top-title-name:".length);
                        await new Promise(r => setTimeout(r, 5));
                        resolve(await mgmapi.getValue(name));
                        mgmapi.deleteValue(name);
                        window.removeEventListener("message", l);
                    }
                }
            });
            window.top.postMessage("3j4t9uj349-gm-get-title", "*");
        });
    }


    {
        // 请求检测
        const _r_text = unsafeWindow.Response.prototype.text;
        unsafeWindow.Response.prototype.text = function () {
            return new Promise((resolve, reject) => {
                _r_text.call(this).then((text) => {
                    resolve(text);
                    if (checkContent(text)) doM3U({ url: this.url, content: text });
                }).catch(reject);
            });
        }

        const _open = unsafeWindow.XMLHttpRequest.prototype.open;
        unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
            this.addEventListener("load", () => {
                try {
                    let content = this.responseText;
                    if (checkContent(content)) doM3U({ url: args[1], content });
                } catch { }
            });
            return _open.apply(this, args);
        }


        function checkUrl(url) {
            url = new URL(url, location.href);
            if (url.pathname.endsWith(".m3u8") || url.pathname.endsWith(".m3u")) {
                return true;
            }
        }

        function checkContent(content) {
            if (content.trim().startsWith("#EXTM3U")) {
                return true;
            }
        }


        // 检查纯视频
        setInterval(doVideos, 1000);

    }

    const rootDiv = document.createElement("div");
    rootDiv.style = `
        position: fixed;
        z-index: 9999999999999999;
        opacity: 0.9;
    `;
    rootDiv.style.display = "none";
    document.documentElement.appendChild(rootDiv);

    const shadowDOM = rootDiv.attachShadow({ mode: 'open' });
    const wrapper = document.createElement("div");
    shadowDOM.appendChild(wrapper);


    // 指示器
    const bar = document.createElement("div");
    bar.style = `
        text-align: right;
    `;
    bar.innerHTML = `
        <span
            class="number-indicator"
            data-number="0"
            style="
                display: inline-flex;
                width: 25px;
                height: 25px;
                background: black;
                padding: 10px;
                border-radius: 100px;
                margin-bottom: 5px;
                cursor: pointer;
                border: 3px solid #83838382;
            "
        >
            <svg
            style="
                filter: invert(1);
            "
            version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 585.913 585.913" style="enable-background:new 0 0 585.913 585.913;" xml:space="preserve">
                <g>
                    <path d="M11.173,46.2v492.311l346.22,47.402V535.33c0.776,0.058,1.542,0.109,2.329,0.109h177.39
                    c20.75,0,37.627-16.883,37.627-37.627V86.597c0-20.743-16.877-37.628-37.627-37.628h-177.39c-0.781,0-1.553,0.077-2.329,0.124V0
                    L11.173,46.2z M110.382,345.888l-1.37-38.273c-0.416-11.998-0.822-26.514-0.822-41.023l-0.415,0.01
                    c-2.867,12.767-6.678,26.956-10.187,38.567l-10.961,38.211l-15.567-0.582l-9.239-37.598c-2.801-11.269-5.709-24.905-7.725-37.361
                    l-0.25,0.005c-0.503,12.914-0.879,27.657-1.503,39.552L50.84,343.6l-17.385-0.672l5.252-94.208l25.415-0.996l8.499,32.064
                    c2.724,11.224,5.467,23.364,7.428,34.819h0.389c2.503-11.291,5.535-24.221,8.454-35.168l9.643-33.042l27.436-1.071l5.237,101.377
                    L110.382,345.888z M172.479,349.999c-12.569-0.504-23.013-4.272-28.539-8.142l4.504-17.249c3.939,2.226,13.1,6.445,22.373,6.687
                    c12.009,0.32,18.174-5.497,18.174-13.218c0-10.068-9.838-14.683-19.979-14.74l-9.253-0.052v-16.777l8.801-0.066
                    c7.708-0.208,17.646-3.262,17.646-11.905c0-6.121-4.914-10.562-14.635-10.331c-7.95,0.189-16.245,3.914-20.213,6.446l-4.52-16.693
                    c5.693-4.008,17.224-8.11,29.883-8.588c21.457-0.795,33.643,10.407,33.643,24.625c0,11.029-6.197,19.691-18.738,24.161v0.314
                    c12.229,2.216,22.266,11.663,22.266,25.281C213.89,338.188,197.866,351.001,172.479,349.999z M331.104,302.986
                    c0,36.126-19.55,52.541-51.193,51.286c-29.318-1.166-46.019-17.103-46.019-52.044v-61.104l25.711-1.006v64.201
                    c0,19.191,7.562,29.146,21.179,29.502c14.234,0.368,22.189-8.976,22.189-29.26v-66.125l28.122-1.097v65.647H331.104z
                    M359.723,70.476h177.39c8.893,0,16.125,7.236,16.125,16.126v411.22c0,8.888-7.232,16.127-16.125,16.127h-177.39
                    c-0.792,0-1.563-0.116-2.329-0.232V380.782c17.685,14.961,40.504,24.032,65.434,24.032c56.037,0,101.607-45.576,101.607-101.599
                    c0-56.029-45.581-101.603-101.607-101.603c-24.93,0-47.749,9.069-65.434,24.035V70.728
                    C358.159,70.599,358.926,70.476,359.723,70.476z M390.873,364.519V245.241c0-1.07,0.615-2.071,1.586-2.521
                    c0.981-0.483,2.13-0.365,2.981,0.307l93.393,59.623c0.666,0.556,1.065,1.376,1.065,2.215c0,0.841-0.399,1.67-1.065,2.215
                    l-93.397,59.628c-0.509,0.4-1.114,0.61-1.743,0.61l-1.233-0.289C391.488,366.588,390.873,365.585,390.873,364.519z" />
                </g>
            </svg>
        </span>
    `;

    wrapper.appendChild(bar);

    // 添加播放器设置
    addPlayerSettings();

    // 样式
    const style = document.createElement("style");

    style.innerHTML = `
        .number-indicator{
            position:relative;
        }

        .number-indicator::after{
            content: attr(data-number);
            position: absolute;
            bottom: 0;
            right: 0;
            color: #40a9ff;
            font-size: 14px;
            font-weight: bold;
            background: #000;
            border-radius: 10px;
            padding: 3px 5px;
        }

        .copy-link:active{
            color: #ccc;
        }

        .download-btn:hover, .local-play-btn:hover{
            text-decoration: underline;
        }
        .download-btn:active, .local-play-btn:active{
            opacity: 0.9;
        }

        .m3u8-item{
            color: white;
            margin-bottom: 5px;
            display: flex;
            flex-direction: row;
            background: black;
            padding: 3px 10px;
            border-radius: 3px;
            font-size: 14px;
            user-select: none;
            align-items: center;
            flex-wrap: wrap;
        }

        [data-shown="false"] {
            opacity: 0.8;
            zoom: 0.8;
        }

        [data-shown="false"]:hover{
            opacity: 1;
        }

        [data-shown="false"] .m3u8-item,
        [data-shown="false"] .player-settings {
            display: none;
        }

        .local-play-btn {
            margin-left: 10px;
            cursor: pointer;
            color: #40a9ff;
            white-space: nowrap;
        }

        .player-settings {
            user-select: none;
        }

        .player-option:hover {
            background-color: #555;
        }

        .player-option.potplayer {
            background-color: #666;
        }
    `;

    wrapper.appendChild(style);


    const barBtn = bar.querySelector(".number-indicator");

    // 关于显隐和移动
    (async function () {
        let shown = await mgmapi.getValue("shown", true);
        wrapper.setAttribute("data-shown", shown);

        let x = await mgmapi.getValue("x", 10);
        let y = await mgmapi.getValue("y", 10);

        x = Math.min(innerWidth - 50, x);
        y = Math.min(innerHeight - 50, y);

        if (x < 0) x = 0;
        if (y < 0) y = 0;

        rootDiv.style.top = `${y}px`;
        rootDiv.style.right = `${x}px`;

        barBtn.addEventListener("mousedown", e => {
            let startX = e.pageX;
            let startY = e.pageY;

            let moved = false;

            let mousemove = e => {
                let offsetX = e.pageX - startX;
                let offsetY = e.pageY - startY;
                if (moved || (Math.abs(offsetX) + Math.abs(offsetY)) > 5) {
                    moved = true;
                    rootDiv.style.top = `${y + offsetY}px`;
                    rootDiv.style.right = `${x - offsetX}px`;
                }
            };
            let mouseup = e => {
                let offsetX = e.pageX - startX;
                let offsetY = e.pageY - startY;

                if (moved) {
                    x -= offsetX;
                    y += offsetY;
                    mgmapi.setValue("x", x);
                    mgmapi.setValue("y", y);
                } else {
                    shown = !shown;
                    mgmapi.setValue("shown", shown);
                    wrapper.setAttribute("data-shown", shown);
                }

                removeEventListener("mousemove", mousemove);
                removeEventListener("mouseup", mouseup);
            }
            addEventListener("mousemove", mousemove);
            addEventListener("mouseup", mouseup);
        });
    })();


    let count = 0;
    let shownUrls = [];


    function doVideos() {
        for (let v of Array.from(document.querySelectorAll("video"))) {
            if (v.duration && v.src && v.src.startsWith("http") && (!shownUrls.includes(v.src))) {
                const src = v.src;

                shownUrls.push(src);
                showVideo({
                    type: "video",
                    url: new URL(src),
                    duration: `${Math.ceil(v.duration * 10 / 60) / 10} mins`,
                    download() {
                        const details = {
                            url: src,
                            name: (() => {
                                let name = new URL(src).pathname.split("/").slice(-1)[0];
                                if (!/\.\w+$/.test(name)) {
                                    if (name.match(/^\s*$/)) name = Date.now();
                                    name = name + ".mp4";
                                }
                                return name;
                            })(),
                            headers: {
                                origin: location.origin
                            },
                            onerror(e) {
                                mgmapi.openInTab(src);
                            }
                        };
                        mgmapi.download(details);
                    },
                    playLocally() {
                        launchLocalPlayer(src);
                    }
                })
            }
        }
    }

    async function doM3U({ url, content }) {
        url = new URL(url);

        if (shownUrls.includes(url.href)) return;

        // 解析 m3u
        content = content || await (await fetch(url)).text();

        const parser = new m3u8Parser.Parser();
        parser.push(content);
        parser.end();
        const manifest = parser.manifest;

        if (manifest.segments) {
            let duration = 0;
            manifest.segments.forEach((segment) => {
                duration += segment.duration;
            });
            manifest.duration = duration;
        }

        showVideo({
            type: "m3u8",
            url,
            duration: manifest.duration ? `${Math.ceil(manifest.duration * 10 / 60) / 10} mins` : manifest.playlists ? `多(Multi)(${manifest.playlists.length})` : "未知(unknown)",
            async download() {
                mgmapi.openInTab(
                    `https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({
                        m3u8: url.href,
                        referer: location.href,
                        filename: (await getTopTitle()) || ""
                    })}`
                );
            },
            playLocally() {
                launchLocalPlayer(url.href);
            }
        })
    }


    async function showVideo({
        type,
        url,
        duration,
        download,
        playLocally
    }) {
        let div = document.createElement("div");
        div.className = "m3u8-item";
        div.innerHTML = `
            <span>${type}</span>
            <span
                class="copy-link"
                title="${url}"
                style="
                    max-width: 200px;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    overflow: hidden;
                    margin-left: 10px;
                "
            >${url.pathname}</span>
            <span
                style="
                    margin-left: 10px;
                    flex-grow: 1;
                    white-space: nowrap;
                "
            >${duration}</span>
            <span
                class="local-play-btn"
                style="
                "
            >本地播放(Local)</span>
            <span
                class="download-btn"
                style="
                    margin-left: 10px;
                    cursor: pointer;
            ">下载(Download)</span>
        `;

        div.querySelector(".copy-link").addEventListener("click", () => {
            mgmapi.copyText(url.href);
            mgmapi.message("已复制链接 (link copied)", 2000);
        });

        div.querySelector(".download-btn").addEventListener("click", download);
        div.querySelector(".local-play-btn").addEventListener("click", playLocally);

        rootDiv.style.display = "block";

        count++;
        shownUrls.push(url.href);
        bar.querySelector(".number-indicator").setAttribute("data-number", count);
        wrapper.appendChild(div);
    }

})();

(function () {
    'use strict';

    const reg = /magnet:\?xt=urn:btih:\w{10,}([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;

    let l = navigator.language || "en";
    if (l.startsWith("en-")) l = "en";
    else if (l.startsWith("zh-")) l = "zh-CN";
    else l = "en";

    const T = {
        "en": {
            play: "Play",
            localPlay: "Local Play (PotPlayer)"
        },
        "zh-CN": {
            play: '播放',
            localPlay: '本地播放 (PotPlayer)'
        }
    }[l];

    // 播放器配置 - 磁力链接专用,修复URL编码
    const magnetPlayers = {
        "vlc": {
            name: "VLC Player",
            protocol: "vlc://",
            format: (url) => `vlc://${encodeURIComponent(url)}`
        },
        "potplayer": {
            name: "PotPlayer",
            protocol: "potplayer://",
            // PotPlayer磁力链接也使用未编码的URL
            format: (url) => `potplayer://${url}`
        }
    };

    // 获取默认磁力链接播放器
    async function getMagnetDefaultPlayer() {
        const defaultPlayer = await GM.getValue("magnetDefaultPlayer", "potplayer");
        return magnetPlayers[defaultPlayer] ? defaultPlayer : "potplayer";
    }

    // 调用本地播放器播放磁力链接 - 修复URL编码问题
    function launchMagnetLocalPlayer(url) {
        // 确保URL解码
        let decodedUrl = url;
        try {
            if (decodedUrl !== decodeURIComponent(decodedUrl)) {
                decodedUrl = decodeURIComponent(decodedUrl);
            }
        } catch (e) {
            console.log("磁力链接URL无需解码或解码失败:", e);
        }

        getMagnetDefaultPlayer().then(defaultPlayer => {
            try {
                const player = magnetPlayers[defaultPlayer];
                const playerUrl = player.format(decodedUrl);

                const link = document.createElement('a');
                link.href = playerUrl;
                link.style.display = 'none';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);

                const messageDiv = document.createElement('div');
                messageDiv.textContent = `正在使用PotPlayer播放...`;
                messageDiv.style = `
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    padding: 8px 12px;
                    background: #333;
                    color: white;
                    border-radius: 4px;
                    z-index: 999999;
                `;
                document.body.appendChild(messageDiv);

                setTimeout(() => {
                    messageDiv.remove();
                }, 3000);
            } catch (e) {
                console.error("磁力链接播放器调用失败:", e);
                const errorDiv = document.createElement('div');
                errorDiv.textContent = `调用PotPlayer失败,请确保已安装PotPlayer并正确配置`;
                errorDiv.style = `
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    padding: 8px 12px;
                    background: #ff4444;
                    color: white;
                    border-radius: 4px;
                    z-index: 999999;
                `;
                document.body.appendChild(errorDiv);

                setTimeout(() => {
                    errorDiv.remove();
                }, 5000);
            }
        });
    }

    whenDOMReady(() => {
        addStyle(`
            button[data-wtmzjk-mag-url]{
                all: initial;
                border: none;
                outline: none;
                background: none;
                background: #08a6f7;
                margin: 2px 8px;
                border-radius: 3px;
                color: white;
                cursor: pointer;
                display: inline-flex;
                height: 1.6em;
                padding: 0 .8em;
                align-items: center;
                justify-content: center;
                transition: background .15s;
                text-decoration: none;
                border-radius: 0.8em;
                font-size: small;
            }
            button[data-wtmzjk-mag-local]{
                all: initial;
                border: none;
                outline: none;
                background: none;
                background: #4CAF50;
                margin: 2px 8px;
                border-radius: 3px;
                color: white;
                cursor: pointer;
                display: inline-flex;
                height: 1.6em;
                padding: 0 .8em;
                align-items: center;
                justify-content: center;
                transition: background .15s;
                text-decoration: none;
                border-radius: 0.8em;
                font-size: small;
            }
            button[data-wtmzjk-mag-url]>svg,
            button[data-wtmzjk-mag-local]>svg{
                height: 60%;
                fill: white;
                pointer-events: none;
            }
            button[data-wtmzjk-mag-url]:hover{
                background: #39b9f9;
            }
            button[data-wtmzjk-mag-local]:hover{
                background: #45a049;
            }
            button[data-wtmzjk-mag-url]:active{
                background: #0797df;
            }
            button[data-wtmzjk-mag-local]:active{
                background: #3d8b40;
            }
            button[data-wtmzjk-mag-url]>span,
            button[data-wtmzjk-mag-local]>span{
                pointer-events: none;
                font-size: small;margin-right: .5em;font-weight:bold;color:white !important;
            }
        `);
        window.addEventListener("click", onEvents, true);
        window.addEventListener("mousedown", onEvents, true);
        window.addEventListener("mouseup", onEvents, true);

        watchBodyChange(work);
    });

    function onEvents(e) {
        if (e.target.hasAttribute('data-wtmzjk-mag-url')) {
            e.preventDefault();
            e.stopPropagation();
            if (e.type == "click") {
                let a = document.createElement('a');
                a.href = 'https://www.diancigaoshou.com/#' + new URLSearchParams({ url: e.target.getAttribute('data-wtmzjk-mag-url') });
                a.target = "_blank";
                a.click();
            }
        } else if (e.target.hasAttribute('data-wtmzjk-mag-local')) {
            e.preventDefault();
            e.stopPropagation();
            if (e.type == "click") {
                launchMagnetLocalPlayer(e.target.getAttribute('data-wtmzjk-mag-local'));
            }
        }
    }


    function createWatchButton(url, isForPlain = false) {
        let onlineButton = document.createElement("button");
        onlineButton.setAttribute('data-wtmzjk-mag-url', url);
        if (isForPlain) onlineButton.setAttribute('data-wtmzjk-button-for-plain', '');
        onlineButton.innerHTML = `<span>${T.play}</span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>`;

        let localButton = document.createElement("button");
        localButton.setAttribute('data-wtmzjk-mag-local', url);
        if (isForPlain) localButton.setAttribute('data-wtmzjk-button-for-plain', '');
        localButton.innerHTML = `<span>${T.localPlay}</span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M192 0C85.96 0 0 85.96 0 192s85.96 192 192 192 192-85.96 192-192S298 0 192 0zm0 352c-88.22 0-160-71.78-160-160S103.8 32 192 32s160 71.78 160 160-71.78 160-160 160zm-16-192c0-8.84 7.16-16 16-16h48c8.84 0 16 7.16 16 16v64c0 8.84-7.16 16-16 16h-48c-8.84 0-16-7.16-16-16V160z"/></svg>`;

        let container = document.createElement("span");
        container.appendChild(onlineButton);
        container.appendChild(localButton);

        return container;
    }

    function hasPlainMagUrlThatNotHandled() {
        let m = document.body.textContent.match(new RegExp(reg, 'g'));
        return document.querySelectorAll(`[data-wtmzjk-button-for-plain]`).length != (m ? m.length : 0);
    }

    function work() {
        if (!document.body) return;
        if (hasPlainMagUrlThatNotHandled()) {
            for (let node of getAllTextNodes(document.body)) {
                if (node.nextSibling && node.nextSibling.hasAttribute && node.nextSibling.hasAttribute('data-wtmzjk-mag-url')) continue;
                let text = node.nodeValue;
                if (!reg.test(text)) continue;
                let match = text.match(reg);
                if (match) {
                    let url = match[0];
                    let p = node.parentNode;
                    p.insertBefore(document.createTextNode(text.slice(0, match.index + url.length)), node);
                    p.insertBefore(createWatchButton(url, true), node);
                    p.insertBefore(document.createTextNode(text.slice(match.index + url.length)), node);
                    p.removeChild(node);
                }
            }
        }
        for (let a of Array.from(document.querySelectorAll(
            ['href', 'value', 'data-clipboard-text', 'data-value', 'title', 'alt', 'data-url', 'data-magnet', 'data-copy'].map(n => `[${n}*="magnet:?xt=urn:btih:"]`).join(',')
        ))) {
            if (a.nextSibling && a.nextSibling.hasAttribute && a.nextSibling.hasAttribute('data-wtmzjk-mag-url')) continue;
            if (reg.test(a.textContent)) continue;
            for (let attr of a.getAttributeNames()) {
                let val = a.getAttribute(attr);
                if (!reg.test(val)) continue;
                let url = val.match(reg)[0];
                a.parentNode.insertBefore(createWatchButton(url), a.nextSibling);
            }
        }
    }


    function watchBodyChange(onchange) {
        let timeout;
        let observer = new MutationObserver(() => {
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    onchange();
                }, 200);
            }
        });
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            characterData: true
        });

    }

    function getAllTextNodes(parent) {
        var re = [];
        if (["STYLE", "SCRIPT", "BASE", "COMMAND", "LINK", "META", "TITLE", "XTRANS-TXT", "XTRANS-TXT-GROUP", "XTRANS-POPUP"].includes(parent.tagName)) return re;
        for (let node of parent.childNodes) {
            if (node.childNodes.length) re = re.concat(getAllTextNodes(node));
            else if (Text.prototype.isPrototypeOf(node) && (!node.nodeValue.match(/^\s*$/))) re.push(node);
        }
        return re;
    }

    function whenDOMReady(f) {
        if (document.body) f();
        else window.addEventListener("DOMContentLoaded", f);
    }

    function addStyle(s) {
        let style = document.createElement("style");
        style.innerHTML = s;
        document.documentElement.appendChild(style);
    }

})();