Greasy Fork

来自缓存

Greasy Fork is available in English.

云展网(yunzhan365) 全书图片 PDF 下载器 (v2.2 - 动态文件名)

自动翻页提取全书 "高清" 图片, 严格按页码排序, 并尝试合并为 PDF。如果失败,则回退显示链接。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         云展网(yunzhan365) 全书图片 PDF 下载器 (v2.2 - 动态文件名)
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  自动翻页提取全书 "高清" 图片, 严格按页码排序, 并尝试合并为 PDF。如果失败,则回退显示链接。
// @author       (Your Name)
// @match        *://book.yunzhan365.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// @run-at       document-idle
// @license GPLv3
// ==/UserScript==

(function() {
    'use strict';

    // --- [1. 配置项] ---

    // [重要修正] 您设置的 "15" (15毫秒) 太快了。
    // 必须设置足够长的时间 (1.5 秒) 来等待页面响应和图片加载。
    const CLICK_INTERVAL_MS = 0.01; // 1.5 秒

    const NEXT_PAGE_SELECTOR = 'div.button[aria-label="下一页"]';
    const IMAGE_SELECTOR = 'div.side-image img[src*="/files/large/"]';
    const THUMBNAIL_SELECTOR = '.thumbnail_win10 .item.button[aria-label^="page "]';

    // --- [2. 状态变量 (不变)] ---
    let clickIntervalId = null;
    let isRunning = false;
    let controlButton = null;
    let pageUrlMap = new Map();
    let totalPageCount = 0;

    // --- [3. 核心功能函数] ---

    /**
     * @brief [新函数] 清理文件名中的非法字符
     */
    function sanitizeFilename(name) {
        if (!name) return 'downloaded_book';
        // 替换所有在 Windows/Linux/Mac 中不安全的文件名字符
        let illegalChars = /[\\/:*?"<>|]/g;
        let safeName = name.replace(illegalChars, "_"); // 替换为下划线
        return safeName.trim() || 'downloaded_book'; // 确保不是空字符串
    }

    function getTotalPageCount() {
        // ... (此函数与 V2 脚本完全相同) ...
        const allPageThumbs = document.querySelectorAll(THUMBNAIL_SELECTOR);
        if (allPageThumbs.length === 0) {
            const swiperItems = document.querySelectorAll('.thumbnail_win10 .swiper .item_focus');
            if (swiperItems.length === 0) return 0;
            const lastSwiperItem = swiperItems[swiperItems.length - 1];
            const lastPageButtons = lastSwiperItem.querySelectorAll('.item.button[aria-label^="page "]');
            if (lastPageButtons.length === 0) return 0;
            const lastButton = lastPageButtons[lastPageButtons.length - 1];
            const label = lastButton.getAttribute('aria-label');
            if (label) return parseInt(label.replace('page ', ''), 10);
            return 0;
        }
        const lastThumb = allPageThumbs[allPageThumbs.length - 1];
        const label = lastThumb.getAttribute('aria-label');
        if (label) return parseInt(label.replace('page ', ''), 10);
        return 0;
    }

    /**
     * @brief [高清 URL 修复] (与 V2.1 相同)
     */
    function extractCurrentImages() {
        const images = document.querySelectorAll(IMAGE_SELECTOR);
        images.forEach(img => {
            const src = img.src;
            if (!src || src.startsWith('data:')) return;
            const pageElement = img.closest('div[id^="page"]');
            if (pageElement && pageElement.id) {
                const pageNumMatch = pageElement.id.match(/^page(\d+)$/);
                if (pageNumMatch && pageNumMatch[1]) {
                    const pageNum = parseInt(pageNumMatch[1], 10);
                    if (!pageUrlMap.has(pageNum)) {
                        const hdSrc = src.split('?')[0]; // [关键] 获取高清 URL
                        console.log(`油猴脚本: 发现新图片 P${pageNum} (已提取高清 URL)`);
                        pageUrlMap.set(pageNum, hdSrc);
                    }
                }
            }
        });
    }

    function clickNextPage() {
        // ... (此函数与 V2 脚本完全相同) ...
        const nextPageButton = document.querySelector(NEXT_PAGE_SELECTOR);
        if (nextPageButton) {
            nextPageButton.click();
            return true;
        }
        return false;
    }

    function fallbackToShowUrls(sortedUrls) {
        // ... (此函数与 V2 脚本完全相同) ...
        let outputArea = document.getElementById('yz-helper-output');
        if (!outputArea) {
            outputArea = document.createElement('textarea');
            outputArea.id = 'yz-helper-output';
            outputArea.readOnly = true;
            document.body.appendChild(outputArea);
        }
        outputArea.value = sortedUrls.join('\n');
        outputArea.style.display = 'block';
        outputArea.select();
    }

    /**
     * @brief [核心] 尝试从已排序的 URL 数组生成 PDF
     */
    function generatePdfFromUrls(sortedUrls) {
        // ... (检查 sortedUrls 是否为空) ...
        if (!sortedUrls || sortedUrls.length === 0) {
            alert("未提取到任何图片。");
            stopScraping();
            return;
        }

        const { jsPDF } = window.jspdf;
        if (controlButton) {
            controlButton.innerText = '■ PDF 生成中 (0%)...';
            controlButton.disabled = true;
        }

        const loadImage = (url) => {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.crossOrigin = "anonymous";
                img.onload = () => resolve(img);
                img.onerror = () => reject(new Error(`加载图片失败 (可能是CORS): ${url}`));
                img.src = url;
            });
        };

        const imagePromises = sortedUrls.map(loadImage);

        Promise.all(imagePromises)
            .then(images => {
                if (controlButton) controlButton.innerText = '■ 正在拼接 PDF...';
                const firstImg = images[0];
                const w = firstImg.naturalWidth;
                const h = firstImg.naturalHeight;
                const doc = new jsPDF('p', 'px', [w, h]);

                images.forEach((img, index) => {
                    if (index > 0) doc.addPage([w, h], 'p');
                    doc.addImage(img, 'WEBP', 0, 0, w, h);
                    if (controlButton) {
                        const percent = Math.round(((index + 1) / images.length) * 100);
                        controlButton.innerText = `■ PDF 生成中 (${percent}%)...`;
                    }
                });

                // --- [!!!文件名修改!!!] ---
                // 1. 获取页面标题
                const pageTitle = document.title;
                // 2. 清理标题中的非法字符
                const safeFilename = sanitizeFilename(pageTitle);

                // 3. 使用清理后的标题保存
                doc.save(safeFilename + '.pdf');
                // --- [修改结束] ---

                alert("PDF 生成完毕,已开始下载!");
                stopScraping();
            })
            .catch(error => {
                // *** [失败] (CORS) ***
                console.error("PDF 生成失败:", error);
                alert(
                    "❌ PDF 生成失败!(错误: " + error.message + ")\n\n" +
                    "这【极有可能】是由于 CORS 安全策略导致的。\n\n" +
                    "脚本将回退到显示【高清且已排序】的图片链接,请手动复制。"
                );
                fallbackToShowUrls(sortedUrls);
                stopScraping();
            });
    }

    /**
     * @brief 循环检查函数
     */
    function checkAndClickNext() {
        // ... (此函数与 V2 脚本完全相同) ...
        extractCurrentImages();
        const collectedCount = pageUrlMap.size;
        if (controlButton) {
            controlButton.innerText = `■ 提取中 (${collectedCount} / ${totalPageCount})...`;
        }
        if (collectedCount >= totalPageCount) {
            console.log(`油猴脚本: 提取完成! 提取到 ${collectedCount} 张图片。`);
            stopScraping();
            const sortedEntries = Array.from(pageUrlMap.entries());
            sortedEntries.sort((a, b) => a[0] - b[0]);
            const finalSortedUrls = sortedEntries.map(entry => entry[1]);
            generatePdfFromUrls(finalSortedUrls);
        } else {
            if (!clickNextPage()) {
                console.log("油猴脚本: '下一页' 按钮消失,自动停止。");
                stopScraping();
                const sortedEntries = Array.from(pageUrlMap.entries()).sort((a, b) => a[0] - b[0]);
                const finalSortedUrls = sortedEntries.map(entry => entry[1]);
                generatePdfFromUrls(finalSortedUrls);
            }
        }
    }

    // --- [5. "启动" / "停止" 功能] ---
    function startScraping() {
        // ... (此函数与 V2 脚本完全相同) ...
        if (isRunning) return;
        totalPageCount = getTotalPageCount();
        if (totalPageCount === 0) {
            alert("油猴脚本: 无法获取总页数!\n\n[重要] 请先手动点击一次底部的 '缩略图' 按钮 (让缩略图列表加载出来),然后再点击本按钮。");
            return;
        }
        isRunning = true;
        pageUrlMap.clear();
        console.log(`油猴脚本: 开始提取。目标页数: ${totalPageCount}`);
        if (controlButton) {
            controlButton.innerText = `■ 提取中... (0 / ${totalPageCount})`;
            controlButton.style.backgroundColor = '#d9534f';
        }
        checkAndClickNext();
        clickIntervalId = setInterval(checkAndClickNext, CLICK_INTERVAL_MS);
    }

    function stopScraping() {
        // ... (此函数与 V2 脚本完全相同) ...
        if (!isRunning) return;
        isRunning = false;
        if (clickIntervalId) {
            clearInterval(clickIntervalId);
            clickIntervalId = null;
        }
        console.log("油猴脚本: 提取已停止。");
        if (controlButton) {
            controlButton.innerText = '▶ 自动提取 PDF (v2.2)'; // [修改] 更新按钮文本
            controlButton.style.backgroundColor = '#0275d8';
            controlButton.disabled = false;
        }
    }

    function toggleScraping() {
        // ... (此函数与 V2 脚本完全相同) ...
        if (isRunning) {
            stopScraping();
            const sortedEntries = Array.from(pageUrlMap.entries()).sort((a, b) => a[0] - b[0]);
            const finalSortedUrls = sortedEntries.map(entry => entry[1]);
            generatePdfFromUrls(finalSortedUrls);
        } else {
            startScraping();
        }
    }

    // --- [6. 注入"开关"按钮到页面] ---

    window.addEventListener('load', () => {
        controlButton = document.createElement('button');
        controlButton.id = 'gm-image-scraper-pdf';
        controlButton.innerText = '▶ 自动提取 PDF (v2.2)'; // [修改] 更新按钮文本
        document.body.appendChild(controlButton);
        controlButton.addEventListener('click', toggleScraping);

        // --- 样式 (与 V2 脚本完全相同) ---
        GM_addStyle(`
            #gm-image-scraper-pdf {
                position: fixed; bottom: 10px; right: 10px; z-index: 99999;
                background-color: #0275d8; color: white; border: none;
                padding: 10px 15px; border-radius: 5px; cursor: pointer;
                font-size: 14px; font-weight: bold;
                box-shadow: 0 4px 8px rgba(0,0,0,0.3);
                transition: background-color 0.2s;
            }
            #gm-image-scraper-pdf:hover { filter: brightness(1.1); }
            #gm-image-scraper-pdf:disabled { background-color: #5bc0de; cursor: wait; }
            #yz-helper-output {
                position: fixed; bottom: 60px; right: 10px; z-index: 99998;
                width: 400px; height: 300px; border: 2px solid #d9534f;
                border-radius: 5px; display: none; font-size: 12px;
                line-height: 1.5; background: #f8f8f8; color: #333;
                padding: 5px; resize: none;
            }
        `);

        console.log("油猴脚本: 全书图片 PDF 下载器 (v2.2 - 动态文件名) 已注入。");
    });

})();