批量下载 AO3 作品为 EPUB,支持 tag 页、作者页、详情页,自动翻页
// ==UserScript==
// @name AO3 Helper
// @namespace http://tampermonkey.net/
// @version 1.1
// @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';
let maxWorks = Number(localStorage.getItem('ao3_helper_maxWorks')) || 1000;
let worksProcessed = Number(localStorage.getItem('ao3_helper_worksProcessed')) || 0;
const delay = 4000;
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; }
.ao3-gear-btn {
position: fixed;
right: 10px;
top: 130px;
z-index: 999999;
padding: 6px 10px;
border: none;
border-radius: 6px;
background: #555;
color: #fff;
font-size: 16px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
transition: opacity .2s, transform .3s;
line-height: 1;
}
.ao3-gear-btn:hover { opacity: .85; transform: rotate(45deg); }
.ao3-settings-modal {
position: fixed;
right: 60px;
top: 125px;
z-index: 9999999;
background: #fff;
border: 1px solid #ddd;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,.2);
padding: 16px;
min-width: 240px;
font-size: 13px;
color: #333;
}
.ao3-settings-modal h3 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.ao3-settings-modal label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #555;
}
.ao3-settings-modal input {
width: 100%;
box-sizing: border-box;
padding: 5px 8px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 13px;
margin-bottom: 10px;
}
.ao3-settings-modal .ao3-settings-note {
font-size: 11px;
color: #999;
margin-top: -8px;
margin-bottom: 10px;
}
.ao3-settings-save {
width: 100%;
padding: 6px;
background: #1e90ff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
}
.ao3-settings-save:hover { opacity: .9; }
`;
document.head.appendChild(style);
const button = document.createElement('button');
button.className = 'ao3-helper-btn';
button.innerText = '开始下载';
document.body.appendChild(button);
// 齿轮按钮
const gearBtn = document.createElement('button');
gearBtn.className = 'ao3-gear-btn';
gearBtn.title = '下载设置';
gearBtn.textContent = '⚙';
document.body.appendChild(gearBtn);
// 设置弹窗
const modal = document.createElement('div');
modal.className = 'ao3-settings-modal';
modal.style.display = 'none';
modal.innerHTML = `
<h3>下载设置</h3>
<label>最大下载数量</label>
<input id="ao3-max-input" type="number" min="1" max="99999" />
<button class="ao3-settings-save" id="ao3-settings-save">保存</button>
`;
document.body.appendChild(modal);
modal.querySelector('#ao3-max-input').value = maxWorks;
gearBtn.addEventListener('click', (e) => {
e.stopPropagation();
modal.style.display = modal.style.display === 'none' ? 'block' : 'none';
});
modal.querySelector('#ao3-settings-save').addEventListener('click', () => {
const max = parseInt(modal.querySelector('#ao3-max-input').value, 10);
if (max > 0) {
maxWorks = max;
localStorage.setItem('ao3_helper_maxWorks', max);
}
modal.style.display = 'none';
if (isDownloading) updateButtonProgress();
});
document.addEventListener('click', (e) => {
if (!modal.contains(e.target) && e.target !== gearBtn) {
modal.style.display = 'none';
}
});
const isSingleWork = /\/works\/\d+/.test(window.location.pathname);
button.addEventListener('click', () => {
if (isSingleWork) {
downloadCurrentWork();
return;
}
if (isDownloading) {
downloadInterrupted = true;
button.innerText = '开始下载';
localStorage.setItem('ao3_helper_stopFlag', 'true');
localStorage.removeItem('ao3_helper_worksProcessed');
worksProcessed = 0;
isDownloading = false;
location.reload();
} else {
localStorage.removeItem('ao3_helper_stopFlag');
downloadInterrupted = false;
startDownload();
}
});
const savedUrl = localStorage.getItem('ao3_helper_downloadOriginUrl');
if (savedUrl && savedUrl === window.location.href && localStorage.getItem('ao3_helper_worksProcessed') && localStorage.getItem('ao3_helper_stopFlag') !== 'true') {
isDownloading = true;
updateButtonProgress();
processDoc(document);
}
function downloadCurrentWork() {
const title = document.querySelector('h2.title')?.textContent.trim() || '无标题';
const author = document.querySelector('a[rel="author"]')?.textContent.trim() || 'Anonymous';
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 => {
saveBlob(res.response, filename).then(() => {
button.innerText = '下载完成';
button.disabled = false;
});
},
onerror: () => {
button.innerText = '下载失败';
button.disabled = false;
}
});
}
function startDownload() {
worksProcessed = 0;
localStorage.removeItem('ao3_helper_worksProcessed');
console.log(`开始下载最多 ${maxWorks} 篇作品...`);
isDownloading = true;
updateButtonProgress();
processDoc(document);
}
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() || 'Anonymous';
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 => { saveBlob(res.response, filename); },
onerror: e => console.error(`[AO3] 下载失败: ${filename}`, e)
});
worksProcessed++;
localStorage.setItem('ao3_helper_worksProcessed', worksProcessed);
updateButtonProgress();
setTimeout(() => processWorksWithDelay(workLinks, index + 1, pageDoc), delay);
},
onerror: () => {
console.error(`加载内容失败: ${workUrl}`);
setTimeout(() => processWorksWithDelay(workLinks, index + 1, pageDoc), delay);
}
});
}
function processDoc(doc) {
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);
}
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);
localStorage.setItem('ao3_helper_downloadOriginUrl', nextPageUrl);
window.location.href = nextPageUrl;
} else {
completeAndReset();
}
}
function completeAndReset() {
console.log('下载完成,清空记录。');
localStorage.removeItem('ao3_helper_worksProcessed');
localStorage.removeItem('ao3_helper_stopFlag');
localStorage.removeItem('ao3_helper_downloadOriginUrl');
worksProcessed = 0;
isDownloading = false;
location.reload();
}
function updateButtonProgress() {
button.innerText = `下载中 - 进度:${worksProcessed}/${maxWorks}`;
}
function saveBlob(blob, filename) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
})();