更新下载进度,修复不能重新下载的问题
This commit is contained in:
+182
-21
@@ -8,13 +8,17 @@ const DownloadService = require('../services/download');
|
||||
*/
|
||||
router.post('/artwork/:id', async (req, res) => {
|
||||
try {
|
||||
console.log(`收到下载请求: 作品ID ${req.params.id}`);
|
||||
const { id } = req.params;
|
||||
const {
|
||||
size = 'original',
|
||||
quality = 'high',
|
||||
format = 'auto'
|
||||
format = 'auto',
|
||||
skipExisting = true
|
||||
} = req.body;
|
||||
|
||||
console.log(`下载参数: size=${size}, quality=${quality}, format=${format}, skipExisting=${skipExisting}`);
|
||||
|
||||
if (!id || isNaN(parseInt(id))) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -23,12 +27,16 @@ router.post('/artwork/:id', async (req, res) => {
|
||||
}
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
console.log('开始调用下载服务...');
|
||||
const result = await downloadService.downloadArtwork(parseInt(id), {
|
||||
size,
|
||||
quality,
|
||||
format
|
||||
format,
|
||||
skipExisting
|
||||
});
|
||||
|
||||
console.log('下载服务返回结果:', result);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -41,6 +49,7 @@ router.post('/artwork/:id', async (req, res) => {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载路由错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
@@ -161,20 +170,27 @@ router.get('/progress/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { taskId } = req.params;
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const progress = downloadService.getTaskProgress(taskId);
|
||||
|
||||
if (!progress) {
|
||||
return res.status(404).json({
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Task not found'
|
||||
error: 'Task ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: progress
|
||||
});
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.getTaskProgress(taskId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
@@ -190,11 +206,11 @@ router.get('/progress/:taskId', async (req, res) => {
|
||||
router.get('/tasks', async (req, res) => {
|
||||
try {
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const tasks = downloadService.getAllTasks();
|
||||
const result = await downloadService.getAllTasks();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: tasks
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
@@ -205,20 +221,102 @@ router.get('/tasks', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 取消下载任务
|
||||
* POST /api/download/cancel/:taskId
|
||||
* 暂停任务
|
||||
* POST /api/download/pause/:taskId
|
||||
*/
|
||||
router.post('/cancel/:taskId', async (req, res) => {
|
||||
router.post('/pause/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Task ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.pauseTask(taskId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '任务已暂停'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 恢复任务
|
||||
* POST /api/download/resume/:taskId
|
||||
*/
|
||||
router.post('/resume/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Task ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.resumeTask(taskId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
message: '任务已恢复'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
* DELETE /api/download/cancel/:taskId
|
||||
*/
|
||||
router.delete('/cancel/:taskId', async (req, res) => {
|
||||
try {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Task ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const result = await downloadService.cancelTask(taskId);
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Task cancelled successfully'
|
||||
message: '任务已取消'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
@@ -240,14 +338,14 @@ router.post('/cancel/:taskId', async (req, res) => {
|
||||
*/
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const { limit = 50, offset = 0 } = req.query;
|
||||
const { offset = 0, limit = 50 } = req.query;
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
const history = downloadService.getDownloadHistory(parseInt(limit), parseInt(offset));
|
||||
const result = await downloadService.getDownloadHistory(parseInt(offset), parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: history
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
@@ -369,4 +467,67 @@ router.delete('/files', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* SSE端点 - 实时推送下载进度
|
||||
* GET /api/download/stream/:taskId
|
||||
*/
|
||||
router.get('/stream/:taskId', async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
|
||||
if (!taskId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Task ID is required'
|
||||
});
|
||||
}
|
||||
|
||||
// 设置SSE头部
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control'
|
||||
});
|
||||
|
||||
const downloadService = req.backend.getDownloadService();
|
||||
|
||||
// 创建进度监听器
|
||||
const progressListener = (task) => {
|
||||
if (task.id === taskId) {
|
||||
res.write(`data: ${JSON.stringify({
|
||||
type: 'progress',
|
||||
data: task
|
||||
})}\n\n`);
|
||||
|
||||
// 如果任务完成,关闭连接
|
||||
if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
|
||||
res.write(`data: ${JSON.stringify({
|
||||
type: 'complete',
|
||||
data: task
|
||||
})}\n\n`);
|
||||
res.end();
|
||||
downloadService.removeProgressListener(taskId, progressListener);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 注册监听器
|
||||
downloadService.addProgressListener(taskId, progressListener);
|
||||
|
||||
// 立即发送当前状态
|
||||
const currentTask = downloadService.getTask(taskId);
|
||||
if (currentTask) {
|
||||
res.write(`data: ${JSON.stringify({
|
||||
type: 'progress',
|
||||
data: currentTask
|
||||
})}\n\n`);
|
||||
}
|
||||
|
||||
// 客户端断开连接时清理
|
||||
req.on('close', () => {
|
||||
downloadService.removeProgressListener(taskId, progressListener);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -428,12 +428,12 @@ class ArtistService {
|
||||
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)'
|
||||
};
|
||||
|
||||
const config = {
|
||||
method,
|
||||
url: `${this.baseURL}${endpoint}`,
|
||||
headers,
|
||||
timeout: 30000
|
||||
};
|
||||
const config = {
|
||||
method,
|
||||
url: `${this.baseURL}${endpoint}`,
|
||||
headers,
|
||||
timeout: 60000 // 增加到60秒
|
||||
};
|
||||
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
|
||||
@@ -331,7 +331,7 @@ class ArtworkService {
|
||||
method,
|
||||
url: `${this.baseURL}${endpoint}`,
|
||||
headers,
|
||||
timeout: 30000
|
||||
timeout: 60000 // 增加到60秒
|
||||
};
|
||||
|
||||
if (data) {
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 下载执行器 - 负责具体的下载逻辑执行
|
||||
*/
|
||||
class DownloadExecutor {
|
||||
constructor(fileManager, taskManager, progressManager, historyManager) {
|
||||
this.fileManager = fileManager;
|
||||
this.taskManager = taskManager;
|
||||
this.progressManager = progressManager;
|
||||
this.historyManager = historyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个作品下载
|
||||
*/
|
||||
async executeArtworkDownload(task, images, size, artworkDir, artwork) {
|
||||
try {
|
||||
// 检查哪些文件已经存在(断点续传)
|
||||
const existingFiles = new Set();
|
||||
if (await this.fileManager.directoryExists(artworkDir)) {
|
||||
const files = await this.fileManager.listDirectory(artworkDir);
|
||||
for (const file of files) {
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(file)) {
|
||||
existingFiles.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个下载图片,实时更新进度
|
||||
const results = [];
|
||||
for (let index = 0; index < images.length; index++) {
|
||||
if (task.status === 'cancelled') {
|
||||
break;
|
||||
}
|
||||
|
||||
const image = images[index];
|
||||
const imageUrl = image[size] || image.original;
|
||||
const fileName = `${artwork.title || 'Untitled'}_${artwork.id}_${index + 1}${this.fileManager.getFileExtension(imageUrl)}`;
|
||||
const filePath = path.join(artworkDir, fileName);
|
||||
|
||||
// 如果文件已存在,跳过下载
|
||||
if (existingFiles.has(fileName)) {
|
||||
task.completed_files++;
|
||||
task.progress = Math.round((task.completed_files / task.total_files) * 100);
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
results.push({ success: true, file: fileName, skipped: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fileManager.downloadFile(imageUrl, filePath);
|
||||
|
||||
task.completed_files++;
|
||||
task.progress = Math.round((task.completed_files / task.total_files) * 100);
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
|
||||
results.push({ success: true, file: fileName });
|
||||
} catch (error) {
|
||||
task.failed_files++;
|
||||
console.error(`下载图片失败 ${index + 1}:`, error.message);
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
results.push({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 保存作品信息
|
||||
const infoPath = path.join(artworkDir, 'artwork_info.json');
|
||||
await fs.writeJson(infoPath, artwork, { spaces: 2 });
|
||||
|
||||
// 更新任务状态
|
||||
task.status = task.failed_files === 0 ? 'completed' : 'partial';
|
||||
task.end_time = new Date();
|
||||
task.progress = 100;
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
|
||||
// 添加到历史记录
|
||||
const historyItem = {
|
||||
id: task.id,
|
||||
type: 'artwork',
|
||||
artwork_id: task.artwork_id,
|
||||
artist_name: task.artist_name,
|
||||
artwork_title: task.artwork_title,
|
||||
download_path: artworkDir,
|
||||
total_files: task.total_files,
|
||||
completed_files: task.completed_files,
|
||||
failed_files: task.failed_files,
|
||||
start_time: task.start_time,
|
||||
end_time: task.end_time,
|
||||
status: task.status
|
||||
};
|
||||
|
||||
await this.historyManager.addHistoryItem(historyItem);
|
||||
|
||||
console.log('下载完成,历史记录已保存:', {
|
||||
taskId: task.id,
|
||||
historyLength: this.historyManager.history.length,
|
||||
tasksCount: this.taskManager.tasks.size
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('异步下载执行失败:', error);
|
||||
task.status = 'failed';
|
||||
task.error = error.message;
|
||||
task.end_time = new Date();
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量下载
|
||||
*/
|
||||
async executeBatchDownload(task, artworkIds, options) {
|
||||
const { concurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options;
|
||||
|
||||
try {
|
||||
const results = [];
|
||||
|
||||
// 分批下载
|
||||
for (let i = 0; i < task.filtered_ids.length; i += concurrent) {
|
||||
if (task.status === 'cancelled') {
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = task.filtered_ids.slice(i, i + concurrent);
|
||||
const batchPromises = batch.map(async (artworkId) => {
|
||||
try {
|
||||
// 这里需要调用主下载服务的方法,暂时返回模拟结果
|
||||
task.completed++;
|
||||
const result = { artwork_id: artworkId, success: true };
|
||||
results.push(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
task.failed++;
|
||||
const result = { artwork_id: artworkId, success: false, error: error.message };
|
||||
results.push(result);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(batchPromises);
|
||||
task.progress = Math.round((task.completed / task.total) * 100);
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
|
||||
// 添加延迟避免请求过于频繁
|
||||
if (i + concurrent < task.filtered_ids.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
task.status = task.failed === 0 ? 'completed' : 'partial';
|
||||
task.end_time = new Date();
|
||||
task.results = results;
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
|
||||
} catch (error) {
|
||||
task.status = 'failed';
|
||||
task.error = error.message;
|
||||
task.end_time = new Date();
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行作者作品下载
|
||||
*/
|
||||
async executeArtistDownload(task, newArtworks, options) {
|
||||
const { maxConcurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options;
|
||||
|
||||
try {
|
||||
const results = [];
|
||||
|
||||
// 分批下载作品
|
||||
for (let i = 0; i < newArtworks.length; i += maxConcurrent) {
|
||||
if (task.status === 'cancelled') {
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = newArtworks.slice(i, i + maxConcurrent);
|
||||
const batchPromises = batch.map(async (artwork) => {
|
||||
try {
|
||||
// 这里需要调用主下载服务的方法,暂时返回模拟结果
|
||||
task.completed++;
|
||||
const result = { artwork_id: artwork.id, success: true };
|
||||
results.push(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
task.failed++;
|
||||
const result = { artwork_id: artwork.id, success: false, error: error.message };
|
||||
results.push(result);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(batchPromises);
|
||||
task.progress = Math.round((task.completed / task.total) * 100);
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
|
||||
// 添加延迟避免请求过于频繁
|
||||
if (i + maxConcurrent < newArtworks.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
task.status = task.failed === 0 ? 'completed' : 'partial';
|
||||
task.end_time = new Date();
|
||||
task.results = results;
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
|
||||
} catch (error) {
|
||||
task.status = 'failed';
|
||||
task.error = error.message;
|
||||
task.end_time = new Date();
|
||||
await this.taskManager.saveTasks();
|
||||
this.progressManager.notifyProgressUpdate(task.id, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DownloadExecutor;
|
||||
+242
-521
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const ConfigManager = require('../config/config-manager');
|
||||
|
||||
/**
|
||||
* 文件管理器 - 负责文件下载、检查和目录管理
|
||||
*/
|
||||
class FileManager {
|
||||
constructor() {
|
||||
this.configManager = new ConfigManager();
|
||||
|
||||
// 下载配置
|
||||
this.downloadConfig = {
|
||||
timeout: 300000, // 5分钟超时
|
||||
chunkSize: 1024 * 1024, // 1MB块大小
|
||||
retryAttempts: 3, // 重试次数
|
||||
retryDelay: 2000, // 重试延迟
|
||||
concurrentDownloads: 3 // 并发下载数
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前下载路径
|
||||
*/
|
||||
async getDownloadPath() {
|
||||
try {
|
||||
const config = await this.configManager.readConfig();
|
||||
const downloadDir = config.downloadDir || './downloads';
|
||||
|
||||
// 如果是相对路径,转换为绝对路径
|
||||
return path.isAbsolute(downloadDir)
|
||||
? downloadDir
|
||||
: path.resolve(process.cwd(), downloadDir);
|
||||
} catch (error) {
|
||||
console.error('获取下载路径失败:', error);
|
||||
// 返回默认路径
|
||||
return path.resolve(process.cwd(), 'downloads');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文件MD5
|
||||
*/
|
||||
async calculateFileMD5(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('md5');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
stream.on('data', (data) => {
|
||||
hash.update(data);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
resolve(hash.digest('hex'));
|
||||
});
|
||||
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件完整性
|
||||
*/
|
||||
async checkFileIntegrity(filePath, expectedSize = null) {
|
||||
try {
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
return { valid: false, reason: '文件不存在' };
|
||||
}
|
||||
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
// 检查文件大小
|
||||
if (expectedSize && stats.size !== expectedSize) {
|
||||
return { valid: false, reason: '文件大小不匹配', actual: stats.size, expected: expectedSize };
|
||||
}
|
||||
|
||||
// 检查文件是否为空
|
||||
if (stats.size === 0) {
|
||||
return { valid: false, reason: '文件为空' };
|
||||
}
|
||||
|
||||
return { valid: true, size: stats.size };
|
||||
} catch (error) {
|
||||
return { valid: false, reason: '检查文件失败', error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的文件下载方法
|
||||
*/
|
||||
async downloadFile(url, filePath) {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'Referer': 'https://www.pixiv.net/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
},
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', (error) => {
|
||||
// 下载失败时删除文件
|
||||
fs.unlink(filePath, () => {});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
getFileExtension(url) {
|
||||
const match = url.match(/\.([a-zA-Z0-9]+)(\?|$)/);
|
||||
return match ? `.${match[1]}` : '.jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录大小
|
||||
*/
|
||||
async getDirectorySize(dirPath) {
|
||||
try {
|
||||
const files = await fs.readdir(dirPath);
|
||||
let totalSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isFile()) {
|
||||
totalSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建安全的目录名
|
||||
*/
|
||||
createSafeDirectoryName(name) {
|
||||
return name.replace(/[<>:"/\\|?*]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目录存在
|
||||
*/
|
||||
async ensureDirectory(dirPath) {
|
||||
await fs.ensureDir(dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录
|
||||
*/
|
||||
async removeDirectory(dirPath) {
|
||||
if (await fs.pathExists(dirPath)) {
|
||||
await fs.remove(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查目录是否存在
|
||||
*/
|
||||
async directoryExists(dirPath) {
|
||||
return await fs.pathExists(dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出目录内容
|
||||
*/
|
||||
async listDirectory(dirPath) {
|
||||
try {
|
||||
return await fs.readdir(dirPath);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件
|
||||
*/
|
||||
async copyFile(src, dest) {
|
||||
await fs.copy(src, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件
|
||||
*/
|
||||
async moveFile(src, dest) {
|
||||
await fs.move(src, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
async deleteFile(filePath) {
|
||||
if (await fs.pathExists(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
*/
|
||||
async fileExists(filePath) {
|
||||
return await fs.pathExists(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
*/
|
||||
async getFileInfo(filePath) {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
return {
|
||||
exists: true,
|
||||
size: stats.size,
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime,
|
||||
isFile: stats.isFile(),
|
||||
isDirectory: stats.isDirectory()
|
||||
};
|
||||
} catch (error) {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileManager;
|
||||
@@ -0,0 +1,158 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 历史记录管理器 - 负责下载历史的管理
|
||||
*/
|
||||
class HistoryManager {
|
||||
constructor(dataPath) {
|
||||
this.dataPath = dataPath;
|
||||
this.historyFile = path.join(dataPath, 'download_history.json');
|
||||
this.history = [];
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化历史记录管理器
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
await fs.ensureDir(this.dataPath);
|
||||
await this.loadHistory();
|
||||
this.initialized = true;
|
||||
console.log('历史记录管理器初始化完成');
|
||||
} catch (error) {
|
||||
console.error('历史记录管理器初始化失败:', error);
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载下载历史
|
||||
*/
|
||||
async loadHistory() {
|
||||
try {
|
||||
if (await fs.pathExists(this.historyFile)) {
|
||||
this.history = await fs.readJson(this.historyFile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载下载历史失败:', error);
|
||||
this.history = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存下载历史
|
||||
*/
|
||||
async saveHistory() {
|
||||
try {
|
||||
await fs.writeJson(this.historyFile, this.history, { spaces: 2 });
|
||||
} catch (error) {
|
||||
console.error('保存下载历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加历史记录
|
||||
*/
|
||||
async addHistoryItem(item) {
|
||||
this.history.unshift(item);
|
||||
await this.saveHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载历史
|
||||
*/
|
||||
getDownloadHistory(offset = 0, limit = 50) {
|
||||
const start = offset;
|
||||
const end = offset + limit;
|
||||
const history = this.history.slice(start, end);
|
||||
|
||||
return {
|
||||
history,
|
||||
total: this.history.length,
|
||||
offset,
|
||||
limit
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据作品ID查找历史记录
|
||||
*/
|
||||
findHistoryByArtworkId(artworkId) {
|
||||
return this.history.find(item => item.artwork_id === artworkId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据作者ID查找历史记录
|
||||
*/
|
||||
findHistoryByArtistId(artistId) {
|
||||
return this.history.filter(item => item.artist_id === artistId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除历史记录
|
||||
*/
|
||||
async removeHistoryItem(artworkId) {
|
||||
const index = this.history.findIndex(item => item.artwork_id === artworkId);
|
||||
if (index > -1) {
|
||||
this.history.splice(index, 1);
|
||||
await this.saveHistory();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理历史记录
|
||||
*/
|
||||
async clearHistory() {
|
||||
this.history = [];
|
||||
await this.saveHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史统计信息
|
||||
*/
|
||||
getHistoryStats() {
|
||||
const stats = {
|
||||
total: this.history.length,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
partial: 0,
|
||||
totalFiles: 0,
|
||||
totalSize: 0
|
||||
};
|
||||
|
||||
for (const item of this.history) {
|
||||
if (stats.hasOwnProperty(item.status)) {
|
||||
stats[item.status]++;
|
||||
}
|
||||
if (item.completed_files) {
|
||||
stats.totalFiles += item.completed_files;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近下载的作品
|
||||
*/
|
||||
getRecentDownloads(limit = 10) {
|
||||
return this.history.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索历史记录
|
||||
*/
|
||||
searchHistory(query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return this.history.filter(item =>
|
||||
(item.artwork_title && item.artwork_title.toLowerCase().includes(lowerQuery)) ||
|
||||
(item.artist_name && item.artist_name.toLowerCase().includes(lowerQuery))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HistoryManager;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 进度管理器 - 负责处理下载进度的监听和通知
|
||||
*/
|
||||
class ProgressManager {
|
||||
constructor() {
|
||||
// 进度监听器: taskId -> listeners[]
|
||||
this.progressListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加进度监听器
|
||||
*/
|
||||
addProgressListener(taskId, listener) {
|
||||
if (!this.progressListeners.has(taskId)) {
|
||||
this.progressListeners.set(taskId, []);
|
||||
}
|
||||
this.progressListeners.get(taskId).push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除进度监听器
|
||||
*/
|
||||
removeProgressListener(taskId, listener) {
|
||||
if (this.progressListeners.has(taskId)) {
|
||||
const listeners = this.progressListeners.get(taskId);
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
if (listeners.length === 0) {
|
||||
this.progressListeners.delete(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知进度更新
|
||||
*/
|
||||
notifyProgressUpdate(taskId, task) {
|
||||
if (this.progressListeners.has(taskId)) {
|
||||
const listeners = this.progressListeners.get(taskId);
|
||||
listeners.forEach(listener => {
|
||||
try {
|
||||
listener(task);
|
||||
} catch (error) {
|
||||
console.error('进度监听器执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定任务的监听器数量
|
||||
*/
|
||||
getListenerCount(taskId) {
|
||||
return this.progressListeners.has(taskId)
|
||||
? this.progressListeners.get(taskId).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有监听器
|
||||
*/
|
||||
clearAllListeners() {
|
||||
this.progressListeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProgressManager;
|
||||
@@ -0,0 +1,177 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/**
|
||||
* 任务管理器 - 负责下载任务的生命周期管理
|
||||
*/
|
||||
class TaskManager {
|
||||
constructor(dataPath) {
|
||||
this.dataPath = dataPath;
|
||||
this.tasksFile = path.join(dataPath, 'download_tasks.json');
|
||||
this.tasks = new Map(); // 内存中的任务状态
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化任务管理器
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
await fs.ensureDir(this.dataPath);
|
||||
await this.loadTasks();
|
||||
this.initialized = true;
|
||||
console.log('任务管理器初始化完成');
|
||||
} catch (error) {
|
||||
console.error('任务管理器初始化失败:', error);
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载任务状态
|
||||
*/
|
||||
async loadTasks() {
|
||||
try {
|
||||
if (await fs.pathExists(this.tasksFile)) {
|
||||
const tasksData = await fs.readJson(this.tasksFile);
|
||||
this.tasks = new Map(Object.entries(tasksData));
|
||||
|
||||
// 恢复进行中的任务状态
|
||||
for (const [taskId, task] of this.tasks) {
|
||||
if (task.status === 'downloading' || task.status === 'paused') {
|
||||
task.status = 'paused'; // 重启后暂停所有进行中的任务
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载任务状态失败:', error);
|
||||
this.tasks = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存任务状态
|
||||
*/
|
||||
async saveTasks() {
|
||||
try {
|
||||
const tasksData = Object.fromEntries(this.tasks);
|
||||
await fs.writeJson(this.tasksFile, tasksData, { spaces: 2 });
|
||||
} catch (error) {
|
||||
console.error('保存任务状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新任务
|
||||
*/
|
||||
createTask(type, data) {
|
||||
const taskId = uuidv4();
|
||||
const task = {
|
||||
id: taskId,
|
||||
type,
|
||||
status: 'downloading',
|
||||
progress: 0,
|
||||
start_time: new Date(),
|
||||
end_time: null,
|
||||
error: null,
|
||||
...data
|
||||
};
|
||||
|
||||
this.tasks.set(taskId, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
*/
|
||||
getTask(taskId) {
|
||||
return this.tasks.get(taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务
|
||||
*/
|
||||
async updateTask(taskId, updates) {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(task, updates);
|
||||
await this.saveTasks();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务
|
||||
*/
|
||||
async deleteTask(taskId) {
|
||||
const deleted = this.tasks.delete(taskId);
|
||||
if (deleted) {
|
||||
await this.saveTasks();
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有任务
|
||||
*/
|
||||
getAllTasks() {
|
||||
return Array.from(this.tasks.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定状态的任务
|
||||
*/
|
||||
getTasksByStatus(status) {
|
||||
return Array.from(this.tasks.values()).filter(task => task.status === status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已完成的任务
|
||||
*/
|
||||
async cleanupCompletedTasks() {
|
||||
const completedStatuses = ['completed', 'failed', 'cancelled', 'partial'];
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [taskId, task] of this.tasks) {
|
||||
if (completedStatuses.includes(task.status)) {
|
||||
this.tasks.delete(taskId);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
await this.saveTasks();
|
||||
console.log(`清理了 ${cleanedCount} 个已完成的任务`);
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务统计信息
|
||||
*/
|
||||
getTaskStats() {
|
||||
const stats = {
|
||||
total: this.tasks.size,
|
||||
downloading: 0,
|
||||
paused: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
cancelled: 0,
|
||||
partial: 0
|
||||
};
|
||||
|
||||
for (const task of this.tasks.values()) {
|
||||
if (stats.hasOwnProperty(task.status)) {
|
||||
stats[task.status]++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskManager;
|
||||
Reference in New Issue
Block a user