自动翻页提取全书 "高清" 图片, 严格按页码排序, 并尝试合并为 PDF。如果失败,则回退显示链接。
// ==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 - 动态文件名) 已注入。");
});
})();