From b0179139ccf98ee4af9ff828e73e778ef3373fad Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Sat, 23 Aug 2025 12:03:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=8B=E8=BD=BD=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=8D=E8=83=BD=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E4=B8=8B=E8=BD=BD=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +- backend/routes/download.js | 203 ++++- backend/services/artist.js | 12 +- backend/services/artwork.js | 2 +- backend/services/download-executor.js | 232 ++++++ backend/services/download.js | 763 ++++++------------ backend/services/file-manager.js | 239 ++++++ backend/services/history-manager.js | 158 ++++ backend/services/progress-manager.js | 69 ++ backend/services/task-manager.js | 177 ++++ scripts/create-portable.js | 5 +- ui/dist.zip | Bin 124806 -> 126898 bytes .../components/download/DownloadProgress.vue | 416 ++++++++++ ui/src/services/api.ts | 2 +- ui/src/services/download.ts | 137 ++-- ui/src/types/index.ts | 19 +- ui/src/views/ArtworkView.vue | 149 +++- ui/src/views/DownloadsView.vue | 24 +- 18 files changed, 1986 insertions(+), 633 deletions(-) create mode 100644 backend/services/download-executor.js create mode 100644 backend/services/file-manager.js create mode 100644 backend/services/history-manager.js create mode 100644 backend/services/progress-manager.js create mode 100644 backend/services/task-manager.js create mode 100644 ui/src/components/download/DownloadProgress.vue diff --git a/README.md b/README.md index a79fad7..6e12249 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,15 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功 ### 便携版下载(如果不想自义定或者是懒) 如果懒得配置环境,可以直接下载便携版(日,我自己用怎么还被当成木马了,算了忽略一下,不放心就自己打包): -- **下载链接**: https://pan.baidu.com/s/1pIdl8eqQSA8jc2RM7HoZfg?pwd=j18v -- **提取码**: j18v -- **使用说明**: 下载后解压,记事本打开start.bat配置代理(看readme有介绍),双击 `start.bat` 即可启动,打开网站,按照教程登录即可 + +**方式一:百度网盘下载(更新不勤,版本可能比较落后)** +- **下载链接**: https://pan.baidu.com/s/1SNsiDRzrNoHp4BhUBNvr9w?pwd=2yyn 提取码: 2yyn +- **提取码**: 2yyn + +**方式二:直接下载(可能比较慢,服务器带宽有限辣)** +- **下载链接**: https://sywb.top/Staticfiles/p%E4%B8%8B%E8%BD%BD%E5%99%A8.rar + +**使用说明**: 下载后解压,记事本打开start.bat配置代理(看readme有介绍),双击 `start.bat` 即可启动,打开网站,按照教程登录即可 ### 环境要求 diff --git a/backend/routes/download.js b/backend/routes/download.js index cf8b0ad..6f705fd 100644 --- a/backend/routes/download.js +++ b/backend/routes/download.js @@ -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; \ No newline at end of file diff --git a/backend/services/artist.js b/backend/services/artist.js index ac59340..a21bfaa 100644 --- a/backend/services/artist.js +++ b/backend/services/artist.js @@ -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') { diff --git a/backend/services/artwork.js b/backend/services/artwork.js index 8b1e7ac..2f7d7d5 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -331,7 +331,7 @@ class ArtworkService { method, url: `${this.baseURL}${endpoint}`, headers, - timeout: 30000 + timeout: 60000 // 增加到60秒 }; if (data) { diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js new file mode 100644 index 0000000..6498558 --- /dev/null +++ b/backend/services/download-executor.js @@ -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; \ No newline at end of file diff --git a/backend/services/download.js b/backend/services/download.js index ba08d97..0f49d5b 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -1,17 +1,20 @@ -const axios = require('axios'); -const fs = require('fs-extra'); const path = require('path'); -const { v4: uuidv4 } = require('uuid'); const ArtworkService = require('./artwork'); const ArtistService = require('./artist'); -const ConfigManager = require('../config/config-manager'); +const TaskManager = require('./task-manager'); +const FileManager = require('./file-manager'); +const ProgressManager = require('./progress-manager'); +const HistoryManager = require('./history-manager'); +const DownloadExecutor = require('./download-executor'); +/** + * 下载服务 - 主服务类,协调各个管理器 + */ class DownloadService { constructor(auth) { this.auth = auth; this.artworkService = new ArtworkService(auth); this.artistService = new ArtistService(auth); - this.configManager = new ConfigManager(); // 检测是否在pkg打包环境中运行 const isPkg = process.pkg !== undefined; @@ -24,48 +27,34 @@ class DownloadService { this.dataPath = path.join(__dirname, '../../data'); } - this.tasksFile = path.join(this.dataPath, 'download_tasks.json'); - this.historyFile = path.join(this.dataPath, 'download_history.json'); + // 初始化各个管理器 + this.fileManager = new FileManager(); + this.taskManager = new TaskManager(this.dataPath); + this.progressManager = new ProgressManager(); + this.historyManager = new HistoryManager(this.dataPath); + this.downloadExecutor = new DownloadExecutor( + this.fileManager, + this.taskManager, + this.progressManager, + this.historyManager + ); - this.tasks = new Map(); // 内存中的任务状态 - this.history = []; // 下载历史 this.initialized = false; } - /** - * 获取当前下载路径 - */ - 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'); - } - } - /** * 初始化服务 */ async init() { try { // 确保目录存在 - const downloadPath = await this.getDownloadPath(); - await fs.ensureDir(downloadPath); - await fs.ensureDir(this.dataPath); + const downloadPath = await this.fileManager.getDownloadPath(); + await this.fileManager.ensureDirectory(downloadPath); + await this.fileManager.ensureDirectory(this.dataPath); - // 加载历史记录 - await this.loadHistory(); - - // 加载任务状态 - await this.loadTasks(); + // 初始化各个管理器 + await this.taskManager.init(); + await this.historyManager.init(); this.initialized = true; console.log('下载服务初始化完成,下载路径:', downloadPath); @@ -75,131 +64,115 @@ class DownloadService { } } - /** - * 加载下载历史 - */ - async loadHistory() { - try { - if (await fs.pathExists(this.historyFile)) { - this.history = await fs.readJson(this.historyFile); - } - } catch (error) { - console.error('加载下载历史失败:', error); - this.history = []; - } + // 代理方法 - 进度管理 + addProgressListener(taskId, listener) { + return this.progressManager.addProgressListener(taskId, listener); } - /** - * 保存下载历史 - */ - async saveHistory() { - try { - await fs.writeJson(this.historyFile, this.history, { spaces: 2 }); - } catch (error) { - console.error('保存下载历史失败:', error); - } + removeProgressListener(taskId, listener) { + return this.progressManager.removeProgressListener(taskId, listener); } - /** - * 加载任务状态 - */ - async loadTasks() { - try { - if (await fs.pathExists(this.tasksFile)) { - const tasksData = await fs.readJson(this.tasksFile); - // 只加载未完成的任务 - for (const [taskId, task] of Object.entries(tasksData)) { - if (task.status === 'downloading' || task.status === 'pending') { - this.tasks.set(taskId, task); - } - } - } - } catch (error) { - console.error('加载任务状态失败:', error); - } + notifyProgressUpdate(taskId, task) { + return this.progressManager.notifyProgressUpdate(taskId, task); } - /** - * 保存任务状态 - */ - async saveTasks() { - try { - const tasksData = {}; - for (const [taskId, task] of this.tasks.entries()) { - tasksData[taskId] = task; - } - await fs.writeJson(this.tasksFile, tasksData, { spaces: 2 }); - } catch (error) { - console.error('保存任务状态失败:', error); - } + // 代理方法 - 任务管理 + getTask(taskId) { + return this.taskManager.getTask(taskId); } - /** - * 获取任务进度 - */ - getTaskProgress(taskId) { - const task = this.tasks.get(taskId); + async getTaskProgress(taskId) { + const task = this.taskManager.getTask(taskId); if (!task) { - return null; + return { success: false, error: '任务不存在' }; } return { - id: task.id, - type: task.type, - status: task.status, - progress: task.progress, - total: task.total, - completed: task.completed, - failed: task.failed, - start_time: task.start_time, - end_time: task.end_time, - files: task.files || [], - error: task.error + success: true, + data: task }; } - /** - * 获取所有任务 - */ - getAllTasks() { - const tasks = []; - for (const [taskId, task] of this.tasks.entries()) { - tasks.push(this.getTaskProgress(taskId)); + async getAllTasks() { + return { + success: true, + data: this.taskManager.getAllTasks() + }; + } + + async cancelTask(taskId) { + const task = this.taskManager.getTask(taskId); + if (!task) { + return { success: false, error: '任务不存在' }; } - return tasks; + + await this.taskManager.updateTask(taskId, { + status: 'cancelled', + end_time: new Date() + }); + + this.progressManager.notifyProgressUpdate(taskId, task); + return { success: true }; } - /** - * 获取下载历史 - */ - getDownloadHistory(limit = 50, offset = 0) { - return this.history - .sort((a, b) => new Date(b.end_time) - new Date(a.end_time)) - .slice(offset, offset + limit); + async pauseTask(taskId) { + const task = this.taskManager.getTask(taskId); + if (!task) { + return { success: false, error: '任务不存在' }; + } + + await this.taskManager.updateTask(taskId, { status: 'paused' }); + this.progressManager.notifyProgressUpdate(taskId, task); + return { success: true }; } - /** - * 获取下载的文件列表 - */ + async resumeTask(taskId) { + const task = this.taskManager.getTask(taskId); + if (!task) { + return { success: false, error: '任务不存在' }; + } + + if (task.status !== 'paused') { + return { success: false, error: '任务状态不是暂停状态' }; + } + + await this.taskManager.updateTask(taskId, { status: 'downloading' }); + this.progressManager.notifyProgressUpdate(taskId, task); + + // 重新开始下载 + return this.downloadArtwork(task.artwork_id, { skipExisting: false }); + } + + // 代理方法 - 历史记录管理 + async getDownloadHistory(offset = 0, limit = 50) { + const result = this.historyManager.getDownloadHistory(offset, limit); + return { + success: true, + data: result + }; + } + + // 代理方法 - 文件管理 async getDownloadedFiles() { try { const files = []; - const downloadPath = await this.getDownloadPath(); - const artists = await fs.readdir(downloadPath); + const downloadPath = await this.fileManager.getDownloadPath(); + const artists = await this.fileManager.listDirectory(downloadPath); for (const artist of artists) { const artistPath = path.join(downloadPath, artist); - const artistStat = await fs.stat(artistPath); + const artistStat = await this.fileManager.getFileInfo(artistPath); - if (artistStat.isDirectory()) { - const artworks = await fs.readdir(artistPath); + if (artistStat.exists && artistStat.isDirectory) { + const artworks = await this.fileManager.listDirectory(artistPath); for (const artwork of artworks) { const artworkPath = path.join(artistPath, artwork); - const artworkStat = await fs.stat(artworkPath); + const artworkStat = await this.fileManager.getFileInfo(artworkPath); - if (artworkStat.isDirectory()) { - const artworkFiles = await fs.readdir(artworkPath); + if (artworkStat.exists && artworkStat.isDirectory) { + const artworkFiles = await this.fileManager.listDirectory(artworkPath); const imageFiles = artworkFiles.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file) ); @@ -210,8 +183,8 @@ class DownloadService { artwork: artwork, path: artworkPath, files: imageFiles, - total_size: await this.getDirectorySize(artworkPath), - created_at: artworkStat.birthtime + total_size: await this.fileManager.getDirectorySize(artworkPath), + created_at: artworkStat.created }); } } @@ -226,71 +199,20 @@ class DownloadService { } } - /** - * 检查作品是否已下载 - */ - async isArtworkDownloaded(artworkId) { - try { - const downloadPath = await this.getDownloadPath(); - - // 扫描下载目录查找作品 - const artists = await fs.readdir(downloadPath); - - for (const artist of artists) { - const artistPath = path.join(downloadPath, artist); - const artistStat = await fs.stat(artistPath); - - if (artistStat.isDirectory()) { - const artworks = await fs.readdir(artistPath); - - for (const artwork of artworks) { - // 检查是否是作品目录(包含数字ID) - const artworkMatch = artwork.match(/^(\d+)_(.+)$/); - if (artworkMatch) { - const foundArtworkId = artworkMatch[1]; - - if (parseInt(foundArtworkId) === parseInt(artworkId)) { - // 找到作品目录,检查是否包含图片文件 - const artworkPath = path.join(artistPath, artwork); - const artworkStat = await fs.stat(artworkPath); - - if (artworkStat.isDirectory()) { - const files = await fs.readdir(artworkPath); - const imageFiles = files.filter(file => - /\.(jpg|jpeg|png|gif|webp)$/i.test(file) - ); - return imageFiles.length > 0; - } - } - } - } - } - } - - return false; - } catch (error) { - console.error('检查作品下载状态失败:', error); - return false; - } - } - - /** - * 获取已下载的作品ID列表 - */ async getDownloadedArtworkIds() { try { const downloadedIds = new Set(); - const downloadPath = await this.getDownloadPath(); + const downloadPath = await this.fileManager.getDownloadPath(); // 扫描下载目录获取所有已下载的作品ID - const artists = await fs.readdir(downloadPath); + const artists = await this.fileManager.listDirectory(downloadPath); for (const artist of artists) { const artistPath = path.join(downloadPath, artist); - const artistStat = await fs.stat(artistPath); + const artistStat = await this.fileManager.getFileInfo(artistPath); - if (artistStat.isDirectory()) { - const artworks = await fs.readdir(artistPath); + if (artistStat.exists && artistStat.isDirectory) { + const artworks = await this.fileManager.listDirectory(artistPath); for (const artwork of artworks) { // 检查是否是作品目录(包含数字ID) @@ -300,10 +222,10 @@ class DownloadService { // 检查作品目录是否包含图片文件 const artworkPath = path.join(artistPath, artwork); - const artworkStat = await fs.stat(artworkPath); + const artworkStat = await this.fileManager.getFileInfo(artworkPath); - if (artworkStat.isDirectory()) { - const files = await fs.readdir(artworkPath); + if (artworkStat.exists && artworkStat.isDirectory) { + const files = await this.fileManager.listDirectory(artworkPath); const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file) ); @@ -323,113 +245,100 @@ class DownloadService { } } - /** - * 获取目录大小 - */ - async getDirectorySize(dirPath) { + async isArtworkDownloaded(artworkId) { try { - const files = await fs.readdir(dirPath); - let totalSize = 0; + console.log(`开始检查作品 ${artworkId} 的下载状态...`); + const downloadPath = await this.fileManager.getDownloadPath(); + console.log(`下载路径: ${downloadPath}`); - for (const file of files) { - const filePath = path.join(dirPath, file); - const stat = await fs.stat(filePath); - if (stat.isFile()) { - totalSize += stat.size; + // 扫描所有作者目录 + const artistEntries = await this.fileManager.listDirectory(downloadPath); + console.log(`找到 ${artistEntries.length} 个作者目录`); + + for (const artistEntry of artistEntries) { + const artistPath = path.join(downloadPath, artistEntry); + const artistStat = await this.fileManager.getFileInfo(artistPath); + + if (!artistStat.exists || !artistStat.isDirectory) continue; + + // 扫描作者下的作品目录 + const artworkEntries = await this.fileManager.listDirectory(artistPath); + + for (const artworkEntry of artworkEntries) { + // 检查是否是目标作品目录(包含数字ID) + const artworkMatch = artworkEntry.match(/^(\d+)_(.+)$/); + if (artworkMatch && artworkMatch[1] === artworkId.toString()) { + console.log(`找到作品目录: ${artworkEntry}`); + const artworkPath = path.join(artistPath, artworkEntry); + + // 检查作品信息文件 + const infoPath = path.join(artworkPath, 'artwork_info.json'); + if (!await this.fileManager.fileExists(infoPath)) { + console.log(`作品信息文件不存在: ${infoPath}`); + return false; + } + + // 检查图片文件 + const files = await this.fileManager.listDirectory(artworkPath); + const imageFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp)$/i.test(file) && + file !== 'artwork_info.json' + ); + + console.log(`找到 ${imageFiles.length} 个图片文件`); + + if (imageFiles.length === 0) { + console.log(`没有找到图片文件`); + return false; + } + + // 检查每个图片文件的完整性 + for (const imageFile of imageFiles) { + const imagePath = path.join(artworkPath, imageFile); + const integrity = await this.fileManager.checkFileIntegrity(imagePath); + if (!integrity.valid) { + console.log(`作品 ${artworkId} 的文件 ${imageFile} 不完整: ${integrity.reason}`); + return false; + } + } + + console.log(`作品 ${artworkId} 已完整下载`); + return true; + } } } - return totalSize; + console.log(`作品 ${artworkId} 未找到`); + return false; } catch (error) { - return 0; + console.error('检查作品下载状态失败:', error); + return false; } } - /** - * 删除下载的文件 - */ - async deleteDownloadedFiles(artist, artwork) { - try { - const downloadPath = await this.getDownloadPath(); - const targetPath = path.join(downloadPath, artist, artwork); - if (await fs.pathExists(targetPath)) { - await fs.remove(targetPath); - - // 从历史记录中移除 - this.history = this.history.filter(item => - !(item.artist_name === artist && item.artwork_title === artwork) - ); - await this.saveHistory(); - - return { success: true }; - } - return { success: false, error: '文件不存在' }; - } catch (error) { - return { success: false, error: error.message }; - } - } - - /** - * 取消下载任务 - */ - async cancelTask(taskId) { - const task = this.tasks.get(taskId); - if (!task) { - return { success: false, error: '任务不存在' }; - } - - if (task.status === 'completed' || task.status === 'failed') { - return { success: false, error: '任务已完成,无法取消' }; - } - - task.status = 'cancelled'; - task.end_time = new Date(); - await this.saveTasks(); - - return { success: true }; - } - /** * 下载单个作品 */ async downloadArtwork(artworkId, options = {}) { - const taskId = uuidv4(); const { size = 'original', quality = 'high', format = 'auto', skipExisting = true } = options; try { // 检查是否已下载 if (skipExisting && await this.isArtworkDownloaded(artworkId)) { - console.log(`作品 ${artworkId} 已存在,跳过下载`); + console.log(`作品 ${artworkId} 已存在且完整,跳过下载`); return { success: true, data: { - task_id: taskId, + task_id: null, artwork_id: artworkId, skipped: true, - message: '作品已存在,跳过下载' + message: '作品已存在且完整,跳过下载' } }; + } else if (skipExisting) { + console.log(`作品 ${artworkId} 目录存在但不完整,将重新下载`); } - // 创建任务记录 - const task = { - id: taskId, - type: 'artwork', - artwork_id: artworkId, - status: 'downloading', - progress: 0, - total: 1, - completed: 0, - failed: 0, - files: [], - start_time: new Date(), - end_time: null, - error: null - }; - - this.tasks.set(taskId, task); - await this.saveTasks(); - // 获取作品信息 const artworkResult = await this.artworkService.getArtworkDetail(artworkId); if (!artworkResult.success) { @@ -443,15 +352,22 @@ class DownloadService { throw new Error('作品信息不完整'); } - const artistName = (artwork.user.name || 'Unknown Artist').replace(/[<>:"/\\|?*]/g, '_'); - const artworkTitle = (artwork.title || 'Untitled').replace(/[<>:"/\\|?*]/g, '_'); + const artistName = this.fileManager.createSafeDirectoryName(artwork.user.name || 'Unknown Artist'); + const artworkTitle = this.fileManager.createSafeDirectoryName(artwork.title || 'Untitled'); - // 创建作品目录 - 使用仓库管理格式 - const downloadPath = await this.getDownloadPath(); + // 创建作品目录 + const downloadPath = await this.fileManager.getDownloadPath(); const artistDir = path.join(downloadPath, artistName); const artworkDirName = `${artworkId}_${artworkTitle}`; const artworkDir = path.join(artistDir, artworkDirName); - await fs.ensureDir(artworkDir); + + // 如果是重新下载,先删除现有目录 + if (!skipExisting && await this.fileManager.directoryExists(artworkDir)) { + console.log(`删除现有作品目录: ${artworkDir}`); + await this.fileManager.removeDirectory(artworkDir); + } + + await this.fileManager.ensureDirectory(artworkDir); // 获取图片URL const imagesResult = await this.artworkService.getArtworkImages(artworkId, size); @@ -460,101 +376,36 @@ class DownloadService { } const images = imagesResult.data.images; - task.total = images.length; - - // 下载所有图片 - const downloadPromises = images.map(async (image, index) => { - if (task.status === 'cancelled') { - return { success: false, error: '任务已取消' }; - } - - try { - const imageUrl = image[size] || image.original; - const fileExt = this.getFileExtension(imageUrl); - const fileName = `${artworkTitle}_${artworkId}_${index + 1}${fileExt}`; - const filePath = path.join(artworkDir, fileName); - - await this.downloadFile(imageUrl, filePath); - - task.completed++; - task.progress = Math.round((task.completed / task.total) * 100); - task.files.push({ - path: filePath, - url: imageUrl, - size: size, - filename: fileName - }); - - await this.saveTasks(); - return { success: true, file: fileName }; - } catch (error) { - task.failed++; - console.error(`下载图片失败 ${index + 1}:`, error.message); - return { success: false, error: error.message }; - } - }); - - await Promise.all(downloadPromises); - - // 保存作品信息 - const infoPath = path.join(artworkDir, 'artwork_info.json'); - await fs.writeJson(infoPath, artwork, { spaces: 2 }); - - // 更新任务状态 - task.status = task.failed === 0 ? 'completed' : 'partial'; - task.end_time = new Date(); - await this.saveTasks(); - - // 添加到历史记录 - const historyItem = { - id: taskId, - type: 'artwork', + + // 创建任务记录 + const task = this.taskManager.createTask('artwork', { artwork_id: artworkId, artist_name: artistName, artwork_title: artworkTitle, - download_path: artworkDir, - total_files: task.total, - completed_files: task.completed, - failed_files: task.failed, - files: task.files, - start_time: task.start_time, - end_time: task.end_time, - status: task.status - }; - - this.history.unshift(historyItem); - await this.saveHistory(); - - console.log('下载完成,历史记录已保存:', { - taskId, - historyLength: this.history.length, - tasksCount: this.tasks.size + total_files: images.length, + completed_files: 0, + failed_files: 0 }); + + await this.taskManager.saveTasks(); + // 立即返回任务ID,异步执行下载 + this.downloadExecutor.executeArtworkDownload(task, images, size, artworkDir, artwork); + return { success: true, data: { - task_id: taskId, + task_id: task.id, artwork_id: artworkId, artist_name: artistName, artwork_title: artworkTitle, - download_path: artworkDir, - total_files: task.total, - completed_files: task.completed, - failed_files: task.failed, - files: task.files + status: 'downloading', + message: '下载任务已创建,正在后台执行' } }; } catch (error) { - const task = this.tasks.get(taskId); - if (task) { - task.status = 'failed'; - task.error = error.message; - task.end_time = new Date(); - await this.saveTasks(); - } - + console.error('下载作品失败:', error); return { success: false, error: error.message @@ -562,43 +413,10 @@ class DownloadService { } } - /** - * 下载文件 - */ - 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: 30000 - }); - - const writer = fs.createWriteStream(filePath); - response.data.pipe(writer); - - return new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }); - } - - /** - * 获取文件扩展名 - */ - getFileExtension(url) { - const match = url.match(/\.([a-zA-Z0-9]+)(\?|$)/); - return match ? `.${match[1]}` : '.jpg'; - } - /** * 批量下载作品 */ async downloadMultipleArtworks(artworkIds, options = {}) { - const taskId = uuidv4(); const { concurrent = 3, size = 'original', quality = 'high', format = 'auto', skipExisting = true } = options; try { @@ -617,38 +435,29 @@ class DownloadService { } // 创建任务记录 - const task = { - id: taskId, - type: 'batch', + const task = this.taskManager.createTask('batch', { artwork_ids: artworkIds, filtered_ids: filteredIds, - status: 'downloading', - progress: 0, total: filteredIds.length, completed: 0, failed: 0, skipped: skippedCount, - results: [], - start_time: new Date(), - end_time: null, - error: null - }; + results: [] + }); - this.tasks.set(taskId, task); - await this.saveTasks(); - - const results = []; + await this.taskManager.saveTasks(); // 如果没有需要下载的作品,直接返回 if (filteredIds.length === 0) { - task.status = 'completed'; - task.end_time = new Date(); - await this.saveTasks(); + await this.taskManager.updateTask(task.id, { + status: 'completed', + end_time: new Date() + }); return { success: true, data: { - task_id: taskId, + task_id: task.id, total_artworks: artworkIds.length, completed_artworks: 0, failed_artworks: 0, @@ -658,62 +467,22 @@ class DownloadService { }; } - // 分批下载 - for (let i = 0; i < filteredIds.length; i += concurrent) { - if (task.status === 'cancelled') { - break; - } - - const batch = filteredIds.slice(i, i + concurrent); - const batchPromises = batch.map(async (artworkId) => { - try { - const result = await this.downloadArtwork(artworkId, { size, quality, format, skipExisting: false }); - task.completed++; - results.push({ artwork_id: artworkId, ...result }); - return result; - } catch (error) { - task.failed++; - results.push({ artwork_id: artworkId, success: false, error: error.message }); - return { success: false, error: error.message }; - } - }); - - await Promise.all(batchPromises); - task.progress = Math.round((task.completed / task.total) * 100); - await this.saveTasks(); - - // 添加延迟避免请求过于频繁 - if (i + concurrent < filteredIds.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.saveTasks(); - + // 异步执行批量下载 + this.downloadExecutor.executeBatchDownload(task, artworkIds, options); + return { success: true, data: { - task_id: taskId, + task_id: task.id, total_artworks: task.total, completed_artworks: task.completed, failed_artworks: task.failed, - results: results + message: '批量下载任务已创建,正在后台执行' } }; } catch (error) { - const task = this.tasks.get(taskId); - if (task) { - task.status = 'failed'; - task.error = error.message; - task.end_time = new Date(); - await this.saveTasks(); - } - + console.error('批量下载失败:', error); return { success: false, error: error.message @@ -725,7 +494,6 @@ class DownloadService { * 下载作者作品 */ async downloadArtistArtworks(artistId, options = {}) { - const taskId = uuidv4(); const { type = 'art', limit = 50, @@ -739,24 +507,16 @@ class DownloadService { try { // 创建任务记录 - const task = { - id: taskId, - type: 'artist', + const task = this.taskManager.createTask('artist', { artist_id: artistId, - status: 'downloading', - progress: 0, total: 0, completed: 0, failed: 0, skipped: 0, - results: [], - start_time: new Date(), - end_time: null, - error: null - }; + results: [] + }); - this.tasks.set(taskId, task); - await this.saveTasks(); + await this.taskManager.saveTasks(); // 获取已下载的作品ID const downloadedIds = skipExisting ? await this.getDownloadedArtworkIds() : []; @@ -799,22 +559,25 @@ class DownloadService { : allArtworks; const skippedCount = allArtworks.length - newArtworks.length; - task.skipped = skippedCount; - task.total = newArtworks.length; - await this.saveTasks(); + + await this.taskManager.updateTask(task.id, { + skipped: skippedCount, + total: newArtworks.length + }); console.log(`作者作品下载: 总共 ${allArtworks.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${newArtworks.length} 个作品`); // 如果没有需要下载的作品,直接返回 if (newArtworks.length === 0) { - task.status = 'completed'; - task.end_time = new Date(); - await this.saveTasks(); + await this.taskManager.updateTask(task.id, { + status: 'completed', + end_time: new Date() + }); return { success: true, data: { - task_id: taskId, + task_id: task.id, artist_id: artistId, total_artworks: allArtworks.length, completed_artworks: 0, @@ -825,65 +588,23 @@ class DownloadService { }; } - 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 { - const result = await this.downloadArtwork(artwork.id, { size, quality, format, skipExisting: false }); - task.completed++; - results.push({ artwork_id: artwork.id, ...result }); - return result; - } catch (error) { - task.failed++; - results.push({ artwork_id: artwork.id, success: false, error: error.message }); - return { success: false, error: error.message }; - } - }); - - await Promise.all(batchPromises); - task.progress = Math.round((task.completed / task.total) * 100); - await this.saveTasks(); - - // 添加延迟避免请求过于频繁 - 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.saveTasks(); - + // 异步执行作者作品下载 + this.downloadExecutor.executeArtistDownload(task, newArtworks, options); + return { success: true, data: { - task_id: taskId, + task_id: task.id, artist_id: artistId, total_artworks: task.total, completed_artworks: task.completed, failed_artworks: task.failed, - results: results + message: '作者作品下载任务已创建,正在后台执行' } }; } catch (error) { - const task = this.tasks.get(taskId); - if (task) { - task.status = 'failed'; - task.error = error.message; - task.end_time = new Date(); - await this.saveTasks(); - } - + console.error('作者作品下载失败:', error); return { success: false, error: error.message diff --git a/backend/services/file-manager.js b/backend/services/file-manager.js new file mode 100644 index 0000000..72dd181 --- /dev/null +++ b/backend/services/file-manager.js @@ -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; \ No newline at end of file diff --git a/backend/services/history-manager.js b/backend/services/history-manager.js new file mode 100644 index 0000000..ba32c23 --- /dev/null +++ b/backend/services/history-manager.js @@ -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; \ No newline at end of file diff --git a/backend/services/progress-manager.js b/backend/services/progress-manager.js new file mode 100644 index 0000000..513efca --- /dev/null +++ b/backend/services/progress-manager.js @@ -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; \ No newline at end of file diff --git a/backend/services/task-manager.js b/backend/services/task-manager.js new file mode 100644 index 0000000..7367af8 --- /dev/null +++ b/backend/services/task-manager.js @@ -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; \ No newline at end of file diff --git a/scripts/create-portable.js b/scripts/create-portable.js index 7813a30..c3c9b4d 100644 --- a/scripts/create-portable.js +++ b/scripts/create-portable.js @@ -65,9 +65,9 @@ pause 2. 在浏览器中访问 http://localhost:3000 3. 按 Ctrl+C 停止服务器 -## 代理设置 +## 代理设置(重要) -如需使用代理,请编辑 \`start.bat\` 文件,修改第6行的端口号: +如需使用代理,请用记事本编辑 \`start.bat\` 文件,修改(PROXY_PORT=xxxx)的端口号: - Clash: 7890 - V2Ray: 10809 - Shadowsocks: 1080 @@ -76,6 +76,7 @@ pause - 首次运行可能需要几秒钟启动时间 - 程序会在当前目录创建数据文件夹 +- 没代理或者代理设置错误无法成功登录,注意仔细检查,获取code的时间比较短,记得快速操作 - 支持Windows 10/11 64位系统 `; diff --git a/ui/dist.zip b/ui/dist.zip index 99b9f17c2d0940f151a68445b5b53be5449dff1f..d9f5bef49bb8d215e3e0ea9924384bc71c7452d3 100644 GIT binary patch delta 81644 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

xY&5YqmO=ccVg2RNSz-hbel&+jC22N318qFyM%Z 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