dowload from lurl&myppt automatically

It will download the media from lurl and myppt automatically

اعتبارا من 17-07-2025. شاهد أحدث إصدار.

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

];
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
        }
    }
]
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'
    };
    let updateProgress = showProgress();
    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)
                console.log(`[强制模式] 正在获取数据... ${percent}%`);
            }
        },
        onload: response => {
            downloadFromBlob(response.response, filename)
        },
        onerror: error => console.error(`[强制模式] 获取数据失败:`, 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;
    };
})();
(function () {
    'use strict';
    // drawImage of canvas
    hookDrawImage()
    // fetch
    unsafeWindow.fetch = hookFetch;
    document.addEventListener('DOMContentLoaded', () => {
        extractPwd(...pwdRules)

        // watch video 
        videoGuards.forEach(guard => {
            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))
                })
            }
        })
    })
})();