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