// ==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.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();
}
})();