// ==UserScript== // @name 番茄小说下载器 // @author 尘۝醉 // @version 2025.09.07.17 // @description 番茄小说下载 // @description:zh-cn 番茄小说下载 // @description:en Fanqienovel Downloader // @license MIT // @match https://fanqienovel.com/page/* // @match https://changdunovel.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js // @icon https://img.onlinedown.net/download/202102/152723-601ba1db7a29e.jpg // @grant GM_xmlhttpRequest // @grant GM_addStyle // @namespace https://github.com/tampermonkey // @downloadURL https://update.greasyfork.icu/scripts/534014/%E7%95%AA%E8%8C%84%E5%B0%8F%E8%AF%B4%E4%B8%8B%E8%BD%BD%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/534014/%E7%95%AA%E8%8C%84%E5%B0%8F%E8%AF%B4%E4%B8%8B%E8%BD%BD%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; // 检查是否为书籍信息页面 let bookId = null; // 检查fanqienovel.com的页面 const fanqienovelMatch = window.location.pathname.match(/^\/page\/(\d+)$/); if (fanqienovelMatch) { bookId = fanqienovelMatch[1]; } // 检查changdunovel.com的页面 if (!bookId && window.location.hostname === 'changdunovel.com') { const changdunovelMatch = window.location.href.match(/book_id=(\d{19})/); if (changdunovelMatch) { bookId = changdunovelMatch[1]; } } if (!bookId) { console.log('番茄小说下载器: 当前页面不是书籍信息页面,不显示下载按钮'); return; } // 常量定义 const BATCH_SIZE = 30; // 批量请求的章节数量 // EPUB模板 const EPUB_TEMPLATES = { MIMETYPE: 'application/epub+zip', CONTAINER: ` ` }; // 界面样式 GM_addStyle(` .tamper-container { position: fixed; top: 220px; right: 20px; background: #fff; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 15px; z-index: 9999; width: 200px; font-size: 14px; line-height: 1.3 } .tamper-button { background: #ff6b00; color: #fff; border: none; border-radius: 20px; padding: 10px 20px; margin: 5px 0; cursor: pointer; font-size: 14px; font-weight: bold; transition: all 0.2s; width: 100%; text-align: center } .tamper-button:hover { background: #ff5500 } .tamper-button:disabled { background: #ccc; cursor: not-allowed } .tamper-button.txt { background: #4CAF50; } .tamper-button.epub { background: #2196F3; } .stats-container { display: flex; justify-content: space-between; margin-top: 15px; font-size: 12px } .stat-item { display: flex; flex-direction: column; align-items: center; flex: 1; padding: 5px } .stat-label { margin-bottom: 5px; color: #666 } .stat-value { font-weight: bold; font-size: 16px } .total-value { color: #333 } .success-value { color: #4CAF50 } .failed-value { color: #F44336 } .tamper-notification { position: fixed; bottom: 40px; right: 40px; background-color: #4CAF50; color: white; padding: 30px; border-radius: 10px; box-shadow: 0 8px 16px rgba(0,0,0,0.2); z-index: 9999; font-size: 28px; animation: fadeIn 0.5s; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .progress-bar { width: 100%; height: 10px; background-color: #f0f0f0; border-radius: 5px; margin-top: 10px; overflow: hidden; } .progress-fill { height: 100%; background-color: #4CAF50; transition: width 0.3s ease; } `); // 辅助函数 function decodeHtmlEntities(str) { const entities={'"':'"',''':"'",'&':'&','<':'<','>':'>'}; return str.replace(/"|'|&|<|>/g, match => entities[match]); } function sanitizeFilename(name) { return name.replace(/[\\/*?:"<>|]/g, '').trim(); } function showNotification(message, isSuccess = true) { const notification = document.createElement('div'); notification.className = 'tamper-notification'; notification.style.cssText = `position:fixed;bottom:40px;right:40px;background-color:${isSuccess ? '#4CAF50' : '#F44336'};color:white;padding:30px;border-radius:10px;box-shadow:0 8px 16px rgba(0,0,0,0.2);z-index:9999;font-size:28px;animation:fadeIn 0.5s`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => notification.remove(), 500); }, 3000); return notification; } function formatContent(content) { let decoded = decodeHtmlEntities(content); return decoded.replace(/

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

/g,'').replace(//g,'\n').replace(/<\/p>/g,'\n').replace(/<[^>]+>/g,'').replace(/^\s+|\s+$/g,'').replace(/\n{3,}/g, '\n'); } function createDownloadUI() { const container = document.createElement('div'); container.className = 'tamper-container'; const txtBtn = document.createElement('button'); txtBtn.className = 'tamper-button txt'; txtBtn.textContent = '下载TXT'; container.appendChild(txtBtn); const epubBtn = document.createElement('button'); epubBtn.className = 'tamper-button epub'; epubBtn.textContent = '下载EPUB'; epubBtn.style.marginTop = '10px'; container.appendChild(epubBtn); // 添加进度条 const progressContainer = document.createElement('div'); progressContainer.style.marginTop = '10px'; progressContainer.style.display = 'none'; const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; const progressFill = document.createElement('div'); progressFill.className = 'progress-fill'; progressFill.style.width = '0%'; progressBar.appendChild(progressFill); progressContainer.appendChild(progressBar); container.appendChild(progressContainer); const statsContainer = document.createElement('div'); statsContainer.className = 'stats-container'; const totalStat = document.createElement('div'); totalStat.className = 'stat-item'; totalStat.innerHTML = `

总章节
0
`; const successStat = document.createElement('div'); successStat.className = 'stat-item'; successStat.innerHTML = `
成功
0
`; const failedStat = document.createElement('div'); failedStat.className = 'stat-item'; failedStat.innerHTML = `
失败
0
`; statsContainer.appendChild(totalStat); statsContainer.appendChild(successStat); statsContainer.appendChild(failedStat); container.appendChild(statsContainer); document.body.appendChild(container); return { container, txtBtn, epubBtn, progressContainer, progressFill, updateStats: (total, success, failed) => { totalStat.querySelector('.stat-value').textContent = total; successStat.querySelector('.stat-value').textContent = success; failedStat.querySelector('.stat-value').textContent = failed; }, updateProgress: (percentage) => { progressFill.style.width = `${percentage}%`; }, showProgress: () => { progressContainer.style.display = 'block'; }, hideProgress: () => { progressContainer.style.display = 'none'; } }; } async function getBookInfo(bookId) { const url = `https://i.snssdk.com/reading/bookapi/multi-detail/v/?aid=1967&book_id=${bookId}`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { 'User-Agent': 'okhttp/4.9.3' }, onload: resolve, onerror: reject, timeout: 8000 }); }); if (response.status !== 200) throw new Error(`HTTP ${response.status}`); const data = JSON.parse(response.responseText); if (!data.data || !data.data[0]) throw new Error('未获取到书籍信息'); const book = data.data[0]; return { title: sanitizeFilename(book.book_name), author: sanitizeFilename(book.author), abstract: book.abstract, wordCount: book.word_number, chapterCount: book.serial_count, thumb_url: book.thumb_url, infoText: `书名:${book.book_name}\n作者:${book.author}\n字数:${parseInt(book.word_number)/10000}万字\n章节数:${book.serial_count}\n简介:${book.abstract}\n免责声明:本小说下载器仅为个人学习、研究或欣赏目的提供便利,下载的小说版权归原作者及版权方所有。若因使用本下载器导致任何版权纠纷或法律问题,使用者需自行承担全部责任。` }; } async function getChapters(bookId) { const url = `https://fanqienovel.com/api/reader/directory/detail?bookId=${bookId}`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { 'User-Agent': 'okhttp/4.9.3' }, onload: resolve, onerror: reject, timeout: 8000 }); }); if (response.status !== 200) throw new Error(`HTTP ${response.status}`); const text = response.responseText; const chapterListMatch = text.match(/"chapterListWithVolume":\[(.*?)\]]/); if (!chapterListMatch) throw new Error('未找到章节列表'); const chapterListStr = chapterListMatch[1]; const itemIds = chapterListStr.match(/"itemId":"(.*?)"/g).map(m => m.match(/"itemId":"(.*?)"/)[1]); const titles = chapterListStr.match(/"title":"(.*?)"/g).map(m => m.match(/"title":"(.*?)"/)[1]); return itemIds.map((id, index) => ({ id: id, title: titles[index] || `第${index+1}章` })); } async function downloadChaptersBatch(chapterIds) { try { const url = `http://localhost:9999/batch_full?item_ids=${chapterIds.join(',')}`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { 'User-Agent': 'okhttp/4.9.3' }, onload: resolve, onerror: reject, timeout: 30000 // 增加超时时间,因为批量请求可能更耗时 }); }); if (response.status !== 200) throw new Error(`HTTP ${response.status}`); const data = JSON.parse(response.responseText); if (!data || !data.data) throw new Error('无效的响应格式'); return data.data.map(chapter => ({ title: chapter.title, content: formatContent(chapter.content || ''), success: true })); } catch (error) { console.error(`批量下载章节失败:`, error); // 返回失败的结果 return chapterIds.map(() => ({ title: '未知章节', content: '[下载失败]', success: false })); } } /* global JSZip */ async function generateEPUB(bookInfo, chapters, contents, coverUrl) { const zip = new JSZip(); const uuid = URL.createObjectURL(new Blob([])).split('/').pop(); const now = new Date().toISOString().replace(/\.\d+Z$/, 'Z'); // 1. 必须包含的文件 zip.file('mimetype', EPUB_TEMPLATES.MIMETYPE, { compression: 'STORE' }); // 2. 容器文件 const metaInf = zip.folder('META-INF'); metaInf.file('container.xml', EPUB_TEMPLATES.CONTAINER); // 3. 内容文件夹 const oebps = zip.folder('OEBPS'); // 创建Text文件夹 const textFolder = oebps.folder('Text'); // 4. CSS样式(增强阅读体验) const cssContent = `body { font-family: "Microsoft Yahei", serif; line-height: 1.8; margin: 2em auto; padding: 0 20px; color: #333; text-align: justify; background-color: #f8f4e8; } h1 { font-size: 1.4em; margin: 1.2em 0; color: #0057BD; } h2 { font-size: 1.0em; margin: 0.8em 0; color: #0057BD; } .pic { margin: 50% 30% 0 30%; padding: 2px 2px; border: 1px solid #f5f5dc; background-color: rgba(250,250,250, 0); border-radius: 1px; } p { text-indent: 2em; margin: 0.8em 0; hyphens: auto; } .book-info { margin: 1em 0; padding: 1em; background: #f8f8f8; border-radius: 5px; } .book-info p { text-indent: 0; }`; oebps.file('Styles/main.css', cssContent); // 5. 封面处理 let coverImage; if (coverUrl) { try { coverImage = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: coverUrl, responseType: 'blob', onload: (r) => resolve(r.response), onerror: reject }); }); oebps.file('Images/cover.jpg', coverImage, { binary: true }); // 生成封面页面 const coverHtml = ` 封面
${bookInfo.title}封面

${bookInfo.title}

${bookInfo.author}

`; textFolder.file('cover.html', coverHtml); } catch (e) { console.warn('封面下载失败:', e); } } // 6. 生成书籍信息页面 const infoHtml = ` 书籍信息

${bookInfo.title}

作者:${bookInfo.author}

字数:${parseInt(bookInfo.wordCount)/10000}万字

章节数:${bookInfo.chapterCount}

简介

${bookInfo.abstract.replace(/\n/g, '

')}

免责声明

本小说下载器仅为个人学习、研究或欣赏目的提供便利,下载的小说版权归原作者及版权方所有。若因使用本下载器导致任何版权纠纷或法律问题,使用者需自行承担全部责任。

`; textFolder.file('info.html', infoHtml); // 7. 生成章节文件 const manifestItems = [ '', '', coverImage ? '' : '', '', coverImage ? '' : '' ].filter(Boolean); const spineItems = [ coverImage ? '' : '', '' ]; const navPoints = []; // 生成章节内容 chapters.forEach((chapter, index) => { const filename = `chapter_${index}.html`; const safeContent = contents[index] .replace(//g, '>') .replace(/\n/g, '

'); const chapterContent = ` ${chapter.title}

${chapter.title}

${safeContent}

`; textFolder.file(filename, chapterContent); manifestItems.push(``); spineItems.push(``); // 生成导航点 navPoints.push(` ${chapter.title} `); }); // 8. 生成toc.ncx文件 const tocNcx = ` ${bookInfo.title} 封面 书籍信息 ${navPoints.join('\n ')} `; oebps.file('toc.ncx', tocNcx); // 9. 生成content.opf const contentOpf = ` urn:uuid:${uuid} ${bookInfo.title} ${bookInfo.author} zh-CN ${manifestItems.join('\n ')} ${spineItems.join('\n ')} `; oebps.file('content.opf', contentOpf); return await zip.generateAsync({ type: 'blob', mimeType: 'application/epub+zip', compression: 'DEFLATE', compressionOptions: { level: 9 } }); } // 主函数 async function main() { const ui = createDownloadUI(); let bookInfo, chapters; try { bookInfo = await getBookInfo(bookId); chapters = await getChapters(bookId); ui.updateStats(chapters.length, 0, 0); } catch (error) { showNotification('获取书籍信息失败: ' + error.message, false); return; } let isDownloading = false; let successCount = 0; let failedCount = 0; let contents = []; async function startDownload(format) { if (isDownloading) return; isDownloading = true; ui.txtBtn.disabled = true; ui.epubBtn.disabled = true; ui.showProgress(); successCount = 0; failedCount = 0; contents = Array(chapters.length).fill(''); showNotification('开始批量下载章节内容...'); // 分批下载章节 const batchCount = Math.ceil(chapters.length / BATCH_SIZE); for (let i = 0; i < batchCount; i++) { const startIndex = i * BATCH_SIZE; const endIndex = Math.min(startIndex + BATCH_SIZE, chapters.length); const batchChapters = chapters.slice(startIndex, endIndex); const chapterIds = batchChapters.map(ch => ch.id); try { const batchResults = await downloadChaptersBatch(chapterIds); // 处理批量结果 - 使用for循环避免函数作用域问题 for (let j = 0; j < batchResults.length; j++) { const result = batchResults[j]; const globalIndex = startIndex + j; contents[globalIndex] = result.content; if (result.success) { successCount++; } else { failedCount++; } } // 更新UI ui.updateStats(chapters.length, successCount, failedCount); ui.updateProgress(((i + 1) / batchCount) * 100); } catch (error) { console.error(`批量下载第 ${i + 1} 批章节失败:`, error); // 标记这一批章节为失败 for (let j = startIndex; j < endIndex; j++) { contents[j] = `[下载失败: ${chapters[j].title}]`; failedCount++; } ui.updateStats(chapters.length, successCount, failedCount); } } if (format === 'txt') { // 生成带章节标题的TXT内容 let txtContent = bookInfo.infoText + '\n\n'; for (let i = 0; i < chapters.length; i++) { txtContent += `${chapters[i].title}\n`; txtContent += `${contents[i]}\n\n`; } const blob = new Blob([txtContent], { type: 'text/plain;charset=utf-8' }); saveAs(blob, `${bookInfo.title}.txt`); } else if (format === 'epub') { try { const epubBlob = await generateEPUB(bookInfo, chapters, contents, bookInfo.thumb_url); /* global saveAs */ saveAs(epubBlob, `${bookInfo.title}.epub`); } catch (error) { showNotification('生成EPUB失败: ' + error.message, false); } } showNotification(`下载完成!成功: ${successCount}, 失败: ${failedCount}`); ui.txtBtn.disabled = false; ui.epubBtn.disabled = false; ui.hideProgress(); isDownloading = false; } ui.txtBtn.addEventListener('click', () => startDownload('txt')); ui.epubBtn.addEventListener('click', () => startDownload('epub')); } // 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();