// ==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 // @downloadURL none // ==/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 = `

下载设置

`; 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); } })();