Greasy Fork is available in English.
批量下载 AO3 作品为 EPUB,支持 tag 页、作者页、详情页,自动翻页
当前为
// ==UserScript==
// @name AO3 Helper
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 批量下载 AO3 作品为 EPUB,支持 tag 页、作者页、详情页,自动翻页
// @author Lumiarna
// @match https://archiveofourown.org/tags/*/works*
// @match https://archiveofourown.org/works?*
// @match https://archiveofourown.org/*
// @grant GM_xmlhttpRequest
// @connect archiveofourown.org
// @connect download.archiveofourown.org
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const maxWorks = 1000;
const delay = 4000;
let worksProcessed = Number(localStorage.getItem('worksProcessed')) || 0;
let isDownloading = false;
let downloadInterrupted = false;
const style = document.createElement('style');
style.textContent = `
.ao3-helper-btn {
position: fixed;
right: 10px;
top: 90px;
z-index: 999999;
padding: 8px 14px;
border: none;
border-radius: 6px;
background: #1e90ff;
color: #fff;
font-size: 13px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
transition: opacity .2s, transform .15s;
white-space: nowrap;
}
.ao3-helper-btn:hover { opacity: .9; transform: translateY(-1px); }
.ao3-helper-btn:active { transform: translateY(0); }
.ao3-helper-btn:disabled { opacity: .6; cursor: not-allowed; }
`;
document.head.appendChild(style);
const button = document.createElement('button');
button.className = 'ao3-helper-btn';
button.innerText = '开始下载';
document.body.appendChild(button);
const isSingleWork = /\/works\/\d+/.test(window.location.pathname);
button.addEventListener('click', () => {
if (isSingleWork) {
downloadCurrentWork();
return;
}
if (isDownloading) {
downloadInterrupted = true;
button.innerText = '开始下载';
localStorage.setItem('stopFlag', 'true');
localStorage.removeItem('worksProcessed');
worksProcessed = 0;
isDownloading = false;
location.reload();
} else {
localStorage.removeItem('stopFlag');
downloadInterrupted = false;
startDownload();
}
});
if (localStorage.getItem('worksProcessed') && localStorage.getItem('stopFlag') !== 'true') {
startDownload();
}
function downloadCurrentWork() {
const title = document.querySelector('h2.title')?.textContent.trim() || '无标题';
const author = document.querySelector('a[rel="author"]')?.textContent.trim() || '匿名';
const epubHref = document.querySelector('li.download ul a[href*=".epub"]')?.getAttribute('href');
if (!epubHref) {
alert('未找到epub下载链接');
return;
}
const safeTitle = title.replace(/[\/:*?"<>|]/g, '');
const safeAuthor = author.replace(/[\/:*?"<>|]/g, '');
const filename = `${safeTitle}_${safeAuthor}.epub`;
const epubUrl = `https://download.archiveofourown.org${epubHref}`;
button.innerText = '下载中...';
button.disabled = true;
GM_xmlhttpRequest({
method: 'GET',
url: epubUrl,
responseType: 'blob',
onload: res => {
const a = document.createElement('a');
a.href = URL.createObjectURL(res.response);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
button.innerText = '下载完成';
button.disabled = false;
},
onerror: () => {
button.innerText = '下载失败';
button.disabled = false;
}
});
}
function startDownload() {
console.log(`开始下载最多 ${maxWorks} 篇作品...`);
isDownloading = true;
updateButtonProgress();
processPage(window.location.href);
}
function processWorksWithDelay(workLinks, index = 0, pageDoc) {
if (downloadInterrupted || index >= workLinks.length || worksProcessed >= maxWorks) {
checkForNextPage(pageDoc);
return;
}
const { workUrl } = workLinks[index];
GM_xmlhttpRequest({
method: 'GET',
url: workUrl,
onload: response => {
if (downloadInterrupted) return;
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, "text/html");
const title = doc.querySelector('h2.title')?.textContent.trim() || '无标题';
const author = doc.querySelector('a[rel="author"]')?.textContent.trim() || '匿名';
const epubHref = doc.querySelector('li.download ul a[href*=".epub"]')?.getAttribute('href');
if (!epubHref) {
setTimeout(() => processWorksWithDelay(workLinks, index + 1, pageDoc), delay);
return;
}
const safeTitle = title.replace(/[\/:*?"<>|]/g, '');
const safeAuthor = author.replace(/[\/:*?"<>|]/g, '');
const filename = `${safeTitle}_${safeAuthor}.epub`;
const epubUrl = `https://download.archiveofourown.org${epubHref}`;
GM_xmlhttpRequest({
method: 'GET',
url: epubUrl,
responseType: 'blob',
onload: res => {
const a = document.createElement('a');
a.href = URL.createObjectURL(res.response);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
},
onerror: e => console.error(`[AO3] 下载失败: ${filename}`, e)
});
worksProcessed++;
localStorage.setItem('worksProcessed', worksProcessed);
updateButtonProgress();
setTimeout(() => processWorksWithDelay(workLinks, index + 1, pageDoc), delay);
},
onerror: () => {
console.error(`加载内容失败: ${workUrl}`);
setTimeout(() => processWorksWithDelay(workLinks, index + 1, pageDoc), 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 workLinks = Array.from(doc.querySelectorAll('h4.heading a'))
.filter(a => /\/works\/\d+$/.test(a.pathname))
.map(a => ({ workUrl: `${a.href}?view_adult=true` }));
processWorksWithDelay(workLinks, 0, doc);
},
onerror: () => console.error(`加载页面失败: ${url}`)
});
}
function checkForNextPage(doc) {
if (worksProcessed >= maxWorks || downloadInterrupted) {
completeAndReset();
return;
}
const nextLink = doc.querySelector('li.next a');
if (nextLink) {
const nextPageUrl = new URL(nextLink.href, window.location.origin).toString();
console.log('跳转下一页:', nextPageUrl);
window.location.href = nextPageUrl;
} else {
completeAndReset();
}
}
function completeAndReset() {
console.log('下载完成,清空记录。');
localStorage.clear();
worksProcessed = 0;
isDownloading = false;
location.reload();
}
function updateButtonProgress() {
button.innerText = `下载中 - 进度:${worksProcessed}/${maxWorks}`;
}
})();