// ==UserScript== // @name 番茄小说增强助手 // @namespace http://tampermonkey.net/ // @version 3.0 // @description 番茄小说阅读增强:正文替换 + 高速下载,支持并发控制 // @author Combined Script // @license MIT License // @match https://fanqienovel.com/* // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect tt.sjmyzq.cn // @connect fanqienovel.com // @connect 20071006.xyz // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/570631/%E7%95%AA%E8%8C%84%E5%B0%8F%E8%AF%B4%E5%A2%9E%E5%BC%BA%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/570631/%E7%95%AA%E8%8C%84%E5%B0%8F%E8%AF%B4%E5%A2%9E%E5%BC%BA%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function() { 'use strict'; const Utils = { delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, extractBookId(input) { if (!input) return null; const patterns = [ /book_id=(\d+)/, /\/page\/(\d+)/, /\/reader\/(\d+)/, /^(\d+)$/ ]; for (const pattern of patterns) { const match = input.match(pattern); if (match) return match[1]; } return null; }, extractChapterId(url) { const match = url.match(/\/(\d+)/); return match ? match[1] : null; }, safeJSONParse(text, defaultValue = null) { try { return JSON.parse(text); } catch (e) { return defaultValue; } } }; class ContentReplacer { constructor() { this.currentURL = window.location.href; this.init(); } init() { const pageType = this.detectPageType(); if (pageType === 'reader') { this.setupReaderPage(); } else if (pageType === 'page') { this.setupBookPage(); } } detectPageType() { const url = window.location.href; if (url.includes('/reader/')) return 'reader'; if (url.includes('/page/')) return 'page'; return null; } setupReaderPage() { this.replaceReaderContent(); setInterval(() => { if (window.location.href !== this.currentURL) { this.currentURL = window.location.href; this.replaceReaderContent(); } }, 1000); } async replaceReaderContent() { const contentDiv = document.querySelector('.muye-reader-content.noselect, .muye-reader-content'); if (!contentDiv) return; const chapterId = Utils.extractChapterId(window.location.href); if (!chapterId) return; document.title = document.title.replace(/在线免费阅读_番茄小说官网$/, ''); const apiUrl = `https://tt.sjmyzq.cn/api/raw_full?item_id=${chapterId}`; GM_xmlhttpRequest({ method: 'GET', url: apiUrl, onload: (response) => { try { const data = Utils.safeJSONParse(response.responseText); if (data?.code === 200 && data.data && data.data.content) { let content = data.data.content; const articleMatch = content.match(/
([\s\S]*?)<\/article>/); if (articleMatch) { content = articleMatch[1]; } else { content = content.replace(/]*>.*?<\/h1>/gi, ''); content = content.replace(/]*>/gi, '

').replace(/<\/p>/gi, '

'); } if (contentDiv.classList.length > 1) { contentDiv.classList = contentDiv.classList[0]; } contentDiv.innerHTML = content; const vipBanner = document.querySelector('.muye-to-vip'); if (vipBanner) { vipBanner.remove(); } const toFanqie = document.querySelector('.muye-to-fanqie'); if (toFanqie) { toFanqie.remove(); } const readerBox = document.querySelector('.muye-reader-box'); if (readerBox) { readerBox.style.filter = ''; readerBox.style.backdropFilter = ''; readerBox.style.webkitBackdropFilter = ''; readerBox.classList.remove('serial-filter-gray'); } contentDiv.style.filter = ''; contentDiv.style.backdropFilter = ''; contentDiv.style.webkitBackdropFilter = ''; contentDiv.style.webkitFilter = ''; const masks = contentDiv.querySelectorAll('[class*="mask"], [class*="blur"], [class*="lock"]'); masks.forEach(el => el.remove()); contentDiv.classList.remove('noselect'); console.log('[FanqieEnhancer] 正文替换成功'); } } catch (e) { console.error('[FanqieEnhancer] 正文替换失败:', e); } }, onerror: (error) => { console.error('[FanqieEnhancer] 请求失败:', error); } }); } setupBookPage() { const downloadBtn = document.querySelector('a[href*="download"], .download-btn, [class*="download"]'); if (downloadBtn) { downloadBtn.addEventListener('click', (e) => { e.preventDefault(); if (window.fanqieDownloader) { window.fanqieDownloader.startDownloadFromPage(); } }); } } } class FanqieDownloader { constructor() { this.config = { maxLogEntries: 100, version: '3.0' }; this.state = { currentBookId: null, currentBookName: null, isDownloading: false, abortController: null }; this.cache = { elements: {} }; this.init(); } init() { this.addStyles(); this.createPanel(); this.cacheElements(); this.bindEvents(); this.loadSettings(); setTimeout(() => this.detectCurrentPageBook(), 1000); window.fanqieDownloader = this; console.log('[FanqieEnhancer] 初始化完成 v' + this.config.version); } cacheElements() { const ids = [ 'fanqieBody', 'fanqieToggle', 'fanqieBookId', 'fanqieStart', 'fanqieEnd', 'fanqieConcurrency', 'fanqieProgress', 'fanqieProgressFill', 'fanqieProgressPercent', 'fanqieProgressStatus', 'fanqieSuccessCount', 'fanqieFailCount', 'fanqieSpeed', 'fanqieLog', 'fanqieCurrentBook', 'fanqieCurrentBookInfo', 'fanqieDownloadCurrent', 'fanqieEncoding', 'fanqieDualApi', 'fanqieBackupConcurrency' ]; ids.forEach(id => { this.cache.elements[id] = document.getElementById(id); }); } addStyles() { const styles = ` .muye-reader-content.noselect::after { display: none !important; } .muye-reader-content.noselect::before { display: none !important; } .muye-to-vip, .muye-to-vip-mask, .reader-unlock, .reader-lock-overlay, [class*="vip"], [class*="lock"], [class*="mask"], [class*="blur"] { display: none !important; } .fanqie-downloader-panel { position: fixed; top: 20px; right: 20px; width: 300px; background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; overflow: hidden; font-size: 12px; transition: all 0.3s ease; } .fanqie-downloader-panel.fanqie-collapsed { width: auto; min-width: 0; right: 0; border-radius: 8px 0 0 8px; } .fanqie-downloader-panel.fanqie-collapsed .fanqie-downloader-header { padding: 6px 10px; font-size: 11px; border-radius: 8px 0 0 8px; } .fanqie-downloader-panel.fanqie-collapsed .fanqie-downloader-header span { display: none; } .fanqie-downloader-panel.fanqie-collapsed .fanqie-downloader-header button { width: 18px; height: 18px; font-size: 11px; } .fanqie-downloader-header { background: linear-gradient(135deg, #ff6b6b, #ff8e8e); color: white; padding: 10px 12px; font-size: 13px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .fanqie-downloader-header button { background: rgba(255,255,255,0.2); border: none; color: white; width: 22px; height: 22px; border-radius: 50%; cursor: pointer; font-size: 12px; transition: all 0.3s; } .fanqie-downloader-header button:hover { background: rgba(255,255,255,0.3); } .fanqie-downloader-body { padding: 10px 12px; max-height: 400px; overflow-y: auto; } .fanqie-downloader-body.hidden { display: none; } .fanqie-input-group { margin-bottom: 8px; } .fanqie-input-group label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; } .fanqie-input-group input, .fanqie-input-group select { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; transition: border-color 0.3s; box-sizing: border-box; } .fanqie-input-group input:focus, .fanqie-input-group select:focus { outline: none; border-color: #ff6b6b; } .fanqie-input-row { display: flex; gap: 8px; } .fanqie-input-row .fanqie-input-group { flex: 1; } .fanqie-btn { width: 100%; padding: 8px; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; margin-bottom: 6px; transition: all 0.3s; } .fanqie-btn-primary { background: #ff6b6b; color: white; } .fanqie-btn-primary:hover:not(:disabled) { background: #ff5252; } .fanqie-btn-primary:disabled { background: #ccc; cursor: not-allowed; } .fanqie-progress { margin-top: 10px; padding: 8px; background: #f8f8f8; border-radius: 4px; } .fanqie-progress-bar { height: 4px; background: #e0e0e0; border-radius: 2px; overflow: hidden; margin-bottom: 6px; } .fanqie-progress-fill { height: 100%; background: linear-gradient(90deg, #ff6b6b, #ff8e8e); transition: width 0.3s; width: 0%; } .fanqie-progress-text { font-size: 11px; color: #666; display: flex; justify-content: space-between; } .fanqie-log { margin-top: 8px; max-height: 150px; overflow-y: auto; background: #1e1e1e; color: #0f0; padding: 10px; border-radius: 6px; font-size: 12px; font-family: monospace; } .fanqie-log-entry { margin-bottom: 3px; animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } .fanqie-log-error { color: #ff6b6b; } .fanqie-log-success { color: #69f0ae; } .fanqie-log-info { color: #64b5f6; } .fanqie-log-warning { color: #ffd93d; } .fanqie-current-book { background: #fff3f3; border: 1px solid #ff6b6b; border-radius: 6px; padding: 10px; margin-bottom: 12px; } .fanqie-current-book-title { font-weight: 600; color: #ff6b6b; margin-bottom: 5px; } .fanqie-current-book-id { font-size: 12px; color: #666; } .fanqie-stats { display: flex; justify-content: space-between; font-size: 12px; color: #666; margin-top: 5px; } .fanqie-section-title { font-size: 13px; font-weight: 600; color: #333; margin: 15px 0 10px 0; padding-bottom: 5px; border-bottom: 1px solid #eee; } `; const styleEl = document.createElement('style'); styleEl.textContent = styles; document.head.appendChild(styleEl); } createPanel() { const panel = document.createElement('div'); panel.className = 'fanqie-downloader-panel'; panel.innerHTML = `
📚 番茄小说增强 v${this.config.version}
⚙️ 配置
`; document.body.appendChild(panel); } bindEvents() { this.cache.elements.fanqieToggle?.addEventListener('click', () => this.togglePanel()); this.cache.elements.fanqieDownloadCurrent?.addEventListener('click', () => this.downloadCurrentBook()); // 拖拽功能 this.initDraggable(); // 下载配置区域折叠 const configToggle = document.getElementById('fanqieConfigToggle'); const configContent = document.getElementById('fanqieConfigContent'); const toggleIcon = document.getElementById('fanqieConfigToggleIcon'); if (configToggle && configContent) { configToggle.addEventListener('click', () => { const isHidden = configContent.style.display === 'none'; configContent.style.display = isHidden ? 'block' : 'none'; if (toggleIcon) { toggleIcon.textContent = isHidden ? '▼' : '▲'; } }); } // 并发数限制 if (this.cache.elements.fanqieConcurrency) { this.cache.elements.fanqieConcurrency.addEventListener('input', () => { let val = parseInt(this.cache.elements.fanqieConcurrency.value); if (val > 30) this.cache.elements.fanqieConcurrency.value = 30; if (val < 1) this.cache.elements.fanqieConcurrency.value = 1; }); } if (this.cache.elements.fanqieBackupConcurrency) { this.cache.elements.fanqieBackupConcurrency.addEventListener('input', () => { let val = parseInt(this.cache.elements.fanqieBackupConcurrency.value); if (val > 30) this.cache.elements.fanqieBackupConcurrency.value = 30; if (val < 1) this.cache.elements.fanqieBackupConcurrency.value = 1; }); } // 备用接口配置显示/隐藏 this.cache.elements.fanqieDualApi?.addEventListener('change', (e) => { const backupConfig = document.getElementById('fanqieBackupConfig'); if (backupConfig) { backupConfig.style.display = e.target.checked ? 'block' : 'none'; } this.saveSettings(); }); // 保存设置 ['fanqieConcurrency', 'fanqieEncoding', 'fanqieBackupConcurrency'].forEach(id => { this.cache.elements[id]?.addEventListener('change', () => this.saveSettings()); }); } initDraggable() { const panel = document.querySelector('.fanqie-downloader-panel'); const header = document.querySelector('.fanqie-downloader-header'); if (!panel || !header) return; let isDragging = false; let startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panel.style.left = startLeft + dx + 'px'; panel.style.top = startTop + dy + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; panel.style.transition = ''; }); } saveSettings() { const settings = { concurrency: this.cache.elements.fanqieConcurrency?.value || '30', encoding: this.cache.elements.fanqieEncoding?.value || 'utf-8', dualApi: this.cache.elements.fanqieDualApi?.checked || false, backupConcurrency: this.cache.elements.fanqieBackupConcurrency?.value || '20' }; GM_setValue('fanqieSettings', JSON.stringify(settings)); } loadSettings() { try { const saved = GM_getValue('fanqieSettings', '{}'); const settings = JSON.parse(saved); if (settings.concurrency) { this.cache.elements.fanqieConcurrency.value = settings.concurrency; } if (settings.encoding) { this.cache.elements.fanqieEncoding.value = settings.encoding; } if (settings.dualApi !== undefined && this.cache.elements.fanqieDualApi) { this.cache.elements.fanqieDualApi.checked = settings.dualApi; const backupConfig = document.getElementById('fanqieBackupConfig'); if (backupConfig) { backupConfig.style.display = settings.dualApi ? 'block' : 'none'; } } if (settings.backupConcurrency) { this.cache.elements.fanqieBackupConcurrency.value = settings.backupConcurrency; } } catch (e) { console.log('[FanqieEnhancer] 加载设置失败:', e); } } togglePanel() { const body = this.cache.elements.fanqieBody; const panel = document.querySelector('.fanqie-downloader-panel'); const btn = this.cache.elements.fanqieToggle; if (body.classList.contains('hidden')) { body.classList.remove('hidden'); panel.classList.remove('fanqie-collapsed'); btn.textContent = '−'; } else { body.classList.add('hidden'); panel.classList.add('fanqie-collapsed'); btn.textContent = '+'; } } log(message, type = 'info') { const logDiv = this.cache.elements.fanqieLog; if (!logDiv) return; const entry = document.createElement('div'); entry.className = `fanqie-log-entry fanqie-log-${type}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logDiv.appendChild(entry); while (logDiv.children.length > this.config.maxLogEntries) { logDiv.removeChild(logDiv.firstChild); } logDiv.scrollTop = logDiv.scrollHeight; } updateProgress(percent, status, successCount, failCount, speed) { const els = this.cache.elements; if (els.fanqieProgressFill) { els.fanqieProgressFill.style.width = percent + '%'; } if (els.fanqieProgressPercent) { els.fanqieProgressPercent.textContent = percent + '%'; } if (status && els.fanqieProgressStatus) { els.fanqieProgressStatus.textContent = status; } if (successCount !== undefined && els.fanqieSuccessCount) { els.fanqieSuccessCount.textContent = `成功: ${successCount}`; } if (failCount !== undefined && els.fanqieFailCount) { els.fanqieFailCount.textContent = `失败: ${failCount}`; } if (speed !== undefined && els.fanqieSpeed) { els.fanqieSpeed.textContent = `速度: ${speed.toFixed(1)}章/秒`; } } showProgress() { if (this.cache.elements.fanqieProgress) { this.cache.elements.fanqieProgress.style.display = 'block'; } } detectCurrentPageBook() { try { const url = window.location.href; let bookId = null; let bookName = null; // 判断页面类型 if (url.includes('/reader/')) { // 阅读页面:需要从页面中提取 book_id this.log('检测到阅读页面,正在提取书籍信息...', 'info'); // 方法1:从返回按钮或书籍链接中提取 const bookLink = document.querySelector('a[href*="/page/"], a[href*="book_id"]'); if (bookLink) { const href = bookLink.getAttribute('href') || ''; const match = href.match(/\/page\/(\d+)/); if (match) { bookId = match[1]; this.log(`从书籍链接提取到 book_id: ${bookId}`, 'info'); } } // 方法2:从页面标题提取书名 const titleEl = document.querySelector('.reader-chapter-title, .chapter-title, h1'); if (titleEl) { bookName = titleEl.textContent.trim(); } // 方法3:从面包屑导航提取 if (!bookId) { const breadcrumb = document.querySelector('.breadcrumb, .nav-path, [class*="breadcrumb"]'); if (breadcrumb) { const pageLink = breadcrumb.querySelector('a[href*="/page/"]'); if (pageLink) { const match = pageLink.getAttribute('href').match(/\/page\/(\d+)/); if (match) { bookId = match[1]; } } } } // 方法4:从页面元数据或脚本中提取 if (!bookId) { const scripts = document.querySelectorAll('script'); for (const script of scripts) { const content = script.textContent || ''; const match = content.match(/"bookId"\s*:\s*"?(\d+)/); if (match) { bookId = match[1]; break; } } } } else if (url.includes('/page/')) { // 详情页:直接从 URL 提取 bookId = Utils.extractBookId(url); const bookNameEl = document.querySelector('h1, .book-title, [class*="title"]'); bookName = bookNameEl ? bookNameEl.textContent.trim() : null; } if (bookId) { this.state.currentBookId = bookId; this.state.currentBookName = bookName; if (this.cache.elements.fanqieCurrentBook) { this.cache.elements.fanqieCurrentBook.style.display = 'block'; } if (this.cache.elements.fanqieCurrentBookInfo) { this.cache.elements.fanqieCurrentBookInfo.textContent = `${bookName || '未知书名'} (ID: ${bookId})`; } if (this.cache.elements.fanqieDownloadCurrent) { this.cache.elements.fanqieDownloadCurrent.style.display = 'block'; } if (this.cache.elements.fanqieBookId) { this.cache.elements.fanqieBookId.value = bookId; } this.log(`✅ 检测到小说: ${bookName || '未知书名'} (${bookId})`, 'success'); } else { this.log('⚠️ 未能检测到书籍ID,请手动输入', 'warning'); } } catch (error) { this.log(`检测页面小说失败: ${error.message}`, 'error'); } } downloadCurrentBook() { if (this.state.currentBookId) { this.cache.elements.fanqieBookId.value = this.state.currentBookId; this.fetchChapterList(); } } startDownloadFromPage() { this.detectCurrentPageBook(); if (this.state.currentBookId) { this.fetchChapterList(); } } setDownloadingState(isDownloading) { this.state.isDownloading = isDownloading; const downloadBtn = this.cache.elements.fanqieDownloadCurrent; if (downloadBtn) { downloadBtn.disabled = isDownloading; downloadBtn.textContent = isDownloading ? '⏳ 下载中...' : '📥 下载当前小说'; } } async fetchChapterList() { if (this.state.isDownloading) { this.log('⚠️ 下载正在进行中', 'warning'); return; } const input = this.cache.elements.fanqieBookId?.value.trim(); const bookId = Utils.extractBookId(input); if (!bookId) { alert('请输入有效的小说ID或链接'); return; } this.state.abortController = new AbortController(); this.setDownloadingState(true); this.showProgress(); this.log(`开始获取小说 ${bookId} 的章节列表...`); const url = `https://tt.sjmyzq.cn/api/book?book_id=${bookId}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: (response) => { try { const data = JSON.parse(response.responseText); this.processChapterData(data, bookId); } catch (e) { this.log('❌ 解析响应失败: ' + e.message, 'error'); this.setDownloadingState(false); } }, onerror: () => { this.log('❌ 请求失败', 'error'); this.setDownloadingState(false); } }); } async processChapterData(data, bookId) { // 调试输出 this.log(`API响应: code=${data?.code}, 数据结构: ${JSON.stringify(Object.keys(data || {}))}`, 'info'); if (data?.data) { this.log(`data字段: ${JSON.stringify(Object.keys(data.data))}`, 'info'); } let chapterListWithVolume = null; let bookInfo = null; // 尝试多种可能的数据结构 if (data?.code === 200 && data.data) { this.log(`data.data 类型: ${typeof data.data}, 字段: ${JSON.stringify(Object.keys(data.data))}`, 'info'); if (data.data.data) { this.log(`data.data.data 类型: ${typeof data.data.data}, 字段: ${JSON.stringify(Object.keys(data.data.data))}`, 'info'); } // 直接结构: data.data.chapterListWithVolume if (data.data.chapterListWithVolume) { chapterListWithVolume = data.data.chapterListWithVolume; bookInfo = data.data.bookInfo || data.data; this.log('使用数据结构: data.data.chapterListWithVolume', 'info'); } // 嵌套结构: data.data.data.chapterListWithVolume else if (data.data.data?.chapterListWithVolume) { chapterListWithVolume = data.data.data.chapterListWithVolume; bookInfo = data.data.data.bookInfo || data.data.data; this.log('使用数据结构: data.data.data.chapterListWithVolume', 'info'); this.log(`chapterListWithVolume 类型: ${typeof chapterListWithVolume}, 是数组: ${Array.isArray(chapterListWithVolume)}, 长度: ${chapterListWithVolume?.length || 0}`, 'info'); // 如果 chapterListWithVolume 为空,尝试使用 allItemIds if ((!chapterListWithVolume || chapterListWithVolume.length === 0) && data.data.data.allItemIds) { this.log(`尝试使用 allItemIds, 长度: ${data.data.data.allItemIds.length}`, 'info'); this.log(`allItemIds 预览: ${JSON.stringify(data.data.data.allItemIds).substring(0, 200)}`, 'info'); } } // 其他可能的字段名 else if (data.data.chapters) { chapterListWithVolume = [data.data.chapters]; bookInfo = data.data; this.log('使用数据结构: data.data.chapters', 'info'); } else if (data.data.list) { chapterListWithVolume = [data.data.list]; bookInfo = data.data; this.log('使用数据结构: data.data.list', 'info'); } if (!bookInfo || (!bookInfo.bookName && !bookInfo.name)) { if (data.data.bookName) { bookInfo = { ...bookInfo, bookName: data.data.bookName }; } else if (data.data.name) { bookInfo = { ...bookInfo, name: data.data.name }; } else if (data.data.title) { bookInfo = { ...bookInfo, bookName: data.data.title }; } } } // 如果 chapterListWithVolume 为空,尝试使用 allItemIds 构建 if ((!chapterListWithVolume || chapterListWithVolume.length === 0) && data.data?.data?.allItemIds) { this.log('chapterListWithVolume 为空,使用 allItemIds 构建章节列表', 'info'); const allItemIds = data.data.data.allItemIds; if (Array.isArray(allItemIds) && allItemIds.length > 0) { // 将 allItemIds 转换为 chapterListWithVolume 格式 chapterListWithVolume = [allItemIds.map((itemId, index) => ({ itemId: itemId, title: `第${index + 1}章`, realChapterOrder: index + 1 }))]; this.log(`使用 allItemIds 构建,共 ${allItemIds.length} 章`, 'info'); } } if (chapterListWithVolume && chapterListWithVolume.length > 0) { this.log(`章节列表类型: ${typeof chapterListWithVolume}, 长度: ${chapterListWithVolume.length}`, 'info'); this.log(`第一卷类型: ${typeof chapterListWithVolume[0]}, 是数组: ${Array.isArray(chapterListWithVolume[0])}`, 'info'); const volumes = []; let globalChapterIndex = 0; for (let vIndex = 0; vIndex < chapterListWithVolume.length; vIndex++) { const volume = chapterListWithVolume[vIndex]; const volumeChapters = []; // 确保volume是数组 if (!Array.isArray(volume)) { this.log(`警告: 第${vIndex + 1}卷数据不是数组`, 'warning'); continue; } for (const ch of volume) { // 兼容不同的字段名 const itemId = ch.itemId || ch.item_id || ch.id || ch.chapterId; const title = ch.title || ch.chapterTitle || ch.chapter_title || ch.name || `第${globalChapterIndex + 1}章`; const realChapterOrder = ch.realChapterOrder || ch.order || ch.index || ch.chapterIndex || globalChapterIndex; if (itemId) { volumeChapters.push({ itemId: itemId, title: title, realChapterOrder: realChapterOrder, globalIndex: globalChapterIndex++ }); } } const apiVolumeTitle = volume[0]?.volumeTitle || volume[0]?.volume_name || volume[0]?.volumeName; if (volumeChapters.length > 0) { volumes.push({ volumeIndex: vIndex + 1, volumeTitle: apiVolumeTitle, chapters: volumeChapters }); } } const totalChapters = volumes.reduce((sum, v) => sum + v.chapters.length, 0); let bookName = null; if (this.state.currentBookId === bookId && this.state.currentBookName) { bookName = this.state.currentBookName; } else { if (bookInfo) { bookName = bookInfo.bookName || bookInfo.name || bookInfo.title || bookInfo.book_name; } if (!bookName && chapterListWithVolume[0] && chapterListWithVolume[0][0]) { const firstCh = chapterListWithVolume[0][0]; if (firstCh.bookName) bookName = firstCh.bookName; } } if (!bookName) bookName = '未知书名'; this.log(`✅ 成功获取《${bookName}》共 ${volumes.length} 卷 ${totalChapters} 章`, 'success'); this.cache.elements.fanqieEnd.value = totalChapters; await this.startDownload(bookId, bookName, volumes, totalChapters); } else { this.log('❌ 获取章节列表失败: 数据结构异常', 'error'); this.setDownloadingState(false); } } async startDownload(bookId, bookName, volumes, totalChapters) { const startIndex = parseInt(this.cache.elements.fanqieStart?.value) - 1 || 0; let endIndex = parseInt(this.cache.elements.fanqieEnd?.value) - 1; if (isNaN(endIndex) || endIndex < 0) endIndex = totalChapters - 1; const useDualApi = this.cache.elements.fanqieDualApi?.checked || false; const concurrency = Math.min(parseInt(this.cache.elements.fanqieConcurrency?.value) || 30, 30); const delayTime = 300; const backupConcurrency = useDualApi ? Math.min(parseInt(this.cache.elements.fanqieBackupConcurrency?.value) || 20, 30) : 0; const chaptersToDownload = []; for (const volume of volumes) { for (const ch of volume.chapters) { if (ch.globalIndex >= startIndex && ch.globalIndex <= endIndex) { chaptersToDownload.push({ ...ch, volumeIndex: volume.volumeIndex, volumeTitle: volume.volumeTitle }); } } } const total = chaptersToDownload.length; const results = []; let successCount = 0; let failCount = 0; const maxFail = 10; let consecutiveFails = 0; const downloadStartTime = Date.now(); if (useDualApi) { this.log(`📥 开始下载《${bookName}》: 共${total}章, 双接口并发模式`); this.log(` 主接口: 前${concurrency}章/批, 备用接口: 后${backupConcurrency}章/批`); let primaryIndex = 0; let backupIndex = concurrency; while (primaryIndex < total || backupIndex < total) { if (this.state.abortController?.signal.aborted) { this.log('⏹️ 下载已取消', 'warning'); break; } const primaryBatch = []; const backupBatch = []; for (let i = 0; i < concurrency && primaryIndex < total; i++, primaryIndex++) { primaryBatch.push(chaptersToDownload[primaryIndex]); } for (let i = 0; i < backupConcurrency && backupIndex < total; i++, backupIndex++) { backupBatch.push(chaptersToDownload[backupIndex]); } const allPromises = []; primaryBatch.forEach(ch => { allPromises.push( this.downloadWithSingleApi(ch, bookName, 'primary').then(result => ({ ...result, ch, source: '主接口' })) ); }); backupBatch.forEach(ch => { allPromises.push( this.downloadWithSingleApi(ch, bookName, 'backup').then(result => ({ ...result, ch, source: '备用' })) ); }); const batchResults = await Promise.all(allPromises); for (const { success, content, ch, source } of batchResults) { if (success) { results.push({ volumeIndex: ch.volumeIndex, volumeTitle: ch.volumeTitle, chapterOrder: ch.realChapterOrder, title: ch.title, content: content, globalIndex: ch.globalIndex }); successCount++; consecutiveFails = 0; this.log(`✅ [卷${ch.volumeIndex}] ${ch.title} (${source})`, 'success'); } else { failCount++; consecutiveFails++; this.log(`❌ [卷${ch.volumeIndex}] ${ch.title} 获取失败`, 'error'); } } const processed = Math.max(primaryIndex, backupIndex); const progress = Math.round((processed / total) * 100); const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = elapsed > 0 ? (successCount + failCount) / elapsed : 0; this.updateProgress(progress, `下载中... (${processed}/${total})`, successCount, failCount, speed); if (consecutiveFails >= maxFail) { this.log(`⚠️ 连续失败${maxFail}次,停止下载`, 'error'); break; } if (delayTime > 0 && (primaryIndex < total || backupIndex < total)) { await Utils.delay(delayTime); } } } else { this.log(`📥 开始下载《${bookName}》: 共${total}章, 并发数${concurrency}`); for (let i = 0; i < chaptersToDownload.length; i += concurrency) { if (this.state.abortController?.signal.aborted) { this.log('⏹️ 下载已取消', 'warning'); break; } const batch = chaptersToDownload.slice(i, i + concurrency); const batchPromises = batch.map(ch => this.downloadWithSingleApi(ch, bookName, 'primary').then(result => ({ ...result, ch })) ); const batchResults = await Promise.all(batchPromises); for (const { success, content, ch } of batchResults) { if (success) { results.push({ volumeIndex: ch.volumeIndex, volumeTitle: ch.volumeTitle, chapterOrder: ch.realChapterOrder, title: ch.title, content: content, globalIndex: ch.globalIndex }); successCount++; consecutiveFails = 0; this.log(`✅ [卷${ch.volumeIndex}] ${ch.title}`, 'success'); } else { failCount++; consecutiveFails++; this.log(`❌ [卷${ch.volumeIndex}] ${ch.title} 获取失败`, 'error'); } } const progress = Math.round(((i + batch.length) / total) * 100); const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = elapsed > 0 ? (successCount + failCount) / elapsed : 0; this.updateProgress(progress, `下载中... (${i + batch.length}/${total})`, successCount, failCount, speed); if (consecutiveFails >= maxFail) { this.log(`⚠️ 连续失败${maxFail}次,停止下载`, 'error'); break; } if (delayTime > 0 && i + concurrency < chaptersToDownload.length) { await Utils.delay(delayTime); } } } results.sort((a, b) => { if (a.volumeIndex !== b.volumeIndex) return a.volumeIndex - b.volumeIndex; return a.chapterOrder - b.chapterOrder; }); this.downloadTxt(results, bookName, bookId); this.setDownloadingState(false); } async downloadWithSingleApi(chapter, bookName, apiType = 'primary') { return new Promise((resolve) => { const url = apiType === 'primary' ? `https://tt.sjmyzq.cn/api/raw_full?item_id=${chapter.itemId}` : `https://20071006.xyz/api/content?item_id=${chapter.itemId}&tab=小说`; GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 30000, onload: (response) => { try { const data = Utils.safeJSONParse(response.responseText); const content = this.parseChapterContent(data, apiType); resolve({ success: !!content, content: content }); } catch (e) { resolve({ success: false, content: null }); } }, onerror: () => resolve({ success: false, content: null }), ontimeout: () => resolve({ success: false, content: null }) }); }); } parseChapterContent(data, apiType) { let content = null; // 主接口和备用接口格式相同: code: 200, data.content if (data?.code === 200 && data.data && data.data.content) { content = data.data.content; } if (!content) return null; // 清理HTML标签 content = content.replace(/]*>.*?<\/h1>/gi, ''); content = content.replace(/]*>/gi, '

'); const paragraphs = content.match(/

.*?<\/p>/g); if (paragraphs) { content = paragraphs .map(p => p.replace(/<[^>]*>/g, '').trim()) .join('\n'); } return content; } downloadTxt(results, bookName, bookId) { const lines = []; const encoding = this.cache.elements.fanqieEncoding?.value || 'utf-8'; lines.push(`《${bookName}》`); lines.push(`下载时间: ${new Date().toLocaleString()}`); lines.push(''); let currentVolume = 0; for (const item of results) { if (item.volumeTitle && item.volumeIndex !== currentVolume) { currentVolume = item.volumeIndex; lines.push(''); lines.push(`=== ${item.volumeTitle} ===`); lines.push(''); } lines.push(''); lines.push(item.title); lines.push(''); lines.push(item.content); lines.push(''); } const content = lines.join('\n'); let blob; if (encoding === 'gbk') { const gbkEncoder = new TextEncoder('gbk'); blob = new Blob([gbkEncoder.encode(content)], { type: 'text/plain;charset=gbk' }); } else if (encoding === 'utf-16') { blob = new Blob([content], { type: 'text/plain;charset=utf-16' }); } else { blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); } const safeBookName = bookName.replace(/[\\/:*?"<>|]/g, '_'); saveAs(blob, `${safeBookName}.txt`); this.log('💾 文件已下载: ' + `${safeBookName}.txt`, 'success'); } } new ContentReplacer(); new FanqieDownloader(); })();