Sleazy Fork is available in English.

mmdfans 一键下载

在网页上检测视频标签并获取标题和下载链接,点击下载按钮会跳转到视频链接下载并尝试返回页面。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         mmdfans 一键下载
// @namespace    http://tampermonkey.net/
// @version      1.0.4
// @description  在网页上检测视频标签并获取标题和下载链接,点击下载按钮会跳转到视频链接下载并尝试返回页面。
// @author       wzj042
// @match        https://mmdfans.net/mmd/*
// @match        https://cdn.mmdlibrary.eu.org/*
// @match        https://cdn.mmdlibrary2.eu.org/*
// @match        https://cdn.mmdlibrary3.eu.org/*
// @match        https://cdn.bakabakaxi.eu.org/*
// @match        https://cdn.baka6.eu.org/*
// @match        https://cdn.baka7.eu.org/*
// @match        https://cdn.baka8.eu.org/*
// @match        https://cdn.baka9.eu.org/*
// @match        https://cirno.baka9.eu.org/*
// @license      MIT
// @grant        none
// ==/UserScript==


(function () {
    'use strict';

    /**
     * 手动添加 CDN 链接时别忘了在上方添加匹配链接 `match {你要添加的链接}/*`
     */
    const CDN_LIST = [
        'https://cdn.mmdlibrary.eu.org',
        'https://cdn.mmdlibrary2.eu.org',
        'https://cdn.mmdlibrary3.eu.org',
        'https://cdn.bakabakaxi.eu.org',
        'https://cdn.baka6.eu.org',
        'https://cdn.baka7.eu.org',
        'https://cdn.baka8.eu.org',
        'https://cdn.baka9.eu.org',
        'https://cirno.baka9.eu.org',
        // 添加你要添加的 CDN 链接
    ]

    /**
     * 自定义文件名的 slot
     * name: slot 名称
     * transform: 转换函数
     * source: 数据来源
    */
    const CUSTOM_FILENAME_SLOT = [
        {
            name: 'postDate',
            transform: (postAt) => {
                const datePart = postAt.split(' ')[0];
                return datePart;
            },
            source: (videoInfo) => videoInfo['postAt']
        },
        {
            name: 'curDate',
            transform: () => {
                return new Date().toISOString().split('T')[0];
            },
            source: () => {}
        }
    ]
    
    let downloadFlag = false; 
    const TIME_OUT = 1000;
    /**
     * 默认文件名格式
     * 使用 {param} 的方式表示参数,例如 {author} 表示作者
     * 具体的参数名点击显示视频参数按钮后会显示
     */
    const DEF_FILENAME = '[iwara]{author}-{title}-{postAt}.mp4';

    const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

    // 加一个下载防抖事件
    const downloadDebounce = (function () {
        let timer = null;
        return function (pathUrl) {
            if (timer) {
                clearTimeout(timer);
            }
            timer = setTimeout(() => {
                download(pathUrl);
            }, 500);
        };
    })();

    /**
     * 检查视频标签是否存在,如果存在则添加下载按钮
     */
    const checkVideoTag = async () => {
        while (true) {
            const videoTag = document.getElementsByClassName('mdui-video-fluid');
            const sourceElement = document.querySelector('video source');
            const downloadLink = sourceElement ? sourceElement.getAttribute('src') : '';
            const titleElement = document.querySelector('h2.title');
            const title = titleElement ? titleElement.textContent.trim() : '无标题';

            if (videoTag.length > 0) {
                const videoInfo = gatherVideoInfo(downloadLink, title);

                createTagContainer();
                createDownloadButton(videoInfo);
                createCopyCdnButton(videoInfo.baseurl);
                createJsonButton(videoInfo);

                createFilenameInput();

                break;
            } else if (!downloadFlag) {
                const curUrl = window.location.href
                if (isCdnLink(curUrl)) {
                    downloadDebounce(curUrl);
                }
            }

            await delay(TIME_OUT); // 等待下一次检查
        }
    };

    /**
     * 检查当前页面是否为 mmdfans 视频的 CDN 链接
     * @param   {string}  url - 当前页面链接 
     * @returns {boolean}     - 如果是 mmdfans 视频的 CDN 链接,则返回 true,否则返回 false
     */
    function isCdnLink(url) {
        return CDN_LIST.some(cdn => url.startsWith(cdn));
    }

    function download(pathUrl) {
        const a = document.createElement("a");
        const url = new URL(pathUrl);
        const downloadURL = url.protocol + '//' + url.host + url.pathname;
        const filename = url.searchParams.get('filename');

        a.download = filename;
        a.href = downloadFlag ? downloadURL : pathUrl;
        a.click();
        a.remove();
        setTimeout(function () {
            /**
             * 下载完成后返回上一页
             */
            if (isCdnLink(window.location.href) && downloadFlag) {
                window.history.back();
            }
            downloadFlag = false;
        }, TIME_OUT);
        
        downloadFlag = true;
    }

    /**
     * 匹配文件名中的参数并替换为对应的值,返回完整的下载链接
     * @param {*} filenameInfo - 文件名信息对象
     * @param {*} videoInfo - 视频信息对象
     * @returns {string} - 下载链接
     */
    function filename2Url(filenameInfo, videoInfo) {
        const params = filenameInfo.params;
        let filename = filenameInfo.filename;
        console.log(filenameInfo, videoInfo);
        // 校验参数
        if (!params || params.length === 0 || !filename) {
            return videoInfo.downloadLink;
        }

        // 匹配 params 和 videoInfo 中的属性
        params.forEach(param => {
            let value = videoInfo[param];

            // 如果是自定义的 slot 则调用 transform 方法
            const customSlot = CUSTOM_FILENAME_SLOT.find(slot => slot.name === param);
            if (customSlot && customSlot.transform) {
                value = customSlot.transform(customSlot.source(videoInfo));
            }

            filename = filename.replace(`{${param}}`, value);
        });
        // 为了防止文件名中含有特殊字符,需要对文件名进行编码
        return `${videoInfo.downloadLink}?filename=${encodeURIComponent(filename)}`;
    }

    function createFilenameInput() {
        const filenameButton = document.createElement('button');
        filenameButton.innerHTML = 'FileName';
        filenameButton.classList.add('mdui-btn');
    
        const filenameInput = document.createElement('input');
        filenameInput.id = 'filename-input';
        filenameInput.placeholder = '请输入文件名格式';
        filenameInput.type = 'text';
        // 如果 localStorage 中有 filename 则使用 localStorage 中的值
        filenameInput.value = localStorage.getItem('filename') || DEF_FILENAME;
        
        filenameInput.style.width = '50%';
        filenameInput.style.display = 'inline-block';

        filenameInput.classList.add('mdui-textfield-input');

        // 编辑完成后保存到 localStorage
        filenameInput.onchange = () => {
            localStorage.setItem('filename', filenameInput.value);
        };
    
        const tagDivider = document.querySelector('.mdui-divider.tag-divider');
        const tagContainer = document.createElement('div');
        tagContainer.classList.add('tag-container');
        
        tagContainer.appendChild(filenameButton);
        tagContainer.appendChild(filenameInput);
        tagDivider.parentNode.insertBefore(tagContainer, tagDivider);

    }

    /**
     * 在标题上方添加一个标签容器,用于存放下载按钮
     */
    function createTagContainer() {
        const tagContainer = document.createElement('div');
        tagContainer.classList.add('tag-container');
        const titleDiv = document.querySelector('.mdui-typo');
        titleDiv.parentNode.insertBefore(tagContainer, titleDiv);
    }

    function createDownloadButton(videoInfo) {
        const downloadButton = document.createElement('button');
        downloadButton.innerHTML = '下载视频';
        downloadButton.classList.add('mdui-btn', 'mdui-color-theme-accent', 'mdui-ripple');

        downloadButton.onclick = () => {

            let baseurl = videoInfo.baseurl;

            if (!isCdnLink(baseurl)) {
                showAlertWithButtons(baseurl);
                return;
            }

            downloadDebounce(filename2Url(gatherFilenameInfo(), videoInfo));
        };

        const tagContainer = document.querySelector('.tag-container');
        tagContainer.appendChild(downloadButton);
        tagContainer.appendChild(createSpace());
    }

    function createCopyCdnButton(downloadLink) {
        const copyCdnButton = document.createElement('button');
        copyCdnButton.innerHTML = '复制 CDN 链接';
        copyCdnButton.classList.add('mdui-btn', 'mdui-color-theme-accent', 'mdui-ripple');

        copyCdnButton.onclick = () => {
            if (!downloadLink) {
                alert('未获取到视频链接');
                return;
            }
            navigator.clipboard.writeText(downloadLink);
            alert('复制成功');
        };

        const tagContainer = document.querySelector('.tag-container');
        tagContainer.appendChild(copyCdnButton);
        tagContainer.appendChild(createSpace());
    }

    /**
     * 创建一个 Html 空格(占位)元素
     * @returns {HTMLElement} - 返回一个空格元素
     */
    function createSpace() {
        const space = document.createElement('span');
        space.innerHTML = '  ';
        return space;
    }

    function createJsonButton(videoInfo) {
        const jsonButton = document.createElement('button');
        jsonButton.innerHTML = '显示视频信息';
        jsonButton.classList.add('mdui-btn', 'mdui-color-theme-accent', 'mdui-ripple');
    
        jsonButton.onclick = () => {
            showPopup(videoInfo);
        };
    
        const tagContainer = document.querySelector('.tag-container');
        tagContainer.appendChild(jsonButton);
        tagContainer.appendChild(createSpace());
    }

    /**
     * 展示视频信息的弹窗
     * @param {*} videoInfo - 视频信息
     */
    function showPopup(videoInfo) {
        const popup = document.createElement('div');
        popup.style.position = 'fixed';
        popup.style.top = '50%';
        popup.style.left = '50%';
        popup.style.transform = 'translate(-50%, -50%)';
        popup.style.padding = '20px';
        popup.style.width = '42%';
        popup.style.backgroundColor = '#fff';
        popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
        popup.style.zIndex = '1000';
        popup.style.overflowX = 'auto';
    
    
        const pre = document.createElement('pre');
        pre.innerHTML = JSON.stringify(videoInfo, null, 2);
        popup.appendChild(pre);

        
        const closeButton = document.createElement('button');
        closeButton.innerHTML = '关闭';
        closeButton.classList.add('mdui-btn', 'mdui-color-theme-accent', 'mdui-ripple');
        closeButton.style.width = '100%';
        closeButton.onclick = () => popup.remove();
        popup.appendChild(closeButton);

        popup.onclick = (e) => {
            if (e.target === popup) {
                popup.remove();
            }
        };


        document.body.appendChild(popup);
    }
    
    /**
     * 展示提示框,提示用户手动添加 CDN 链接
     * @param {*} cdnLink - 视频下载链接
     */
    function showAlertWithButtons(cdnLink) {
        const div = document.createElement('div');
        div.innerHTML = `
            <p>当前 CDN 链接需要手动添加 CDN 链接(脚本主页有教程),否则无法启动下载任务</p>
            <button id="open-homepage" class="mdui-btn mdui-color-theme-accent mdui-ripple">打开脚本主页</button>
            <button id="copy-baseurl" class="mdui-btn mdui-color-theme-accent mdui-ripple">复制视频 CDN 链接</button>
        `;
        const tagContainer = document.querySelector('.tag-container');
        tagContainer.appendChild(div);

        document.getElementById('open-homepage').onclick = () => {
            window.open('https://greasyfork.org/zh-CN/scripts/476557-mmdfans%E4%B8%80%E9%94%AE%E4%B8%8B%E8%BD%BD', '_blank');
        };

        document.getElementById('copy-baseurl').onclick = () => {
            navigator.clipboard.writeText(cdnLink);
            alert('已复制 CDN 链接');
        };
    }

    /**
     * 获取文件名输入框中的参数
     * @returns {*} - 返回文件名参数
     * @example 
     * {
     *   filename: '[iwara]{author}{title}.mp4',
     *   params: ['author', 'title']
     * }
     */
    function gatherFilenameInfo() {
        const filenameInput = document.getElementById('filename-input');
        const filename = filenameInput ? filenameInput.value : '';
        const paramMatches = filename.match(/\{(\w+)\}/g);
        const params = paramMatches ? paramMatches.map(param => param.slice(1, -1)) : [];
    
        return {
            filename: filename,
            params: params
        };
    }

    /**
     * 将视频信息整合到一个对象中
     * @param {string} downloadLink - 视频下载链接
     * @param {string} title - 视频标题
     * @returns {object} 视频信息对象
     */
    function gatherVideoInfo(downloadLink, title) {
        const tagContainers = document.querySelectorAll('.tag-container');
        const info = {
            title: title,
            downloadLink: downloadLink,
            tags: [],
            author: '无作者',
            baseurl: '',
            source: '',
            postAt: '',
            comment: ''
        };

        if (downloadLink) {
            const url = new URL(downloadLink);
            info.baseurl = url.protocol + '//' + url.host;
        }

        tagContainers.forEach(container => {
            const buttons = container.querySelectorAll('button');
            
            buttons.forEach(button => {
                const text = button.textContent.trim();
                switch (text) {
                    case 'Tags':
                    case 'Source':
                    case 'PostAt':
                    case 'Author':
                        break;
                    default:
                        if (container.textContent.includes('Tags')) {
                            info.tags.push(text);
                        } else if (container.textContent.includes('Author')) {
                            info.author = text;
                        } else if (container.textContent.includes('Source')) {
                            info.source = container.querySelector('a').href;
                        } else if (container.textContent.includes('PostAt')) {
                            info.postAt = text;
                        }
                        break;
                }
            });
            if (container.textContent.includes('Comment')) {
                info.comment = document.querySelector('blockquote').textContent.trim();
            }
        });

        return info;
    }

    checkVideoTag();
})();