dowload from lurl&myppt automatically

It will download the media from lurl and myppt automatically

// ==UserScript==
// @name         dowload from  lurl&myppt automatically 
// @name:zh-CN   从lurl&myppt自动下载
// @name:zh-TW   從lurl&myppt自動下載
// @namespace    org.jw23.dcardtools
// @version      0.1.42
// @description  It will download the media from lurl and myppt automatically
// @description:zh-TW 從lurl和mypppt自動下載媒體
// @description:zh-CN 从lurl和myppt自动下载媒体
// @author       jw23
// @match        https://lurl.cc/*
// @match        https://iiil.io/*
// @match        https://myppt.cc/*
// @match        https://mork.ro/*
// @match        https://risu.io/*
// @match        https://cat.85xvideo.com/*
// @run-at       document-start
// @grant        GM_download
// @grant        GM.xmlHttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @connect      lurl.cc
// @connect      *.lurl.cc
// @connect      risu.io
// @connect      85xvideo.com
// @connect      mork.ro
// @connect      myppt.cc
// @connect      *.myppt.cc
// @license     CC BY-NC 
// ==/UserScript==

// type: image? video?


const urlGuards = [
    {
        guard: (url) => {
            return url && /lurl\.cc\/\d+/.test(url)
        },
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },

    {
        guard: (url) => {
            return url && /myppt\.cc\/\d+/.test(url)
        },
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },
    {
        guard: (url) => {
            return url && url.indexOf('storage') !== -1
        },
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },
    {
        guard: (url) => {
            return true
        },
        extractFilename: (url) => {
            let pureUrl = url.split('?')[0]
            let splices = pureUrl.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    }
];
const pwdRules = [
    {
        urlGuard: url => url.indexOf('lurl.cc') != -1,
        cssSelector: 'div.col-sm-12  span.login_span',
        extractRegx: /\d{4}-(\d{2})-(\d{2})/,
        join: (month, year) => `${month}${year}`,
        inputSelector: 'input#password'
    },
    {
        urlGuard: url => url.indexOf('myppt.cc') != -1,
        cssSelector: 'div.col-sm-12  span.login_span',
        extractRegx: /\d{4}-(\d{2})-(\d{2})/,
        join: (month, year) => `${month}${year}`,
        inputSelector: 'input#pasahaicsword'
    },
    {
        urlGuard: url => url.indexOf('mork.ro') != -1,
        cssSelector: '#passwordVerifyForm>h3:first-of-type',
        extractRegx: /\d{4}-(\d{2})-(\d{2})/,
        join: (month, year) => `${month}${year}`,
        inputSelector: 'input#id_pass_key'
    },

];
const videoGuards = [
    {
        urlGuard: (url) => {
            return url.startsWith('https://lurl.cc/')
        },
        videoSelector: '.vjs-tech source',
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },
    {
        urlGuard: (url) => {
            return url.startsWith('https://myppt.cc/')
        },
        videoSelector: '.vjs-tech source',
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },
    {
        urlGuard: (url) => {
            return url.startsWith('https://cat.85xvideo.com')
        },
        videoSelector: '#video source',
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },
    // if match failed, execute default action
    {
        urlGuard: (url) => {
            return true
        },
        videoSelector: 'video source',
        extractFilename: (url) => {
            let pureUrl = url.split('?')[0]
            let splices = pureUrl.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },

]
let oldFetch = unsafeWindow.fetch;
function hookFetch(...args) {
    return oldFetch(...args).then(resp => {
        for (let guard of urlGuards) {
            putMsg("capture the url ", args[0])
            if (guard.guard(args[0])) {
                downloadFromUrl(args[0], guard.extractFilename(args[0]))
            }
            return resp;
        }
    })
}


function hookDrawImage() {
    // 1. 获取 Canvas 2D 上下文的原型
    const ctxPrototype = unsafeWindow.CanvasRenderingContext2D.prototype;

    // 2. 保存原始的 drawImage 方法
    const originalDrawImage = ctxPrototype.drawImage;

    // 3. 创建并应用我们的钩子函数
    ctxPrototype.drawImage = function (...args) {
        let { putMsg } = window.debugPanel
        // 第一个参数就是被绘制的图像源
        const imageSource = args[0];

        // 检查源是否是 HTMLImageElement 并且有 src 属性
        if (imageSource && imageSource instanceof unsafeWindow.HTMLImageElement && imageSource.src) {
            const url = imageSource.src;
            // 运行你的守卫逻辑
            for (const guard of urlGuards) {
                if (guard.guard(url)) {
                    putMsg(`[Canvas 劫持] 捕获到 drawImage URL: ${url}`);
                    downloadFromUrl(url, guard.extractFilename(url));
                    break;
                }
            }
        }
        // 如果源是另一个 Canvas,你也可以处理
        else if (imageSource && imageSource instanceof unsafeWindow.HTMLCanvasElement) {
            console.log("[Canvas 劫持] 正在绘制另一个 Canvas,无法直接获取 URL。");
        }

        // 4. **至关重要**: 调用原始的 drawImage 方法,否则页面无法正常显示
        return originalDrawImage.apply(this, args);
    };
    console.log("Canvas.drawImage 劫持成功!");
}


function extractPwd(...pwdRules) {
    let { putMsg } = window.debugPanel
    for (let pr of pwdRules) {
        if (pr.urlGuard(document.URL)) {
            putMsg("Match the site, and run fiiling pwd")
            waitUtil(pr.inputSelector).then(ele => {
                putMsg("find the input ", ele)
                let updatedDataNode = document.querySelector(pr.cssSelector);
                let updatedData = updatedDataNode.textContent;
                let matches = pr.extractRegx.exec(updatedData)
                putMsg("find the node contains pwd, mathes", updatedData, matches)
                if (matches && matches.length > 2) {
                    putMsg(updatedData)
                    let pwd = pr.join(...matches.slice(1))
                    putMsg(ele, pwd)
                    ele.value = pwd
                }

            }).catch(err => {
                putMsg(`filling pwd failed: ${err}`)
            })
            return
        }
    }
}
function waitUtil(cssSelector) {
    return new Promise((resolve, reject) => {
        const observer = new MutationObserver((mutations, obs) => {
            const ele = document.querySelector(cssSelector);
            if (ele) {
                resolve(ele)
                obs.disconnect()
            }
        })
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true,
        });
        setTimeout(() => {
            observer.disconnect();
            reject(new Error(`等待元素 "${cssSelector}" 超时`));
        }, 20000);
    })
}


function downloadFromUrl(url, filename) {
    const headers = {
        // 关键修复 #1: 添加 Referer 头,告诉服务器我们是从哪个页面来的
        'Referer': window.origin,
        'sec-fetch-site': 'same-site',
        'sec-fetch-mode': 'no-cors',
        'sec-fetch-dest': 'video',
    };
    let updateProgress = showProgress();
    let { putMsg } = window.debugPanel;
    GM.xmlHttpRequest({
        method: 'GET',
        url: url,
        headers: headers,
        responseType: 'blob',
        onprogress: progress => {
            if (progress.lengthComputable) {
                const percent = Math.round((progress.loaded / progress.total) * 100);
                updateProgress(percent / 100)
                putMsg(`[强制模式] 正在获取数据... ${percent}%`);
            }
        },
        onload: response => {
            putMsg("[强制模式]非二进制: ", response)
            downloadFromBlob(response.response, filename)
        },
        onerror: error => {
            if (error.status === 200 && error.response) {
                putMsg(`[强制模式] 获取数据成功 (onerror fallback)`);
                downloadFromBlob(error.response, filename);
            } else {
                putMsg(`[强制模式] 获取数据失败: ${JSON.stringify(error)}`);
                const result = confirm("Whether to retry???")
                if (result) {
                    downloadFromUrl(url, filename)
                }
            }

        }
    });
}

function downloadFromBlob(blob, filname) {
    const blobUrl = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = blobUrl;
    link.download = filname;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(blobUrl);
}
/**
 * 使用闭包创建一个 showProgress 函数.
 * 这样可以拥有一个私有的 nextTopPosition 变量, 用于在多次调用之间保持状态.
 */
const showProgress = (() => {

    // 这个变量存在于闭包中, 不会污染全局作用域, 并且在多次调用之间保持它的值.
    let nextTopPosition = 10; // 第一个进度条的初始top值

    const BAR_HEIGHT = 8; // 进度条的高度
    const PADDING_BETWEEN_BARS = 6; // 进度条之间的垂直间距

    // IIFE返回的这个函数才是我们最终使用的 showProgress 函数
    return function () {
        // --- 这部分是每次调用时实际执行的代码 ---

        const progressContainer = document.createElement('div');
        progressContainer.style.position = 'fixed';
        // 使用闭包中保存的 nextTopPosition 变量
        progressContainer.style.top = `${nextTopPosition}px`;
        progressContainer.style.right = '10px';
        progressContainer.style.width = '250px';
        progressContainer.style.height = `${BAR_HEIGHT}px`;
        progressContainer.style.backgroundColor = '#e0e0e0';
        progressContainer.style.borderRadius = '5px';
        progressContainer.style.overflow = 'hidden';
        progressContainer.style.zIndex = '999999';
        progressContainer.style.transition = 'opacity 0.5s ease-out';

        const progressBar = document.createElement('div');
        progressBar.style.width = '0%';
        progressBar.style.height = '100%';
        progressBar.style.backgroundColor = '#4CAF50';
        progressBar.style.borderRadius = '5px';
        progressBar.style.transition = 'width 0.2s ease-in-out';

        progressContainer.appendChild(progressBar);
        document.body.appendChild(progressContainer);

        // 为下一次调用 showProgress 更新 top 值
        // 这个操作会直接修改闭包中的 nextTopPosition
        nextTopPosition += BAR_HEIGHT + PADDING_BETWEEN_BARS;

        /**
         * 设置进度.
         * @param {number} progress - 0 到 1 之间的小数.
         */
        const setProgress = (progress) => {
            // 注意:你原始代码中的 `initTop += (height + 6)` 被移除了
            // 因为位置应该在创建时就固定, 而不是在更新进度时改变

            const clampedProgress = Math.max(0, Math.min(1, progress));
            progressBar.style.width = `${clampedProgress * 100}%`;

            // 当进度完成时,自动淡出并移除
            if (clampedProgress >= 1) {
                setTimeout(() => {
                    progressContainer.style.opacity = '0';
                    setTimeout(() => {
                        progressContainer.remove();
                    }, 500); // 匹配 transition 的时间
                }, 300); // 延迟一会再消失
            }
        };

        // 返回进度设置函数
        return setProgress;
    };
})();
/**
         * Displays and manages a debug panel in the bottom-left corner of the page.
         * @returns {{putMsg: function(any): void}} An object containing the putMsg method.
         */
function showDebugPanel(on = true) {
    if (!on) {
        let putMsg = (...msg) => {
            console.log(...msg)
        }
        window.debugPanel = { putMsg }
        return {
            putMsg: putMsg
        }
    }
    // Check if the debug panel already exists. If so, return its interface directly.
    if (document.getElementById('debug-panel-container')) {
        return window.debugPanel;
    }

    // 1. Create the HTML structure for the debug panel.
    const panelContainer = document.createElement('div');
    panelContainer.id = 'debug-panel-container';

    const panelHeader = document.createElement('div');
    panelHeader.id = 'debug-panel-header';
    panelHeader.textContent = 'Debug Panel';

    const closeButton = document.createElement('span');
    closeButton.id = 'debug-panel-close';
    closeButton.textContent = '×'; // Close button character

    const panelContent = document.createElement('div');
    panelContent.id = 'debug-panel-content';

    panelHeader.appendChild(closeButton);
    panelContainer.appendChild(panelHeader);
    panelContainer.appendChild(panelContent);

    document.body.appendChild(panelContainer);

    // 2. Add CSS styles for the debug panel.
    const style = document.createElement('style');
    style.textContent = `
                #debug-panel-container {
                    position: fixed;
                    bottom: 15px;
                    left: 15px;
                    width: 400px;
                    max-width: 90%;
                    height: 250px;
                    background-color: rgba(0, 0, 0, 0.75);
                    border-radius: 8px;
                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
                    color: #fff;
                    display: flex;
                    flex-direction: column;
                    font-family: monospace, sans-serif;
                    font-size: 14px;
                    z-index: 9999;
                    transition: opacity 0.3s, transform 0.3s;
                }
                #debug-panel-header {
                    padding: 8px 12px;
                    background-color: rgba(0, 0, 0, 0.4);
                    font-weight: bold;
                    cursor: move; /* Optional: reserve for future drag functionality */
                }
                #debug-panel-close {
                    float: right;
                    cursor: pointer;
                    font-size: 20px;
                    line-height: 1;
                    font-weight: bold;
                }
                #debug-panel-close:hover {
                    color: #ff5555;
                }
                #debug-panel-content {
                    padding: 10px;
                    overflow-y: auto;
                    flex-grow: 1;
                    color: #fff;
                    word-wrap: break-word; /* Ensure long strings can wrap */
                }
                .debug-message {
                    padding-bottom: 5px;
                    border-bottom: 1px solid #444;
                    margin-bottom: 5px;
                    color: #fff;
                }
                .debug-message:last-child {
                    border-bottom: none;
                }
            `;
    document.head.appendChild(style);

    // 3. Implement the core functionality.
    const putMsg = (...msg) => {
        const isScrolledToBottom = panelContent.scrollHeight - panelContent.clientHeight <= panelContent.scrollTop + 1;

        // Format the message content.
        let formattedMsg = msg.map(item => {
            if (typeof item === 'object' && item !== null) {
                return JSON.stringify(item, null, 2);
            } else {
                return item
            }
        }).join(" ")

        const time = new Date().toLocaleTimeString();
        const msgElement = document.createElement('div');
        msgElement.className = 'debug-message';
        msgElement.innerHTML = `<strong>[${time}]</strong><pre style="margin:0; white-space: pre-wrap;color:#fff;">${formattedMsg}</pre>`;

        panelContent.appendChild(msgElement);

        // If it was already scrolled to the bottom, auto-scroll to the new bottom.
        if (isScrolledToBottom) {
            panelContent.scrollTop = panelContent.scrollHeight;
        }
    };

    // Close button functionality.
    closeButton.addEventListener('click', () => {
        panelContainer.style.opacity = '0';
        panelContainer.style.transform = 'scale(0.9)';
        setTimeout(() => panelContainer.style.display = 'none', 300);
    });

    // Expose the interface to the global scope for easy access and to implement the singleton pattern.
    window.debugPanel = { putMsg };
    return window.debugPanel;
}
(function () {
    'use strict';
    // drawImage of canvas
    hookDrawImage()
    // fetch
    unsafeWindow.fetch = hookFetch;
    document.addEventListener('DOMContentLoaded', () => {
        let on = GM_getValue('debug_mode', false);

        // 2. 注册一个菜单命令
        GM_registerMenuCommand('是否debug', () => {
            // 点击菜单时,执行这里的代码

            // a. 切换 on 变量的状态 (true -> false, false -> true)
            on = !on;

            // b. 将新的状态保存到存储中,以便下次打开页面时保持该状态
            GM_setValue('debug_mode', on);

            // c. 给用户一个明确的反馈
            alert(`debug功能已设置为:${on ? '✅ 开启' : '❌ 关闭'}\n\n请刷新页面使设置生效。`);
        });
        let { putMsg } = showDebugPanel(on)
        extractPwd(...pwdRules)
        // watch video 
        for (let guard of videoGuards) {
            if (guard.urlGuard(document.URL)) {
                waitUtil(guard.videoSelector).then(ele => {
                    putMsg("find element", ele)
                    ele && ele.src && downloadFromUrl(ele.src, guard.extractFilename(ele.src))
                }).catch(err => {
                    putMsg(`download video ${err}`)
                })
                putMsg("match the url of the video", document.URL)
                break;
            }
        }
    })
})();