// ==UserScript== // @name AO3 阅读历史增强导出器(kudos获取修复) // @namespace https://github.com/KWzhabing/ao3-reading-exporter // @version 2.5 // @description 一键导出 AO3 阅读历史为 JSON // @author KWzhabing // @match https://archiveofourown.org/users/*/readings* // @icon https://archiveofourown.org/favicon.ico // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const username = window.location.pathname.split('/')[2]; if (!username) return; // 创建按钮容器 const container = document.createElement('div'); container.style.margin = '1rem 0'; container.innerHTML = ` `; const readingsList = document.querySelector('#main > dl, #main > ul, .reading-list'); if (readingsList) { readingsList.parentNode.insertBefore(container, readingsList); } else { document.querySelector('#main')?.prepend(container); } // ========== 工具函数:提取 work_id ========== function extractWorkIdFromUrl(url) { const match = url.match(/\/works\/(\d+)/); return match ? match[1] : null; } // ========== 新增:通过 Kudos API 检查是否点过赞 ========== async function checkKudosGiven(workId, username) { if (!workId || !username) return false; try { let page = 1; while (true) { const kudosUrl = `https://archiveofourown.org/works/${workId}/kudos?page=${page}`; const res = await fetch(kudosUrl, { credentials: 'include', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }); if (!res.ok) { // 404 表示无 Kudos 或私有作品 if (res.status === 404) return false; throw new Error(`Kudos 请求失败: ${res.status}`); } const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); //正确选择器:查找 .kudos 下的所有 标签 const userLinks = doc.querySelectorAll('#kudos a[href^="/users/"]'); for (const link of userLinks) { const name = link.textContent.trim(); if (name === username) { return true; } } // 如果当前页少于 20 个用户,则为最后一页 if (userLinks.length < 20) break; page++; } return false; } catch (err) { console.warn(`检查 Kudos 失败 (work_id=${workId}):`, err.message); return false; // 安静降级 } } // ========== 获取评论状态(保持原逻辑,但优化健壮性)========== async function fetchAllCommentsHtml(workId, username) { if (!workId) return false; try { const commentUrl = `https://archiveofourown.org/comments/show_comments?work_id=${workId}`; const res = await fetch(commentUrl, { credentials: 'include' }); if (!res.ok) return false; const html = await res.text(); const lowerHtml = html.toLowerCase(); const lowerUser = username.toLowerCase(); return ( lowerHtml.includes(`>${lowerUser}<`) || lowerHtml.includes(`by ${lowerUser}`) || lowerHtml.includes(`comment by ${lowerUser}`) || lowerHtml.includes(`href="/users/${username}"`) ); } catch (err) { console.warn('获取评论失败 (work_id=' + workId + '):', err.message); return false; } } // ========== 快速导出(不变)========== async function exportBasicReadingHistory(btnEl) { const originalText = btnEl.textContent; btnEl.disabled = true; btnEl.textContent = '导出中...'; try { const baseUrl = `https://archiveofourown.org/users/${username}/readings`; const parser = new DOMParser(); let allReadings = []; let page = 1; while (true) { const url = page === 1 ? baseUrl : `${baseUrl}?page=${page}`; const res = await fetch(url, { credentials: 'include' }); if (!res.ok) throw new Error(`列表页请求失败: ${res.status}`); const text = await res.text(); const doc = parser.parseFromString(text, 'text/html'); const readings = Array.from(doc.querySelectorAll('li.reading')).map(item => { const titleEl = item.querySelector('h4.heading a'); const dateEl = item.querySelector('.datetime'); if (!titleEl || !dateEl) return null; const authorLinks = Array.from(item.querySelectorAll('h4.heading a[rel="author"]')); const authors = authorLinks.map(a => a.textContent.trim()); return { title: titleEl.textContent.trim(), url: 'https://archiveofourown.org' + titleEl.getAttribute('href'), authors: authors.length > 0 ? authors : ['匿名'], date: dateEl.title || dateEl.textContent.trim() }; }).filter(Boolean); if (readings.length === 0) break; allReadings.push(...readings); page++; } downloadJson(allReadings, 'ao3_reading_history_basic'); } catch (err) { console.error(err); alert('快速导出失败:' + err.message); } finally { btnEl.disabled = false; btnEl.textContent = originalText; } } // ========== 完整导出(关键修改:使用新 Kudos 检查)========== async function exportFullReadingHistory(btnEl) { const originalText = btnEl.textContent; btnEl.disabled = true; btnEl.textContent = '准备中...'; try { const baseUrl = `https://archiveofourown.org/users/${username}/readings`; const parser = new DOMParser(); let allReadings = []; let page = 1; while (true) { const url = page === 1 ? baseUrl : `${baseUrl}?page=${page}`; const res = await fetch(url, { credentials: 'include' }); if (!res.ok) throw new Error(`列表页请求失败: ${res.status}`); const text = await res.text(); const doc = parser.parseFromString(text, 'text/html'); const readings = Array.from(doc.querySelectorAll('li.reading')).map(item => { const titleEl = item.querySelector('h4.heading a'); const dateEl = item.querySelector('.datetime'); if (!titleEl || !dateEl) return null; const authorLinks = Array.from(item.querySelectorAll('h4.heading a[rel="author"]')); const authors = authorLinks.map(a => a.textContent.trim()); return { title: titleEl.textContent.trim(), url: 'https://archiveofourown.org' + titleEl.getAttribute('href'), authors: authors.length > 0 ? authors : ['匿名'], date: dateEl.title || dateEl.textContent.trim() }; }).filter(Boolean); if (readings.length === 0) break; allReadings.push(...readings); page++; } if (allReadings.length === 0) { alert('未找到任何阅读记录'); return; } const MAX_CONCURRENT = 4; // 降低并发,避免 429 const DELAY_PER_BATCH = 1000; // 增加延迟,更友好 const total = allReadings.length; let completed = 0; const updateProgress = () => { const percent = Math.min(100, Math.round((completed / total) * 100)); btnEl.textContent = `导出中... ${percent}% (${completed}/${total})`; }; async function fetchWorkDetails(reading) { try { const workRes = await fetch(reading.url, { credentials: 'include' }); if (!workRes.ok) throw new Error(`HTTP ${workRes.status}`); const workText = await workRes.text(); const workDoc = new DOMParser().parseFromString(workText, 'text/html'); let words = null; const wordMeta = workDoc.querySelector('dd.words') || [...workDoc.querySelectorAll('p.meta')].find(p => p.textContent.includes('Words:')); if (wordMeta) { const match = wordMeta.textContent.match(/[\d,]+/); if (match) words = parseInt(match[0].replace(/,/g, ''), 10); } const tags = Array.from(workDoc.querySelectorAll('.tag')) .map(el => el.textContent.trim()) .filter(t => t && t !== 'No tags'); //使用新方法检查 Kudos const workId = extractWorkIdFromUrl(reading.url); const kudos_given = workId ? await checkKudosGiven(workId, username) : false; const commented = workId ? await fetchAllCommentsHtml(workId, username) : false; return { ...reading, words, tags, kudos_given, commented }; } catch (err) { console.warn(`跳过作品 ${reading.url}:`, err.message); return { ...reading, words: null, tags: [], kudos_given: false, commented: false }; } finally { completed++; updateProgress(); } } const queue = [...allReadings]; const results = []; while (queue.length > 0) { const batch = queue.splice(0, MAX_CONCURRENT); const promises = batch.map(fetchWorkDetails); const batchResults = await Promise.all(promises); results.push(...batchResults); if (queue.length > 0) { await new Promise(r => setTimeout(r, DELAY_PER_BATCH)); } } downloadJson(results, 'ao3_reading_history_enhanced'); } catch (err) { console.error(err); alert('完整导出失败:' + err.message); } finally { btnEl.disabled = false; btnEl.textContent = originalText; } } // ========== 事件绑定 ========== document.getElementById('ao3-export-basic').addEventListener('click', function () { exportBasicReadingHistory(this); }); document.getElementById('ao3-export-full').addEventListener('click', function () { exportFullReadingHistory(this); }); // ========== 安全下载函数(不变)========== function downloadJson(data, prefix) { const jsonStr = JSON.stringify(data, null, 2); const blob = new Blob([jsonStr], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const filename = `${prefix}_${new Date().toISOString().slice(0, 10)}.json`; const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); const fallbackLink = document.createElement('a'); fallbackLink.href = url; fallbackLink.download = filename; fallbackLink.textContent = `如果未自动下载,请点此保存:${filename}`; fallbackLink.style.cssText = ` position: fixed; bottom: 20px; left: 20px; padding: 10px 16px; background: #ff9800; color: white; border-radius: 4px; z-index: 10000; text-decoration: none; font-family: sans-serif; box-shadow: 0 2px 8px rgba(0,0,0,0.3); `; document.body.appendChild(fallbackLink); setTimeout(() => { if (fallbackLink.parentNode) fallbackLink.parentNode.removeChild(fallbackLink); URL.revokeObjectURL(url); }, 5000); } })();