Greasy Fork

Greasy Fork is available in English.

AO3下载文章

AO3下载tag中的文章并打包成压缩包

当前为 2024-10-26 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AO3下载文章
// @namespace    http://greasyfork.icu/users/1384897
// @version      0.2
// @description  AO3下载tag中的文章并打包成压缩包
// @author       ✌
// @match        https://archiveofourown.org/tags/*/works*
// @match        https://archiveofourown.org/works?*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @connect      archiveofourown.org
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const maxWorks = 1000; // 设置最大下载篇数
    const delay = 4000; // 设置页面跳转的延迟,单位:毫秒
    let worksProcessed = Number(localStorage.getItem('worksProcessed')) || 0;
    let zip = new JSZip();
    let isDownloading = false; // 标志变量,是否正在下载
    let downloadInterrupted = false; // 标志变量,用于控制是否中断下载

    // 恢复未完成的 ZIP 进程
    if (localStorage.getItem('ao3ZipData')) {
        const zipData = JSON.parse(localStorage.getItem('ao3ZipData'));
        Object.keys(zipData).forEach(filename => zip.file(filename, zipData[filename]));
    }

    // 创建下载按钮
    const button = document.createElement('button');
    button.innerText = `开始下载`;
    button.style.margin = "10px auto";
    button.style.display = "block";
    button.style.padding = "10px 20px";
    button.style.backgroundColor = "#3498db";
    button.style.color = "#000";
    button.style.border = "none";
    button.style.borderRadius = "5px";
    button.style.cursor = "pointer";
    button.style.fontSize = "16px";
    button.style.textAlign = "center";
    button.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)";

    // 将按钮插入到 header 中
    const header = document.querySelector('header#header');
    if (header) {
        header.insertAdjacentElement('afterend', button);
    }

    button.addEventListener('click', () => {
        if (isDownloading) {
            // 如果正在下载,则停止下载
            finalizeDownloadPartial(true);
            downloadInterrupted = true;
            console.log('下载已暂停');
            button.innerText = '开始下载';

            localStorage.clear();
            worksProcessed = 0;
            isDownloading = false;
            location.reload();
        } else {
            // 如果没有在下载,则开始下载
            downloadInterrupted = false;
            startDownload();
        }
    });

    // 自动启动下载(用于翻页后的页面)
    if (localStorage.getItem('worksProcessed')) {
        startDownload();
    }

    function startDownload() {
        console.log(`开始下载最多 ${maxWorks} 篇作品...`);
        isDownloading = true;
        button.innerText = `下载中 - 进度:${worksProcessed}/${maxWorks}`;
        updateButtonProgress();
        processPage(window.location.href);
    }

    function processWorksWithDelay(links, index = 0) {
        if (downloadInterrupted) {
            isDownloading = false;
            console.log('下载已中断');
            return;
        }

        if (index >= links.length || worksProcessed >= maxWorks) {
            checkForNextPage(document);
            return;
        }

        const link = links[index];
        GM_xmlhttpRequest({
            method: 'GET',
            url: link,
            onload: response => {
                if (downloadInterrupted) {
                    isDownloading = false;
                    console.log('下载已中断');
                    return;
                }

                const parser = new DOMParser();
                const doc = parser.parseFromString(response.responseText, "text/html");

                const title = doc.querySelector('h2.title').innerText.trim();
                let authorElement = doc.querySelector('a[rel="author"]');
                const author = authorElement ? authorElement.innerText.trim() : "匿名";
                const contentElement = doc.querySelector('#workskin');
                const content = contentElement ? contentElement.innerHTML : "<p>内容不可用</p>";

                const htmlContent = `
                    <!DOCTYPE html>
                    <html lang="en">
                    <head>
                        <meta charset="UTF-8">
                        <title>${title} by ${author}</title>
                    </head>
                    <body>
                        <h1>${title}</h1>
                        <h2>by ${author}</h2>
                        ${content}
                    </body>
                    </html>
                `;

                const filename = `${title} - ${author}.html`.replace(/[\/:*?"<>|]/g, '');
                zip.file(filename, htmlContent);

                try {
                    const zipData = JSON.parse(localStorage.getItem('ao3ZipData')) || {};
                    zipData[filename] = htmlContent;
                    localStorage.setItem('ao3ZipData', JSON.stringify(zipData));
                } catch (e) {
                    if (e.name === 'QuotaExceededError') {
                        console.warn('存储空间已满,立即导出并清空。');
                        finalizeDownloadPartial(true); // 强制导出当前部分
                    } else {
                        console.error('存储时出错:', e);
                    }
                }

                worksProcessed++;
                localStorage.setItem('worksProcessed', worksProcessed);
                console.log(`已处理 ${worksProcessed}/${maxWorks}: ${title} by ${author}`);
                updateButtonProgress();

                // 每200篇下载一个ZIP包
                if (worksProcessed % 200 === 0) {
                    finalizeDownloadPartial();
                }

                setTimeout(() => processWorksWithDelay(links, index + 1), delay);
            },
            onerror: () => {
                console.error(`加载内容失败: ${link}`);
                setTimeout(() => processWorksWithDelay(links, index + 1), delay);
            }
        });
    }

    function processPage(url) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: response => {
                const parser = new DOMParser();
                const doc = parser.parseFromString(response.responseText, "text/html");
                const links = Array.from(doc.querySelectorAll('h4.heading a'))
                                   .filter(link => link.getAttribute("href").includes("/works/"))
                                   .map(link => `${new URL(link.getAttribute('href'), window.location.origin)}?view_adult=true&view_full_work=true`);

                console.log(`正在处理页面,共有 ${links.length} 篇作品...`);
                processWorksWithDelay(links);
            },
            onerror: () => {
                console.error(`加载页面失败: ${url}`);
            }
        });
    }

    function checkForNextPage(doc) {
        if (worksProcessed >= maxWorks || downloadInterrupted) {
            finalizeDownload();
            return;
        }

        const nextLink = document.querySelector('a[rel="next"]');

        if (nextLink) {
            const nextPageUrl = new URL(nextLink.getAttribute('href'), window.location.origin).toString();
            console.log("找到下一页链接:", nextPageUrl);
            window.location.href = nextPageUrl;
        } else {
            console.log("未找到下一页链接,结束下载");
            finalizeDownload();
        }
    }

    function finalizeDownloadPartial(forceDownload = false) {
        console.log(`生成部分 ZIP 文件,包含 ${forceDownload ? worksProcessed % 200 : 200} 篇作品...`);
        zip.generateAsync({ type: "blob" }).then(blob => {
            const partNumber = Math.ceil(worksProcessed / 200);
            GM_download({
                url: URL.createObjectURL(blob),
                name: `AO3_Works_HTML_Part_${partNumber}.zip`,
                saveAs: true
            });

            zip = new JSZip();
            localStorage.removeItem('ao3ZipData');
        }).catch(err => console.error("生成部分 ZIP 时出错:", err));
    }

    function finalizeDownload() {
        if (worksProcessed % 200 !== 0) {
            finalizeDownloadPartial(true);
        }
        console.log("所有作品已处理,下载完成。");

        localStorage.clear();
        worksProcessed = 0;
        isDownloading = false;
        location.reload();
    }

    function updateButtonProgress() {
        button.innerText = `下载中 - 进度:${worksProcessed}/${maxWorks}`;
    }
})();