// ==UserScript== // @name 番茄小说批量下载器 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 在番茄小说搜索页面添加批量下载功能,支持高并发下载 // @author You // @match https://fanqienovel.com/search* // @match https://fanqienovel.com/page/* // @match https://fanqienovel.com/reader/* // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @connect tt.sjmyzq.cn // @connect fanqienovel.com // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 样式定义 const styles = ` .fanqie-downloader-panel { position: fixed; top: 100px; right: 20px; width: 360px; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; overflow: hidden; } .fanqie-downloader-header { background: linear-gradient(135deg, #ff6b6b, #ff8e8e); color: white; padding: 15px 20px; font-size: 16px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; } .fanqie-downloader-header button { background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 14px; } .fanqie-downloader-body { padding: 15px 20px; max-height: 500px; overflow-y: auto; } .fanqie-downloader-body.hidden { display: none; } .fanqie-input-group { margin-bottom: 12px; } .fanqie-input-group label { display: block; font-size: 13px; color: #666; margin-bottom: 5px; } .fanqie-input-group input, .fanqie-input-group select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; } .fanqie-input-row { display: flex; gap: 10px; } .fanqie-input-row .fanqie-input-group { flex: 1; } .fanqie-btn { width: 100%; padding: 10px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; margin-bottom: 8px; transition: all 0.3s; } .fanqie-btn-primary { background: #ff6b6b; color: white; } .fanqie-btn-primary:hover { background: #ff5252; } .fanqie-btn-primary:disabled { background: #ccc; cursor: not-allowed; } .fanqie-btn-secondary { background: #f0f0f0; color: #333; } .fanqie-btn-secondary:hover { background: #e0e0e0; } .fanqie-progress { margin-top: 15px; padding: 10px; background: #f8f8f8; border-radius: 6px; } .fanqie-progress-bar { height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden; margin-bottom: 8px; } .fanqie-progress-fill { height: 100%; background: linear-gradient(90deg, #ff6b6b, #ff8e8e); transition: width 0.3s; width: 0%; } .fanqie-progress-text { font-size: 12px; color: #666; display: flex; justify-content: space-between; } .fanqie-log { margin-top: 10px; 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; } .fanqie-log-error { color: #ff6b6b; } .fanqie-log-success { color: #69f0ae; } .fanqie-log-info { color: #64b5f6; } .fanqie-book-list { margin-top: 10px; max-height: 200px; overflow-y: auto; } .fanqie-book-item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; cursor: pointer; } .fanqie-book-item:hover { background: #f5f5f5; } .fanqie-book-item input { margin-right: 8px; } .fanqie-book-item img { width: 40px; height: 56px; object-fit: cover; border-radius: 4px; margin-right: 10px; } .fanqie-book-info { flex: 1; } .fanqie-book-title { font-size: 13px; font-weight: 500; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .fanqie-book-author { font-size: 11px; color: #999; } .fanqie-download-btn { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; background: #ff6b6b; color: white; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; margin-left: 10px; } .fanqie-download-btn:hover { background: #ff5252; } .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; } `; // 添加样式 function addStyles() { const styleEl = document.createElement('style'); styleEl.textContent = styles; document.head.appendChild(styleEl); } // 创建面板 function createPanel() { const panel = document.createElement('div'); panel.className = 'fanqie-downloader-panel'; panel.innerHTML = `
📚 番茄小说下载器
`; document.body.appendChild(panel); // 绑定事件 document.getElementById('fanqieToggle').addEventListener('click', togglePanel); document.getElementById('fanqieFetchBtn').addEventListener('click', fetchChapterList); document.getElementById('fanqieDownloadCurrent').addEventListener('click', downloadCurrentBook); // 自动检测当前页面小说 setTimeout(detectCurrentPageBook, 1000); } // 切换面板显示 function togglePanel() { const body = document.getElementById('fanqieBody'); const btn = document.getElementById('fanqieToggle'); if (body.classList.contains('hidden')) { body.classList.remove('hidden'); btn.textContent = '−'; } else { body.classList.add('hidden'); btn.textContent = '+'; } } // 日志功能 function log(message, type = 'info') { const logDiv = document.getElementById('fanqieLog'); const entry = document.createElement('div'); entry.className = `fanqie-log-entry fanqie-log-${type}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logDiv.appendChild(entry); logDiv.scrollTop = logDiv.scrollHeight; } // 更新进度 function updateProgress(percent, status, successCount, failCount, speed) { document.getElementById('fanqieProgressFill').style.width = percent + '%'; document.getElementById('fanqieProgressPercent').textContent = percent + '%'; if (status) { document.getElementById('fanqieProgressStatus').textContent = status; } if (successCount !== undefined) { document.getElementById('fanqieSuccessCount').textContent = `成功: ${successCount}`; } if (failCount !== undefined) { document.getElementById('fanqieFailCount').textContent = `失败: ${failCount}`; } if (speed !== undefined) { document.getElementById('fanqieSpeed').textContent = `速度: ${speed.toFixed(1)}章/秒`; } } // 显示进度面板 function showProgress() { document.getElementById('fanqieProgress').style.display = 'block'; } // 隐藏进度面板 function hideProgress() { document.getElementById('fanqieProgress').style.display = 'none'; } // 提取book_id function extractBookId(input) { if (!input) return null; // 如果是URL,提取ID const match = input.match(/book_id=(\d+)/); if (match) return match[1]; // 匹配 /page/xxxx 格式 const pageMatch = input.match(/\/page\/(\d+)/); if (pageMatch) return pageMatch[1]; // 匹配 /reader/xxxx 格式 const readerMatch = input.match(/\/reader\/(\d+)/); if (readerMatch) return readerMatch[1]; // 如果是纯数字,直接返回 if (/^\d+$/.test(input)) return input; return null; } // 检测当前页面小说 function detectCurrentPageBook() { const url = window.location.href; const bookId = extractBookId(url); if (bookId) { currentBookId = bookId; const bookNameEl = document.querySelector('h1, .book-title, [class*="title"]'); currentBookName = bookNameEl ? bookNameEl.textContent.trim() : null; document.getElementById('fanqieCurrentBook').style.display = 'block'; document.getElementById('fanqieCurrentBookInfo').textContent = `${currentBookName || '未知书名'} (ID: ${bookId})`; document.getElementById('fanqieDownloadCurrent').style.display = 'block'; document.getElementById('fanqieBookId').value = bookId; log(`✅ 检测到当前页面小说: ${currentBookName || '未知书名'} (${bookId})`, 'success'); } } let currentBookId = null; let currentBookName = null; // 下载当前页面小说 function downloadCurrentBook() { if (currentBookId) { document.getElementById('fanqieBookId').value = currentBookId; fetchChapterList(); } } // 获取章节列表 async function fetchChapterList() { const input = document.getElementById('fanqieBookId').value.trim(); const bookId = extractBookId(input); if (!bookId) { alert('请输入有效的小说ID或链接'); return; } const btn = document.getElementById('fanqieFetchBtn'); btn.disabled = true; btn.textContent = '获取中...'; showProgress(); log(`开始获取小说 ${bookId} 的章节列表...`); const url = `https://tt.sjmyzq.cn/api/book?book_id=${bookId}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { try { const data = JSON.parse(response.responseText); // 调试:输出数据结构到日志 log('API返回数据结构: ' + JSON.stringify(Object.keys(data)), 'info'); if (data.data) { log('data.data 字段: ' + JSON.stringify(Object.keys(data.data)), 'info'); } let chapterListWithVolume = null; let bookInfo = null; if (data.code === 200 && data.data) { // 尝试多种可能的数据结构 if (data.data.chapterListWithVolume) { chapterListWithVolume = data.data.chapterListWithVolume; bookInfo = data.data.bookInfo || data.data; } else if (data.data.data && data.data.data.chapterListWithVolume) { chapterListWithVolume = data.data.data.chapterListWithVolume; bookInfo = data.data.data.bookInfo || data.data.data; } // 如果bookInfo仍然没有书名,尝试其他字段 if (!bookInfo || (!bookInfo.bookName && !bookInfo.name)) { // 尝试从data.data直接获取 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.data) { if (data.data.data.bookName) { bookInfo = { ...bookInfo, bookName: data.data.data.bookName }; } else if (data.data.data.name) { bookInfo = { ...bookInfo, name: data.data.data.name }; } } } // 尝试更多可能的书名字段 if (bookInfo) { log('bookInfo 字段: ' + JSON.stringify(Object.keys(bookInfo)), 'info'); } } if (chapterListWithVolume && chapterListWithVolume.length > 0) { // 保存卷信息用于后续格式化 const volumes = []; let globalChapterIndex = 0; for (let vIndex = 0; vIndex < chapterListWithVolume.length; vIndex++) { const volume = chapterListWithVolume[vIndex]; const volumeChapters = []; for (const ch of volume) { volumeChapters.push({ itemId: ch.itemId, title: ch.title, realChapterOrder: ch.realChapterOrder, globalIndex: globalChapterIndex++ }); } // 只在API返回了卷名时才使用 const apiVolumeTitle = volume[0]?.volumeTitle; volumes.push({ volumeIndex: vIndex + 1, volumeTitle: apiVolumeTitle, // 可能为undefined chapters: volumeChapters }); } // 计算总章节数 const totalChapters = volumes.reduce((sum, v) => sum + v.chapters.length, 0); // 获取书名 - 优先使用页面检测到的书名 let bookName = null; // 如果当前下载的是页面检测到的书,使用页面获取的书名 if (currentBookId === bookId && currentBookName) { bookName = currentBookName; log(`使用页面检测到的书名: ${bookName}`, 'info'); } else { // 否则从API返回数据中尝试获取 if (bookInfo) { bookName = bookInfo.bookName || bookInfo.name || bookInfo.title || bookInfo.book_name || bookInfo.novelName || bookInfo.novel_name; } // 如果还是空,尝试从第一个卷信息中获取 if (!bookName && chapterListWithVolume[0] && chapterListWithVolume[0][0]) { const firstCh = chapterListWithVolume[0][0]; if (firstCh.bookName) bookName = firstCh.bookName; else if (firstCh.book_name) bookName = firstCh.book_name; } } // 如果还是空,使用默认值 if (!bookName) { bookName = '未知书名'; } log(`✅ 成功获取《${bookName}》共 ${volumes.length} 卷 ${totalChapters} 章`, 'success'); document.getElementById('fanqieEnd').value = totalChapters; // 开始下载 startDownload(bookId, bookName, volumes, totalChapters); } else { log('❌ 获取章节列表失败: 数据结构异常', 'error'); btn.disabled = false; btn.textContent = '获取章节列表'; } } catch (e) { log('❌ 解析响应失败: ' + e.message, 'error'); btn.disabled = false; btn.textContent = '获取章节列表'; } }, onerror: function() { log('❌ 请求失败', 'error'); btn.disabled = false; btn.textContent = '获取章节列表'; } }); } // 获取单章内容 function fetchChapterContent(itemId) { return new Promise((resolve) => { const url = `https://tt.sjmyzq.cn/api/raw_full?item_id=${itemId}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 200 && data.data && data.data.content) { let content = data.data.content .replace(/]*>.*?<\/h1>/, '') .match(/]*>.*?<\/p>/g) ?.map(p => p.replace(/<[^>]*>/g, '').trim()) .join('\n') || data.data.content; resolve({ success: true, content }); } else { resolve({ success: false, content: null }); } } catch (e) { resolve({ success: false, content: null }); } }, onerror: function() { resolve({ success: false, content: null }); } }); }); } // 开始下载 - 支持高并发 async function startDownload(bookId, bookName, volumes, totalChapters) { const startIndex = parseInt(document.getElementById('fanqieStart').value) - 1; let endIndex = parseInt(document.getElementById('fanqieEnd').value) - 1; if (isNaN(endIndex) || endIndex < 0) endIndex = totalChapters - 1; const concurrency = parseInt(document.getElementById('fanqieConcurrency').value) || 10; const delayTime = parseInt(document.getElementById('fanqieDelay').value) || 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(); log(`📥 开始下载《${bookName}》: 共${total}章, 并发${concurrency}, 延迟${delayTime}ms`); // 并发下载函数 async function downloadBatch(batch) { const promises = batch.map(async (ch) => { const result = await fetchChapterContent(ch.itemId); return { ch, result }; }); return await Promise.all(promises); } // 分批次下载 for (let i = 0; i < chaptersToDownload.length; i += concurrency) { const batch = chaptersToDownload.slice(i, i + concurrency); const batchResults = await downloadBatch(batch); for (const { ch, result } of batchResults) { if (result.success) { results.push({ volumeIndex: ch.volumeIndex, volumeTitle: ch.volumeTitle, chapterOrder: ch.realChapterOrder, title: ch.title, content: result.content, globalIndex: ch.globalIndex }); successCount++; consecutiveFails = 0; log(`✅ [卷${ch.volumeIndex}] ${ch.title}`, 'success'); } else { failCount++; consecutiveFails++; log(`❌ [卷${ch.volumeIndex}] ${ch.title} 获取失败`, 'error'); if (consecutiveFails >= maxFail) { log(`⚠️ 连续${maxFail}章失败,停止下载`, 'error'); break; } } } // 更新进度 const progress = Math.round(((i + batch.length) / total) * 100); const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = (successCount + failCount) / elapsed; updateProgress(progress, `下载中...`, successCount, failCount, speed); if (consecutiveFails >= maxFail) break; // 延迟 if (delayTime > 0 && i + concurrency < chaptersToDownload.length) { await new Promise(resolve => setTimeout(resolve, delayTime)); } } updateProgress(100, '下载完成', successCount, failCount, 0); log(`🎉 下载完成! 成功 ${successCount}/${total} 章`, 'success'); if (results.length > 0) { // 按卷和章节顺序排序 results.sort((a, b) => { if (a.volumeIndex !== b.volumeIndex) { return a.volumeIndex - b.volumeIndex; } return a.chapterOrder - b.chapterOrder; }); downloadTxt(results, bookName, bookId); } document.getElementById('fanqieFetchBtn').disabled = false; document.getElementById('fanqieFetchBtn').textContent = '获取章节列表'; } // 下载TXT文件 - 简洁格式,不过度修改 function downloadTxt(results, bookName, bookId) { const lines = []; // 简单的文件标题 lines.push(`《${bookName}》`); 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.originalTitle || item.title); lines.push(''); // 章节内容 lines.push(item.content); lines.push(''); } const content = lines.join('\n'); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); // 清理书名中的非法字符 const safeBookName = bookName.replace(/[\\/:*?"<>|]/g, '_'); const a = document.createElement('a'); a.href = url; a.download = `${safeBookName}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); log('💾 文件已下载: ' + `${safeBookName}.txt`, 'success'); } // 初始化 function init() { addStyles(); createPanel(); log('番茄小说下载器 v2.0 已加载', 'success'); log('支持并发下载(最大50)、自动检测页面小说', 'info'); } // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();