Greasy Fork 支持简体中文。

自动下载模型站预览与简介

Automatically download images and description of some model site. 需要在油猴脚本中设置-通用开启高级模式,然后在下载 BETA中下载模式选择浏览器api,然后增加json进白名单(或者不想下json也可以不设置)。支持模之屋、bowlroll、booth。

// ==UserScript==
// @name         自动下载模型站预览与简介
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically download images and description of some model site. 需要在油猴脚本中设置-通用开启高级模式,然后在下载 BETA中下载模式选择浏览器api,然后增加json进白名单(或者不想下json也可以不设置)。支持模之屋、bowlroll、booth。
// @author       SMatsuri
// @match        *.aplaybox.com/details/model/*
// @match        *bowlroll.net/file/*
// @match        *booth.pm/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 创建下载按钮
    const button = document.createElement('button');
    button.innerText = '下载图片和简介';
    button.id = 'downloadBtn';

    // 添加样式,使按钮位于页面右下角
    const style = document.createElement('style');
    style.innerHTML = `
        #downloadBtn {
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            z-index: 10000;
        }
        #downloadBtn:hover {
            background-color: #45a049;
        }
    `;
    document.head.appendChild(style);
    document.body.appendChild(button);

    // 当前网站类型
    const currentSite = window.location.hostname.includes('aplaybox.com') ? 'aplaybox' :
                        window.location.hostname.includes('bowlroll.net') ? 'bowlroll' : 
                        window.location.hostname.includes('booth.pm') ? 'booth' : 'unknown';

    // 保存工作详情
    let Details = null;

    // 提取 URL 中的数字 ID,例如 https://bowlroll.net/file/326016 提取为 326016
    function extractIdFromUrl(url) {
        const match = url.match(/\/file\/(\d+)/); // 匹配 URL 中的文件 ID
        return match ? match[1] : 'No_ID'; // 如果匹配成功,则返回文件 ID,否则返回 "No_ID"
    }

    let currentId = null;

    // 获取当前页面的数字 ID
    if (currentSite === 'bowlroll' || currentSite === 'booth'){
        currentId = extractIdFromUrl(window.location.href);
    }

    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this.addEventListener('load', function() {
            if (currentSite === 'aplaybox') {
                if (url.includes('getWorkDetails')) { // 检查是否是 getWorkDetails 请求
                    try {
                        const response = JSON.parse(this.responseText); // 解析 JSON 响应
                        if (response.status === "success" && response.code === 200 && response.data && response.data.result && response.data.result.data) {
                            Details = response.data.result.data; // 存储工作详情
                            console.log('Aplaybox Work details captured:', Details);
                        }
                    } catch (e) {
                        console.error('解析 Aplaybox 响应失败:', e);
                    }
                }
            }
        });
        originalOpen.apply(this, arguments);
    };

    // 按钮点击事件处理
    button.addEventListener('click', function() {
        if (currentSite === 'aplaybox') {
            if (Details) {
                handleAplaybox(Details);
            } else {
                alert('工作详情尚未加载,或请求尚未捕获。请稍后再试。');
            }
        } else if (currentSite === 'bowlroll') {
            handleBowlroll();
        } else if (currentSite === 'booth') {
            handleBooth();
        } else {
            alert('当前网站不受支持。');
        }
    });

    // 处理 Aplaybox 的下载
    function handleAplaybox(data) {
        const workName = sanitizeFileName(data.work_name || "Unnamed_Work");
        const introduction = 'Aplaybox\n\n' + decodeUnicode(data.introduction).replace(/\n/g, '\r\n');
        const coverUrl = data.cover;
        const previewImages = data.preview_images;

        // Step 1: 下载 JSON 文件
        const jsonData = JSON.stringify(data, null, 2);
        downloadFile(workName + '/metadata.json', jsonData, 'application/json');

        // Step 2: 下载简介
        downloadFile(workName + '/readme.txt', introduction, 'text/plain');

        // 创建一个 Set 用于存储已下载的图片 URL
        const downloadedImages = new Set();

        // Step 3: 下载封面图片
        downloadImageAplaybox(coverUrl, workName + '/preview0.jpg');
        downloadedImages.add(coverUrl); // 添加到已下载集合中

        // Step 4: 下载预览图片
        previewImages.forEach((imgUrl, index) => {
            // 检查图片 URL 是否已经下载过,避免重复下载
            if (imgUrl && !downloadedImages.has(imgUrl)) {
                downloadedImages.add(imgUrl); // 添加到已下载集合中
                downloadImageAplaybox(imgUrl, `${workName}/preview${index + 1}.jpg`);
            }

        });
    }

    // 处理 BowlRoll 的下载
    function handleBowlroll() {
        // 提取 meta 标签中的信息
        const title = document.querySelector('meta[property="og:title"]')?.getAttribute('content') || 'No Title';
        const description = document.querySelector('meta[name="description"]')?.getAttribute('content') ||
                            document.querySelector('meta[property="og:description"]')?.getAttribute('content') || 'No Description';
        const imageUrl = document.querySelector('meta[property="og:image"]')?.getAttribute('content') || '';

        const workName = sanitizeFileName(title);

        // 下载并合并文件
        downloadBowlrolldata(workName, description);

        downloadImage(imageUrl, workName + '/preview0.jpg');
    }

    // 使用 GM_xmlhttpRequest 代替 GM_download 来获取 JSON/二进制内容并处理
    function downloadBowlrolldata(workName, introduction) {
        // 获取 message 内容
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://bowlroll.net/api/file/' + currentId + '/message',
            responseType: 'json', // 我们假设 API 返回的是 JSON 格式
            onload: function(response) {
                if (response.status === 200) {
                    const messageData = response.response; // 获取 message 数据
                    const messageText = decodeUnicode(messageData.message); // 解码 message 字段

                    // 继续获取 tags 内容
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: 'https://bowlroll.net/api/file/' + currentId + '/tags',
                        responseType: 'json', // 我们假设 API 返回的是 JSON 格式
                        onload: function(response) {
                            if (response.status === 200) {
                                const tagsData = response.response.tags; // 获取 tags 数组
                                const tagsText = extractTags(tagsData); // 提取并解码 tags 的 name 字段

                                // 合并 messageText、tagsText 和 introduction
                                const combinedText = `Bowlroll\n\n${introduction}\n\n---\n\nMessage:\n${messageText}\n\nTags:\n${tagsText}`;

                                // 下载合并后的文件
                                downloadFile(workName + '/readme.txt', combinedText, 'text/plain');
                            } else {
                                console.error('下载 tags 时出错,状态码:', response.status);
                            }
                        },
                        onerror: function(err) {
                            console.error('下载 tags 时发生错误:', err);
                        }
                    });
                } else {
                    console.error('下载 message 时出错,状态码:', response.status);
                }
            },
            onerror: function(err) {
                console.error('下载 message 时发生错误:', err);
            }
        });
    }

    function handleBooth() {
        // 提取标题作为工作名称
        const title = document.querySelector('meta[property="og:title"]')?.getAttribute('content') || 'No Title';
        const workName = sanitizeFileName(title); // 清理标题作为文件夹名
    
        // 提取完整的简介内容,包括所有 <p> 和 <section> 内的文本
        const descriptionElement = document.querySelector('.js-market-item-detail-description');
        let introduction = 'Booth\n\n';

        if (descriptionElement) {
            // 遍历包含介绍内容的 <p>、<section> 及其他相关元素,提取其文本
            const descriptionParts = descriptionElement.querySelectorAll('p, section');
            descriptionParts.forEach(part => {
                introduction += part.innerText.trim() + '\n\n'; // 每个部分用换行符分隔
            });
        } else {
            introduction = 'No Description Available';
        }

        const descriptionElement2 = document.querySelector('.my-40');

        if (descriptionElement) {
            // 遍历包含介绍内容的 <p>、<section> 及其他相关元素,提取其文本
            const descriptionParts = descriptionElement2.querySelectorAll('p, section');
            descriptionParts.forEach(part => {
                introduction += part.innerText.trim() + '\n\n'; // 每个部分用换行符分隔
            });
        } 
    
        // 下载简介
        downloadFile(workName + '/readme.txt', introduction, 'text/plain');

        GM_download({
            url: window.location.href + '.json',
            name: workName + '/metadata.json',
            onerror: function(err) {
                console.error('下载文件时出错:', err);
            },
            ontimeout: function() {
                console.error('下载文件超时');
            },
            onprogress: function(progress) {
                console.log('下载进度:', progress);
            }
        });
    
        // 创建一个 Set 用于存储已下载的图片 URL
        const downloadedImages = new Set();

        // 提取图片链接并下载
        const images = document.querySelectorAll('.market-item-detail-item-image'); // 获取所有图片元素
        images.forEach((imgElement, index) => {
            const imageUrl = imgElement.getAttribute('data-origin'); // 获取原始图片链接

            // 检查图片 URL 是否已经下载过,避免重复下载
            if (imageUrl && !downloadedImages.has(imageUrl)) {
                downloadedImages.add(imageUrl); // 添加到已下载集合中
                downloadImage(imageUrl, `${workName}/preview${index + 1}.jpg`); // 下载图片
            }
        });
    }

    // 提取并解码 tags 中的 name 字段
    function extractTags(tags) {
        return tags.map(tag => decodeUnicode(tag.name)).join(', ');
    }

    // 解码 Unicode 字符
    function decodeUnicode(str) {
        return unescape(str.replace(/\\u/g, '%u'));
    }

    // 函数:清理文件名中的非法字符
    function sanitizeFileName(fileName) {
        // 移除不允许出现在文件名中的字符,替换为下划线
        return fileName.replace(/[\/\\?%*:|"<>。. ]/g, '_');
    }

    // 使用 GM_download 下载文件
    function downloadFile(filename, content, type) {
        const blob = new Blob([content], { type });
        const url = URL.createObjectURL(blob);

        // 使用 GM_download 进行下载
        GM_download({
            url: url,
            name: filename,
            onerror: function(err) {
                console.error('下载文件时出错:', err);
                console.error(`文件名: ${filename}`);
                console.error(`URL: ${url}`);
            },
            ontimeout: function() {
                console.error('下载文件超时');
            },
            onprogress: function(progress) {
                console.log('下载进度:', progress);
            }
        });
    }

    // 下载图片的函数
    function downloadImage(url, filename) {
        GM_download({
            url: url,
            name: filename,
            onerror: function(err) {
                console.error('下载图片时出错:', err);
            },
            ontimeout: function() {
                console.error('下载图片超时');
            },
            onprogress: function(progress) {
                console.log('下载进度:', progress);
            }
        });
    }

    function downloadImageAplaybox(url, filename) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob', // 下载图片作为二进制数据
            headers: {
                // 添加 Cookie,如果需要的话
                'Cookie': document.cookie, // 自动携带当前页面的所有 Cookies
                'User-Agent': navigator.userAgent, // 模拟普通浏览器请求
                'Referer': window.location.href, // 设置 Referer
            },
            onload: function(response) {
                if (response.status === 200) {
                    // 创建 Blob 对象并下载
                    const blob = response.response;
                    const blobUrl = URL.createObjectURL(blob);
                    GM_download({
                        url: blobUrl,
                        name: filename,
                        saveAs: false,
                        onload: function() {
                            console.log('图片下载成功:', filename);
                        },
                        onerror: function(err) {
                            console.error('下载图片时出错:', err);
                        }
                    });
                } else {
                    console.error('服务器返回错误状态码:', response.status);
                }
            },
            onerror: function(err) {
                console.error('下载图片时发生错误:', err);
            }
        });
    }

})();