// ==UserScript==
// @name 番茄小说阅读助手-关关同学修复版
// @namespace http://tampermonkey.net/
// @version 3.3
// @description 修复书籍ID检测,正文直替+全文下载,基于oiapi接口
// @author Modified
// @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 fanqienovel.com
// @connect oiapi.net
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// ========== 接口配置 ==========
const API_URL = 'https://oiapi.net/api/FqRead';
const API_KEY = '';
const Utils = {
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// 修复:新增reader路径匹配,支持从阅读页URL直接提取书籍ID
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;
},
safeJSONParse(text, defaultValue = null) {
try {
return JSON.parse(text);
} catch (e) {
return defaultValue;
}
}
};
// ========== 正文自动替换模块 ==========
class ContentReplacer {
constructor() {
this.currentURL = window.location.href;
this.chapterCache = null;
this.cacheBookId = null;
this.isReplacing = false;
this.init();
}
init() {
const pageType = this.detectPageType();
if (pageType === 'reader') {
this.setupReaderPage();
}
}
detectPageType() {
const url = window.location.href;
if (url.includes('/reader/')) return 'reader';
if (url.includes('/page/')) return 'page';
return null;
}
setupReaderPage() {
setTimeout(() => this.replaceReaderContent(), 1500);
// 监听章节切换
setInterval(() => {
if (window.location.href !== this.currentURL) {
this.currentURL = window.location.href;
setTimeout(() => this.replaceReaderContent(), 1000);
}
}, 1000);
}
// 获取章节列表(带缓存)
fetchChapterList(bookId) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `${API_URL}?key=${API_KEY}&method=chapters&id=${bookId}`,
timeout: 30000,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data && data.data && Array.isArray(data.data)) {
resolve(data.data);
} else {
reject(new Error('章节列表格式异常'));
}
} catch (e) {
reject(e);
}
},
onerror: reject
});
});
}
// 增强:多层级提取书籍ID
getCurrentBookId() {
// 1. 优先从URL直接提取
const urlId = Utils.extractBookId(window.location.href);
if (urlId) return urlId;
// 2. 从页面返回链接提取
const bookLink = document.querySelector('a[href*="/page/"]');
if (bookLink) {
const linkId = Utils.extractBookId(bookLink.getAttribute('href'));
if (linkId) return linkId;
}
// 3. 兜底:从页面全局数据提取
if (window.__INITIAL_STATE__) {
try {
const state = window.__INITIAL_STATE__;
const stateId = state?.reader?.bookId || state?.page?.bookId || state?.book?.id;
if (stateId) return String(stateId);
} catch(e) {}
}
return null;
}
async replaceReaderContent() {
if (this.isReplacing) return;
const contentDiv = document.querySelector('.muye-reader-content.noselect, .muye-reader-content');
if (!contentDiv) return;
const bookId = this.getCurrentBookId();
if (!bookId) return;
// 获取当前章节标题
const titleEl = document.querySelector('.muye-reader-title');
if (!titleEl) return;
const currentTitle = titleEl.innerText.trim().replace(/\s+/g, '');
this.isReplacing = true;
try {
// 缓存命中判断
if (!this.chapterCache || this.cacheBookId !== bookId) {
this.chapterCache = await this.fetchChapterList(bookId);
this.cacheBookId = bookId;
}
// 匹配当前章节序号
const currentChapter = this.chapterCache.find(ch => {
const chTitle = ch.chapter_title.replace(/\s+/g, '');
return chTitle === currentTitle || currentTitle.includes(chTitle) || chTitle.includes(currentTitle);
});
if (!currentChapter) {
this.isReplacing = false;
return;
}
// 请求解析正文
GM_xmlhttpRequest({
method: 'GET',
url: `${API_URL}?key=${API_KEY}&method=chapter&id=${bookId}&chapter=${currentChapter.chapter}`,
timeout: 30000,
onload: (response) => {
try {
const data = Utils.safeJSONParse(response.responseText);
if (data?.data && Array.isArray(data.data) && data.data[0]?.content) {
let content = data.data[0].content;
// 按换行拆分为段落
const paragraphs = content.split(/\n+/)
.map(line => line.trim() ? `
${line.trim()}
` : '')
.join('\n');
// 保留容器本身,只替换内部内容
if (contentDiv.classList.length > 1) {
contentDiv.classList = contentDiv.classList[0];
}
contentDiv.innerHTML = paragraphs;
contentDiv.classList.remove('noselect');
// ========== 移除所有遮挡 ==========
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.classList.remove('serial-filter-gray');
}
contentDiv.style.filter = '';
contentDiv.style.userSelect = 'text';
// 清除遮罩类元素
const masks = contentDiv.querySelectorAll('[class*="mask"], [class*="blur"], [class*="lock"]');
masks.forEach(el => el.remove());
document.title = document.title.replace(/在线免费阅读_番茄小说官网$/, '');
console.log('[FanqieEnhancer] 正文替换成功');
}
} catch (e) {
console.error('[FanqieEnhancer] 正文替换失败:', e);
} finally {
this.isReplacing = false;
}
},
onerror: () => {
this.isReplacing = false;
console.error('[FanqieEnhancer] 正文请求失败');
}
});
} catch (err) {
this.isReplacing = false;
console.error('[FanqieEnhancer] 章节匹配失败:', err);
}
}
}
// ========== 全文下载模块 ==========
class FanqieDownloader {
constructor() {
this.config = {
maxLogEntries: 100,
version: '3.3'
};
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(), 1500);
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'
];
ids.forEach(id => {
this.cache.elements[id] = document.getElementById(id);
});
}
addStyles() {
const styles = `
.muye-reader-content.noselect::after,
.muye-reader-content.noselect::before {
display: none !important;
}
.muye-to-vip,
.muye-to-vip-mask,
.muye-to-fanqie,
.reader-unlock,
.reader-lock-overlay,
[class*="vip"],
[class*="lock"],
[class*="mask"],
[class*="blur"] {
display: none !important;
}
.fanqie-downloader-panel {
position: fixed;
top: 160px;
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-header {
background: linear-gradient(135deg, #2196F3, #42A5F5);
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-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-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: #2196F3;
color: white;
}
.fanqie-btn-primary:hover:not(:disabled) {
background: #1976D2;
}
.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, #2196F3, #42A5F5);
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: #e3f2fd;
border: 1px solid #2196F3;
border-radius: 6px;
padding: 10px;
margin-bottom: 12px;
}
.fanqie-current-book-title {
font-weight: 600;
color: #1976D2;
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;
cursor: pointer;
user-select: none;
}
`;
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 = `
准备中...
0%
成功: 0
失败: 0
速度: 0章/秒
⚙️ 下载设置
▲
`;
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';
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;
});
}
// 保存设置
['fanqieConcurrency', 'fanqieEncoding'].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;
panel.style.left = startLeft + (e.clientX - startX) + 'px';
panel.style.top = startTop + (e.clientY - startY) + 'px';
panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
isDragging = false;
panel.style.transition = '';
});
}
saveSettings() {
const settings = {
concurrency: this.cache.elements.fanqieConcurrency?.value || '5',
encoding: this.cache.elements.fanqieEncoding?.value || 'utf-8'
};
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;
} 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';
}
}
// 修复:增强书籍ID检测逻辑,三层兜底
detectCurrentPageBook() {
try {
const url = window.location.href;
let bookId = null;
let bookName = null;
// 1. 优先从URL直接提取(阅读页/主页通用)
bookId = Utils.extractBookId(url);
// 2. 从DOM元素提取
if (!bookId) {
const bookLink = document.querySelector('a[href*="/page/"]');
if (bookLink) {
bookId = Utils.extractBookId(bookLink.getAttribute('href'));
}
}
// 3. 兜底从页面全局数据提取
if (!bookId && window.__INITIAL_STATE__) {
try {
const state = window.__INITIAL_STATE__;
bookId = state?.reader?.bookId || state?.page?.bookId || state?.book?.id;
if (bookId) bookId = String(bookId);
} catch(e) {}
}
// 提取书名
const titleEl = document.querySelector('.muye-reader-title, h1, .book-title');
if (titleEl) bookName = titleEl.textContent.trim();
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.fanqieBookId) {
this.cache.elements.fanqieBookId.value = bookId;
}
this.log(`✅ 检测到小说: ${bookName || '未知书名'} (${bookId})`, 'success');
} else {
this.log('⚠️ 未能检测到书籍ID,请手动输入', 'warning');
this.log('💡 手动方法:打开小说主页,复制地址栏/page/后面的数字', 'info');
}
} catch (error) {
this.log(`检测页面小说失败: ${error.message}`, 'error');
}
}
downloadCurrentBook() {
if (this.state.currentBookId) {
this.cache.elements.fanqieBookId.value = 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 ? '⏳ 下载中...' : '📥 下载全本TXT';
}
}
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或 /page/ 链接');
return;
}
this.state.abortController = new AbortController();
this.setDownloadingState(true);
this.showProgress();
this.log(`开始获取小说 ${bookId} 的章节列表...`);
GM_xmlhttpRequest({
method: 'GET',
url: `${API_URL}?key=${API_KEY}&method=chapters&id=${bookId}`,
timeout: 30000,
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) {
let chapterListWithVolume = null;
let bookInfo = null;
// 适配oiapi返回格式
if (data && data.data && Array.isArray(data.data) && data.data.length > 0) {
chapterListWithVolume = [data.data]; // 单卷结构
bookInfo = { bookName: data.data[0]?.title || '未知书名' };
this.log(`成功获取章节列表,共 ${data.data.length} 章`, '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 = [];
if (!Array.isArray(volume)) continue;
for (const ch of volume) {
const chapterNum = ch.chapter || globalChapterIndex + 1;
const title = ch.chapter_title || `第${chapterNum}章`;
volumeChapters.push({
chapter: chapterNum,
title: title,
realChapterOrder: chapterNum,
globalIndex: globalChapterIndex++
});
}
const volumeTitle = volume[0]?.volume_name || volume[0]?.volumeTitle || `第${vIndex+1}卷`;
if (volumeChapters.length > 0) {
volumes.push({
volumeIndex: vIndex + 1,
volumeTitle: volumeTitle,
chapters: volumeChapters
});
}
}
const totalChapters = volumes.reduce((sum, v) => sum + v.chapters.length, 0);
let bookName = bookInfo?.bookName || '未知书名';
if (this.state.currentBookId === bookId && this.state.currentBookName) {
bookName = this.state.currentBookName;
}
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 concurrency = Math.min(parseInt(this.cache.elements.fanqieConcurrency?.value) || 5, 30);
const delayTime = 200;
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();
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.downloadChapter(bookId, ch).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;
} 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, `下载中... (${Math.min(i + batch.length, total)}/${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 downloadChapter(bookId, chapter) {
return new Promise((resolve) => {
const url = `${API_URL}?key=${API_KEY}&method=chapter&id=${bookId}&chapter=${chapter.chapter}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 30000,
onload: (response) => {
try {
const data = Utils.safeJSONParse(response.responseText);
const content = this.parseChapterContent(data);
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) {
let content = null;
// 适配oiapi返回格式:data.data[0].content
if (data && data.data && Array.isArray(data.data) && data.data[0]?.content) {
content = data.data[0].content;
}
if (!content) return null;
// 移除正文开头重复的章节标题
content = content.replace(/^第[0-9零一二三四五六七八九十百千]+章.*?\n/, '');
return content.trim();
}
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 {
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();
})();