Greasy Fork

Greasy Fork is available in English.

中少快乐阅读平台中少报刊资源下载器

在幼儿画报期刊列表页为每一期添加“下载”按钮,自动下载 XML 中的高分辨率图像资源(href2)并打包为 ZIP 文件

当前为 2025-05-09 提交的版本,查看 最新版本

// ==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 完成阅读刊物时自动下载资源
// ==/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 => {
                        // 从页面中提取 <title> 标签内容
                        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));
            }
        }
    });
})();