🏠 Home 

AO3下载文章

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


安装此脚本?
// ==UserScript==
// @name         AO3下载文章
// @namespace    https://greasyfork.org/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();
// 每100篇下载一个ZIP包
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.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 % 100 : 100} 篇作品...`);
zip.generateAsync({ type: "blob" }).then(blob => {
const partNumber = Math.ceil(worksProcessed / 100);
GM_download({
url: URL.createObjectURL(blob),
name: `AO3_Works_Part_${partNumber}.zip`,
saveAs: true
});
zip = new JSZip();
localStorage.removeItem('ao3ZipData');
}).catch(err => console.error("生成部分 ZIP 时出错:", err));
}
function finalizeDownload() {
if (worksProcessed % 100 !== 0) {
finalizeDownloadPartial(true);
}
console.log("所有作品已处理,下载完成。");
localStorage.clear();
worksProcessed = 0;
isDownloading = false;
location.reload();
}
function updateButtonProgress() {
button.innerText = `下载中 - 进度:${worksProcessed}/${maxWorks}`;
}
})();