dowload from lurl&myppt automatically

It will download the media from lurl and myppt automatically

As of 2025-07-17. See the latest version.

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         dowload from  lurl&myppt automatically 
// @name:zh-CN   从lurl&myppt自动下载
// @name:zh-TW   從lurl&myppt自動下載
// @namespace    org.jw23.dcardtools
// @version      0.1.6
// @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://myppt.cc/*
// @match        https://risu.io/*
// @run-at       document-start
// @grant        GM_download
// @grant        GM.xmlHttpRequest
// @grant        unsafeWindow
// @connect      lurl.cc
// @connect      *.lurl.cc
// @connect      risu.io
// @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
        }
    },

];
const pwdRules = [
    {
        urlGuad: 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'
    },
    {
        urlGuad: 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'
    },

];
const videoGuards = [
    {
        urlGuard: (url) => {
            return url.startsWith('https://lurl.cc/') !== -1
        },
        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/') !== -1
        },
        videoSelector: '.vjs-tech source',
        extractFilename: (url) => {
            let splices = url.split('/')
            let [last] = splices.slice(-1)
            return last
        }
    },

]
let oldFetch = unsafeWindow.fetch;
function hookFetch(...args) {
    oldFetch(...args).then(resp => {
        for (let guard of urlGuards) {
            console.log("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) {
        // 第一个参数就是被绘制的图像源
        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)) {
                    console.log(`[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) {
    for (let pr of pwdRules) {
        if (pr.urlGuad(document.URL)) {
            console.log("Match the site, and run fiiling pwd")
            waitUtil(pr.inputSelector).then(ele => {
                console.log("find the input ", ele)
                let updatedDataNode = document.querySelector(pr.cssSelector);
                let updatedData = updatedDataNode.textContent;
                let matches = pr.extractRegx.exec(updatedData)
                console.log("find the node contains pwd, mathes", updatedData, matches)
                if (matches && matches.length > 2) {
                    console.log(updatedData)
                    let pwd = pr.join(...matches.slice(1))
                    console.log(ele, pwd)
                    ele.value = pwd
                }

            }).catch(err => {
                console.log("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.body, {
            childList: true,
            subtree: true,
        });
    })
}


function downloadFromUrl(url, filename) {
    const headers = {
        // 关键修复 #1: 添加 Referer 头,告诉服务器我们是从哪个页面来的
        'Referer': window.origin + '/',

        // 关键修复 #2: 模仿浏览器的 Accept 头,让自己看起来更像一个真正的浏览器
        'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',

        // 为了保险起见,可以把 User-Agent 也明确加上,尽管它看起来是自动继承的
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
        '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 => {
            downloadFromBlob(response.response, filename)
        },
        onerror: error => {
            putMsg(`[强制模式] 获取数据失败:${JSON.stringify(error)}`)

        }
    });
}

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;
        if (typeof msg === 'object' && msg !== null) {
            formattedMsg = JSON.stringify(msg, null, 2);
        }

        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', () => {
        showDebugPanel(false)
        extractPwd(...pwdRules)
        // watch video 
        for (let guard of videoGuards) {
            if (guard.urlGuard(document.URL)) {
                waitUtil(guard.videoSelector).then(ele => {
                    console.log("find element ", ele)
                    ele && ele.src && downloadFromUrl(ele.src, guard.extractFilename(ele.src))
                }).catch(err => {
                    putMsg(`download video ${err}`)
                })
                console.log("match the url of the video", document.URL)
                break;
            }
        }
    })
})();