// ==UserScript==
// @name         m3u8视频侦测下载器【自动嗅探】
// @namespace    https://tools.thatwind.com/
// @homepage     https://tools.thatwind.com/tool/m3u8downloader
// @version      1.0.1
// @description  自动检测页面m3u8视频并进行完整下载。检测到m3u8链接后会自动出现在页面右上角位置,点击下载即可跳转到m3u8下载器。
// @author       allFull
// @match        *://*/*
// @icon         https://tools.thatwind.com/favicon.png
// @require      https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js
// @connect      *
// @grant        unsafeWindow
// @grant        GM_openInTab
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// @downloadURL none
// ==/UserScript==
(function () {
    'use strict';
    if (location.host === "tools.thatwind.com") {
        GM_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
                            };
                        }
                        GM_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;
    }
    {
        // 请求检测
        const _fetch = unsafeWindow.fetch;
        unsafeWindow.fetch = function (...args) {
            checkUrl(args[0]);
            return _fetch(...args);
        }
        const _open = unsafeWindow.XMLHttpRequest.prototype.open;
        unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
            checkUrl(args[1]);
            return _open.apply(this, args);
        }
    }
    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 = `
        
            
        
    `;
    wrapper.appendChild(bar);
    // 样式
    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;
        }
        .download-btn:hover{
            text-decoration: underline;
        }
        .download-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;
        }
        [data-shown="false"] {
            opacity: 0.8;
            zoom: 0.8;
        }
        [data-shown="false"]:hover{
            opacity: 1;
        }
        [data-shown="false"] .m3u8-item{
            display: none;
        }
    `;
    wrapper.appendChild(style);
    const barBtn = bar.querySelector(".number-indicator");
    // 关于显隐和移动
    let shown = GM_getValue("shown", true);
    wrapper.setAttribute("data-shown", shown);
    let x = GM_getValue("x", 10);
    let y = GM_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;
                GM_setValue("x", x);
                GM_setValue("y", y);
            } else {
                shown = !shown;
                GM_setValue("shown", shown);
                wrapper.setAttribute("data-shown", shown);
            }
            removeEventListener("mousemove", mousemove);
            removeEventListener("mouseup", mouseup);
        }
        addEventListener("mousemove", mousemove);
        addEventListener("mouseup", mouseup);
    });
    function checkUrl(url) {
        url = new URL(url, location.href);
        if (url.pathname.endsWith(".m3u8") || url.pathname.endsWith(".m3u")) {
            // 发现
            showM3U(url);
        }
    }
    let count = 0;
    async function showM3U(url) {
        // 解析 m3u
        const 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;
        }
        let div = document.createElement("div");
        div.className = "m3u8-item";
        div.innerHTML = `
            ${url.pathname}
            ${manifest.duration ? `${Math.ceil(manifest.duration * 10 / 60) / 10}分钟` : manifest.playlists ? `多线(${manifest.playlists.length})` : "未知时长"}
            下载
        `;
        div.querySelector(".download-btn").addEventListener("click", () => {
            GM_openInTab(
                `https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({
                    m3u8: url.href,
                    referer: location.href
                })
                }`,
                {
                    active: true
                }
            );
        });
        rootDiv.style.display = "block";
        count++;
        bar.querySelector(".number-indicator").setAttribute("data-number", count);
        wrapper.appendChild(div);
    }
})();