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 99b9f17..d9f5bef 100644
Binary files a/ui/dist.zip and b/ui/dist.zip differ
diff --git a/ui/src/components/download/DownloadProgress.vue b/ui/src/components/download/DownloadProgress.vue
new file mode 100644
index 0000000..d5f8046
--- /dev/null
+++ b/ui/src/components/download/DownloadProgress.vue
@@ -0,0 +1,416 @@
+
+ {{ task.artwork_title || '下载中...' }}
+