// ==UserScript==
// @name AO3下载文章(修复版)
// @namespace https://greasyfork.org/users/1384897
// @version 0.3
// @description AO3下载tag中的文章并打包成压缩包,修复最后一批ZIP下载问题
// @author ✌
// @match https://archiveofourown.org/tags/*/works*
// @match https://archiveofourown.org/works?*
// @match https://archiveofourown.org/*
// @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
// @downloadURL none
// ==/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;
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.cssText = `
margin: 10px auto;
display: block;
padding: 10px 20px;
background-color: #3498db;
color: #000;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);`;
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;
updateButtonProgress();
processPage(window.location.href);
}
function processWorksWithDelay(links, index = 0) {
if (downloadInterrupted || index >= links.length || worksProcessed >= maxWorks) {
checkForNextPage(document);
return;
}
const link = links[index];
GM_xmlhttpRequest({
method: 'GET',
url: link,
onload: response => {
if (downloadInterrupted) return;
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, "text/html");
const title = doc.querySelector('h2.title')?.innerText.trim() || '无标题';
const author = doc.querySelector('a[rel="author"]')?.innerText.trim() || "匿名";
const content = doc.querySelector('#workskin')?.innerHTML || "
内容不可用
";
const htmlContent = `
${title} by ${author}
${title}
by ${author}
${content}
`;
const filename = `${worksProcessed + 1}`.padStart(4, '0') + ` - ${title} - ${author}.html`.replace(/[\/:*?"<>|]/g, '');
zip.file(filename, htmlContent);
const zipData = JSON.parse(localStorage.getItem('ao3ZipData') || '{}');
zipData[filename] = htmlContent;
try {
localStorage.setItem('ao3ZipData', JSON.stringify(zipData));
} catch (e) {
if (e.name === 'QuotaExceededError') {
finalizeDownloadPartial(true);
}
}
worksProcessed++;
localStorage.setItem('worksProcessed', worksProcessed);
updateButtonProgress();
if (worksProcessed % 100 === 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.href.includes("/works/"))
.map(link => `${link.href}?view_adult=true&view_full_work=true`);
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.href, window.location.origin).toString();
console.log("跳转下一页:", nextPageUrl);
window.location.href = nextPageUrl;
} else {
finalizeDownload();
}
}
function finalizeDownloadPartial(forceDownload = false, callback = null) {
if (Object.keys(zip.files).length === 0) {
if (callback) callback();
return;
}
console.log(`生成部分 ZIP 文件...`);
zip.generateAsync({ type: "blob" }).then(blob => {
const partNumber = Math.ceil(worksProcessed / 100) || 1;
GM_download({
url: URL.createObjectURL(blob),
name: `AO3_Works_Part_${partNumber}.zip`,
saveAs: true,
onload: () => {
zip = new JSZip();
localStorage.removeItem('ao3ZipData');
if (callback) callback();
},
onerror: (e) => {
console.error("ZIP 下载失败:", e);
if (callback) callback();
}
});
}).catch(err => {
console.error("ZIP 生成失败:", err);
if (callback) callback();
});
}
function finalizeDownload() {
if (worksProcessed % 100 !== 0) {
finalizeDownloadPartial(true, () => {
completeAndReset();
});
} else {
completeAndReset();
}
}
function completeAndReset() {
console.log("下载完成,清空记录。");
localStorage.clear();
worksProcessed = 0;
isDownloading = false;
location.reload();
}
function updateButtonProgress() {
button.innerText = `下载中 - 进度:${worksProcessed}/${maxWorks}`;
}
})();