// ==UserScript== // @name AO3助手 // @namespace https://greasyfork.org/users/1384897 // @version 0.7 // @description 下载AO3 tag/详情页/作者页文章为EPUB // @author ✌ // @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'; 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: 95px; 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('a[rel="next"]'); 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}`; } })();