m3u8视频侦测下载,调用本地potplayer播放器,调用以后就可以实现VR格式的视频直接3D转2D

自动检测页面m3u8视频并进行完整下载。修复了URL编码问题,确保PotPlayer正确调用。

// ==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);
    }

})();