dowload from lurl&myppt automatically

It will download the media from lurl and myppt automatically

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

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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         dowload from  lurl&myppt automatically 
// @name:zh-CN   从lurl&myppt自动下载
// @name:zh-TW   從lurl&myppt自動下載
// @namespace    org.jw23.dcardtools
// @version      0.1.44
// @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])) {
                window.dispatchEvent(new CustomEvent('download_media', {
                    detail: {
                        url: args[0],
                        filename: 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}`);
                    window.dispatchEvent(new CustomEvent('download_media', {
                        detail: {
                            url: url,
                            filename: 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';
    window.addEventListener('download_media', event => {
        let { url, filename } = event.detail
        downloadFromUrl(url, filename)
    })
    // 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 && window.dispatchEvent(new CustomEvent('download_media', {
                        detail: {
                            url: ele.src,
                            filename: guard.extractFilename(ele.src)
                        }
                    }))
                }).catch(err => {
                    putMsg(`download video ${err}`)
                })
                putMsg("match the url of the video", document.URL)
                break;
            }
        }
    })
})();