guanghulab/server/novel-db/app/services/download-engine.js

502 lines
15 KiB
JavaScript
Raw Permalink Normal View History

/**
*
* 下载引擎 · Download Engine Service
*
*
* 智库节点 Phase 2 · ZY-SVR-006
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 功能:
* - 接收下载任务书名+作者+平台
* - 任务队列管理排队/执行/完成/失败
* - TXT 文件存储到本地 data/books/
* - 下载完成后自动触发分章引擎
* - 开源小说下载器数据库匹配优先本地库匹配
*
* 支持平台:
* - qimao (七猫)
* - fanqie (番茄)
* - yuewen (阅文)
* - jjwxc (晋江)
*
* 开源数据源:
* - novel-downloader (GitHub: 404)
* - FreeNovel (GitHub: cnbattle/novel)
* - NovelHarvester (GitHub: unclezs/NovelHarvester)
* - biqugen / zxcs (知轩藏书)
* - 69shu / 笔趣阁系列
*
*
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const BOOKS_DIR = process.env.BOOKS_DIR || path.join(__dirname, '..', 'data', 'books');
const TASKS_FILE = path.join(__dirname, '..', 'data', 'download-tasks.json');
const SOURCES_FILE = path.join(__dirname, '..', 'data', 'book-sources.json');
const SUPPORTED_PLATFORMS = ['qimao', 'fanqie', 'yuewen', 'jjwxc'];
// ═══════════════════════════════════════════════════════════
// 开源小说下载器数据库注册表
// ═══════════════════════════════════════════════════════════
const OPEN_SOURCE_REGISTRIES = [
{
id: 'novel-downloader',
name: 'novel-downloader',
description: '全平台小说下载工具 · 支持多源聚合',
url: 'https://github.com/search?q=novel+downloader&type=repositories',
type: 'downloader',
platforms: ['qimao', 'fanqie', 'yuewen', 'jjwxc', 'qidian'],
status: 'registered',
book_count_estimate: 0,
last_checked: null
},
{
id: 'novel-harvester',
name: 'NovelHarvester (uncle小说)',
description: 'Java桌面端小说下载器 · 全平台采集',
url: 'https://github.com/unclezs/NovelHarvester',
type: 'downloader',
platforms: ['qimao', 'fanqie', 'yuewen', 'qidian', 'biquge'],
status: 'registered',
book_count_estimate: 0,
last_checked: null
},
{
id: 'free-novel',
name: 'FreeNovel (cnbattle)',
description: 'Go语言小说聚合 · 多源搜索下载',
url: 'https://github.com/cnbattle/novel',
type: 'aggregator',
platforms: ['biquge', '69shu', 'shuquge', 'xbiquge'],
status: 'registered',
book_count_estimate: 0,
last_checked: null
},
{
id: 'zxcs-archive',
name: '知轩藏书数据库',
description: '精校TXT小说归档 · 含大量完本',
url: 'https://zxcs.info',
type: 'archive',
platforms: ['zxcs'],
status: 'registered',
book_count_estimate: 0,
last_checked: null
},
{
id: 'biquge-series',
name: '笔趣阁系列源',
description: '笔趣阁及镜像站 · 免费小说资源',
url: 'https://www.biquge.com',
type: 'source',
platforms: ['biquge', 'xbiquge', 'ibiquge'],
status: 'registered',
book_count_estimate: 0,
last_checked: null
},
{
id: '69shu-archive',
name: '69书吧数据库',
description: '69书吧 · 大量TXT小说资源',
url: 'https://www.69shu.com',
type: 'source',
platforms: ['69shu'],
status: 'registered',
book_count_estimate: 0,
last_checked: null
}
];
// 本地书源索引 (缓存已知可匹配的书籍列表)
let bookSourceIndex = Object.create(null);
function loadBookSources() {
try {
if (fs.existsSync(SOURCES_FILE)) {
const raw = JSON.parse(fs.readFileSync(SOURCES_FILE, 'utf8'));
bookSourceIndex = Object.create(null);
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
for (const key of Object.keys(raw)) {
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
bookSourceIndex[key] = raw[key];
}
}
}
}
} catch {
bookSourceIndex = Object.create(null);
}
}
function saveBookSources() {
try {
const dir = path.dirname(SOURCES_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(SOURCES_FILE, JSON.stringify(bookSourceIndex, null, 2), 'utf8');
} catch (err) {
console.error('[DownloadEngine] 保存书源索引失败:', err.message);
}
}
/**
* 搜索开源数据库匹配
* @param {string} bookName - 书名
* @param {string} author - 作者
* @returns {object} 匹配结果
*/
function searchOpenSources(bookName, author) {
const query = (bookName || '').trim().toLowerCase();
const authorQuery = (author || '').trim().toLowerCase();
const matches = [];
// 1. 搜索本地书源索引(已缓存的书籍)
for (const key of Object.keys(bookSourceIndex)) {
const entry = bookSourceIndex[key];
if (!entry || typeof entry !== 'object') continue;
const name = (entry.book_name || '').toLowerCase();
const auth = (entry.author || '').toLowerCase();
if (name.includes(query) || query.includes(name)) {
if (!authorQuery || auth.includes(authorQuery) || authorQuery.includes(auth)) {
matches.push({
source_id: entry.source_id || 'local',
source_name: entry.source_name || '本地缓存',
book_name: entry.book_name,
author: entry.author,
match_type: name === query ? 'exact' : 'partial',
available: true,
file_path: entry.file_path || null
});
}
}
}
// 2. 检查已下载的书籍
const localBooks = listBooks();
for (const book of localBooks) {
const name = (book.book_name || '').toLowerCase();
if (name.includes(query) || query.includes(name)) {
if (!authorQuery || (book.author || '').toLowerCase().includes(authorQuery)) {
const alreadyMatched = matches.some(m => m.book_name === book.book_name && m.source_id === 'local-downloaded');
if (!alreadyMatched) {
matches.push({
source_id: 'local-downloaded',
source_name: '本地已下载',
book_name: book.book_name,
author: book.author,
match_type: name === query ? 'exact' : 'partial',
available: true,
file_path: book.filename
});
}
}
}
}
// 3. 返回注册的数据源列表(标注哪些可能有这本书)
const potentialSources = OPEN_SOURCE_REGISTRIES.map(reg => ({
source_id: reg.id,
source_name: reg.name,
type: reg.type,
status: reg.status,
platforms: reg.platforms,
url: reg.url,
can_search: reg.status === 'connected',
description: reg.description
}));
return {
query: { book_name: bookName, author: author || '' },
local_matches: matches,
local_match_count: matches.length,
registered_sources: potentialSources,
registered_source_count: potentialSources.length,
recommendation: matches.length > 0
? '本地已有匹配,建议直接使用'
: '本地无匹配,将尝试从平台下载'
};
}
/**
* 获取所有注册数据源信息
*/
function getRegisteredSources() {
return OPEN_SOURCE_REGISTRIES.map(reg => ({
id: reg.id,
name: reg.name,
description: reg.description,
type: reg.type,
url: reg.url,
platforms: reg.platforms,
status: reg.status,
book_count_estimate: reg.book_count_estimate,
last_checked: reg.last_checked
}));
}
/**
* 添加书源记录到本地索引
*/
function addBookSource(bookName, author, sourceId, sourceName, filePath) {
const key = `${(bookName || '').trim()}_${(author || '').trim()}`.toLowerCase();
bookSourceIndex[key] = {
book_name: (bookName || '').trim(),
author: (author || '').trim(),
source_id: sourceId,
source_name: sourceName,
file_path: filePath || null,
indexed_at: new Date().toISOString()
};
saveBookSources();
}
// 内存任务队列
let tasks = [];
// ─── 初始化 ───
function init() {
if (!fs.existsSync(BOOKS_DIR)) {
fs.mkdirSync(BOOKS_DIR, { recursive: true });
}
loadTasks();
}
// ─── 持久化任务列表 ───
function saveTasks() {
try {
const dir = path.dirname(TASKS_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2), 'utf8');
} catch (err) {
console.error('[DownloadEngine] 保存任务列表失败:', err.message);
}
}
function loadTasks() {
try {
if (fs.existsSync(TASKS_FILE)) {
tasks = JSON.parse(fs.readFileSync(TASKS_FILE, 'utf8'));
}
} catch {
tasks = [];
}
}
/**
* 创建下载任务
* @param {object} params - { book_name, author, platform, requested_by }
* @returns {object} task
*/
function createTask({ book_name, author, platform, requested_by }) {
if (!book_name || !platform) {
throw new Error('book_name 和 platform 为必填项');
}
if (!SUPPORTED_PLATFORMS.includes(platform)) {
throw new Error(`不支持的平台: ${platform}。支持: ${SUPPORTED_PLATFORMS.join(', ')}`);
}
const taskId = `DL-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
const task = {
task_id: taskId,
book_name: book_name.trim(),
author: (author || '未知').trim(),
platform,
status: 'queued',
requested_by: requested_by || 'system',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
file_path: null,
file_size: null,
error: null
};
tasks.push(task);
saveTasks();
// 异步执行下载(模拟)
processTask(taskId);
return task;
}
/**
* 处理下载任务Phase 2 使用本地模拟 · Phase 3 接入真实下载器
*/
async function processTask(taskId) {
const task = tasks.find(t => t.task_id === taskId);
if (!task) return;
task.status = 'downloading';
task.updated_at = new Date().toISOString();
saveTasks();
try {
// Phase 2: 创建占位 TXT 文件(模拟下载完成)
// Phase 3: 接入真实下载器(七猫/番茄/阅文/晋江 API 或爬虫)
const safeBookName = task.book_name.replace(/[<>:"/\\|?*]/g, '_');
const safeAuthor = task.author.replace(/[<>:"/\\|?*]/g, '_');
const filename = `${safeBookName}_${safeAuthor}_${task.platform}.txt`;
const filePath = path.join(BOOKS_DIR, filename);
// 模拟下载延时
await new Promise(resolve => setTimeout(resolve, 500));
// 生成占位内容
const content = [
`${task.book_name}`,
`作者: ${task.author}`,
`平台: ${task.platform}`,
`下载时间: ${new Date().toISOString()}`,
'',
'─'.repeat(40),
'',
'第一章 开始',
'',
'这是一本好书。下载引擎已成功创建书籍记录。',
'真实内容将在接入开源下载器后自动填充。',
'',
'─'.repeat(40),
'',
'第二章 世界',
'',
'世界很大,故事很长。',
'每一章都是一个新的开始。',
'',
'─'.repeat(40),
'',
'第三章 结尾',
'',
'故事未完待续。',
'智库节点 Phase 2 · 下载引擎占位内容。',
''
].join('\n');
fs.writeFileSync(filePath, content, 'utf8');
const stats = fs.statSync(filePath);
task.status = 'completed';
task.file_path = filename;
task.file_size = stats.size;
task.updated_at = new Date().toISOString();
// 自动将下载的书籍加入书源索引
addBookSource(task.book_name, task.author, 'platform-' + task.platform, task.platform, filename);
} catch (err) {
task.status = 'failed';
task.error = err.message;
task.updated_at = new Date().toISOString();
}
saveTasks();
}
/**
* 获取任务列表
* @param {object} filter - { status, platform, limit }
*/
function listTasks({ status, platform, limit } = {}) {
let result = [...tasks];
if (status) result = result.filter(t => t.status === status);
if (platform) result = result.filter(t => t.platform === platform);
result.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (limit) result = result.slice(0, limit);
return result;
}
/**
* 获取单个任务
*/
function getTask(taskId) {
return tasks.find(t => t.task_id === taskId) || null;
}
/**
* 获取已下载书籍列表
*/
function listBooks() {
if (!fs.existsSync(BOOKS_DIR)) return [];
return fs.readdirSync(BOOKS_DIR)
.filter(f => f.endsWith('.txt'))
.map(filename => {
const filePath = path.join(BOOKS_DIR, filename);
const stats = fs.statSync(filePath);
// 从文件名解析信息
const parts = filename.replace('.txt', '').split('_');
return {
filename,
book_name: parts[0] || filename,
author: parts[1] || '未知',
platform: parts[2] || '未知',
size: stats.size,
created_at: stats.birthtime.toISOString()
};
})
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
}
/**
* 获取书籍内容
*/
function getBookContent(filename) {
const filePath = path.join(BOOKS_DIR, filename);
if (!fs.existsSync(filePath)) return null;
// 防止路径遍历
const resolvedPath = path.resolve(filePath);
const resolvedBase = path.resolve(BOOKS_DIR);
if (!resolvedPath.startsWith(resolvedBase)) return null;
return fs.readFileSync(filePath, 'utf8');
}
/**
* 获取引擎统计
*/
function getStats() {
const books = listBooks();
const allTasks = listTasks();
return {
total_books: books.length,
total_tasks: allTasks.length,
queued_tasks: allTasks.filter(t => t.status === 'queued').length,
downloading: allTasks.filter(t => t.status === 'downloading').length,
completed_tasks: allTasks.filter(t => t.status === 'completed').length,
failed_tasks: allTasks.filter(t => t.status === 'failed').length,
total_size_bytes: books.reduce((sum, b) => sum + b.size, 0),
supported_platforms: SUPPORTED_PLATFORMS,
registered_sources: OPEN_SOURCE_REGISTRIES.length,
local_index_count: Object.keys(bookSourceIndex).length
};
}
// 初始化
init();
loadBookSources();
module.exports = {
createTask,
listTasks,
getTask,
listBooks,
getBookContent,
getStats,
searchOpenSources,
getRegisteredSources,
addBookSource,
SUPPORTED_PLATFORMS
};