// ==UserScript== // @name 中少快乐阅读平台中少报刊资源下载器 // @namespace http://tampermonkey.net/ // @version 0.2.4 // @description 在幼儿画报期刊列表页为每一期添加“下载”按钮,自动下载 XML 中的高分辨率图像资源(href2)并打包为 ZIP 文件 // @author 野原新之布 // @license GPL-3.0-only // @match http://202.96.31.36:8888/reading/onemagazine/* // @grant GM_xmlhttpRequest // @grant GM_download // @note 2025.05.09-v0.2.4 修复嘟嘟熊画报不能正常下载的bug // @note 2025.05.09-v0.2.3 完善下载按钮的样式 // @note 2025.05.09-v0.2.2 修复被Chrome阻止下载的bug // @note 2025.05.09-v0.2.1 修复无法下载报纸资源的bug // @note 2025.05.09-v0.2 完成在期刊展示列表中添加下载按钮进行下载 // @note 2025.05.08-v0.1 完成阅读刊物时自动下载资源 // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 引入 JSZip 库 const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js'; document.head.appendChild(script); // 提取URL前缀 const origin = window.location.origin; // 查找所有的期刊项 const items = document.querySelectorAll('li.col-md-3.col-sm-3.col-xs-6'); items.forEach(item => { // 提取期号信息 const imgElement = item.querySelector('img'); const hrefElement = item.querySelector('a'); if (imgElement && hrefElement) { // 从图片的 src 获取期号(假设期号在图片 URL 中) const imgSrc = imgElement.src; // 正则提取 qikan/youerhb/2021/12/929/,其中月份是2~4位数字 const match = imgSrc.match( /\/fliphtml5\/password\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(\d{4})\/(\d{2,4})\/(\d+)\/web\/[^\/]+_opf_files\/([^\/]+)_cover_\.jpg$/ ); if (match) { // 路径的第三部分,如main、other const channel = match[1]; // 提取期刊或报纸类别,如“qikan”或“baozhi” const categoryPrefix = match[2]; // 提取具体分类(例如“wmakx”或“youerhb”) const subCategory = match[3]; // 获取年份 const year = match[4]; // 获取月份 const month = match[5]; // 获取编号部分 const number = match[6]; // 获取 xmlFileName,不包括 "_cover_.jpg" const xmlFileName = match[7] + '.xml'; // 获取详情页的 URL const publicationUrl = origin + hrefElement.getAttribute('href'); // 获取 publicationTitle fetch(publicationUrl) .then(response => response.text()) .then(pageContent => { // 从页面中提取 标签内容 const titleMatch = pageContent.match(/<title>(.*?)<\/title>/); const publicationTitle = titleMatch ? titleMatch[1] : '未找到标题'; const xmlUrl = `${origin}/fliphtml5/password/${channel}/${categoryPrefix}/${subCategory}/${year}/${month}/${number}/web/html5/tablet/${xmlFileName}`; // 创建下载按钮 const downloadButton = document.createElement('button'); downloadButton.textContent = '下载'; downloadButton.classList.add('btn', 'btn-primary'); downloadButton.style.marginTop = '10px'; downloadButton.style.position = 'relative'; downloadButton.style.padding = '10px 20px'; downloadButton.style.width = '100%'; // 初始宽度为100% downloadButton.style.border = '2px solid #007bff'; // 保持边框 downloadButton.style.backgroundColor = '#007bff'; // 设置按钮背景色 // 创建进度条的内层 div,初始时不显示进度条 const progressBarContainer = document.createElement('div'); progressBarContainer.style.position = 'absolute'; progressBarContainer.style.top = '0'; progressBarContainer.style.left = '0'; progressBarContainer.style.width = '100%'; progressBarContainer.style.height = '100%'; progressBarContainer.style.backgroundColor = '#28a745'; progressBarContainer.style.borderRadius = '5px'; progressBarContainer.style.transition = 'width 0.3s'; // 平滑变化 progressBarContainer.style.width = '0%'; // 初始进度为0 progressBarContainer.style.display = 'none'; // 初始时隐藏进度条 // 将进度条嵌入到按钮内部 downloadButton.appendChild(progressBarContainer); // 为按钮添加点击事件,触发下载并读取 XML 内容 downloadButton.addEventListener('click', () => { // 显示进度条并立即开始进度更新 progressBarContainer.style.display = 'block'; progressBarContainer.style.width = '0%'; // 使用 fetch 直接加载 XML 文件 fetch(xmlUrl) .then(response => { if (!response.ok) { throw new Error('XML 加载失败'); } return response.text(); // 获取文件的文本内容 }) .then(xmlContent => { // 解析 XML 内容 const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, 'application/xml'); // 获取 manifest 下的所有 item const itemNodes = Array.from(xmlDoc.getElementsByTagName('item')); if (itemNodes.length === 0) { alert('XML 中没有找到 <item> 元素'); return; } const zip = new JSZip(); let completed = 0; const baseUrl = xmlUrl.replace(/[^/]+\.xml$/, ''); // 下载每个 item 中的资源并打包到 ZIP itemNodes.forEach((item, index) => { const href2 = item.getAttribute('href2'); const href = item.getAttribute('href'); const filePath = href2 || href; if (!filePath) return; const fileUrl = baseUrl + filePath; const fileName = filePath.split('/').pop(); fetch(fileUrl) .then(res => res.arrayBuffer()) .then(data => { zip.file(fileName, data); completed++; const percent = Math.round((completed / itemNodes.length) * 100); // 更新进度条宽度 progressBarContainer.style.width = `${percent}%`; // 所有文件下载完成后生成 ZIP 文件并触发下载 if (completed === itemNodes.length) { zip.generateAsync({ type: 'blob' }).then(blob => { const zipUrl = URL.createObjectURL(blob); GM_download({ url: zipUrl, name: `${publicationTitle}.zip`.replace(/[\\/:*?"<>|]/g, '_'), onload: () => console.log('ZIP 下载完成'), onerror: err => console.error('下载失败:', err) }); }); } }) .catch(err => console.error(`下载失败: ${fileUrl}`, err)); }); }) .catch(err => { alert('加载 XML 文件失败'); console.error(err); }); }); // 将下载按钮添加到期刊项中 item.appendChild(downloadButton); }) .catch(err => console.error('获取刊物标题失败:', err)); } } }); })();