更新下载进度,修复不能重新下载的问题

This commit is contained in:
2025-08-23 12:03:40 +08:00
parent 8d4e479ee1
commit b0179139cc
18 changed files with 1986 additions and 633 deletions
+6 -6
View File
@@ -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') {
+1 -1
View File
@@ -331,7 +331,7 @@ class ArtworkService {
method,
url: `${this.baseURL}${endpoint}`,
headers,
timeout: 30000
timeout: 60000 // 增加到60秒
};
if (data) {
+232
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+239
View File
@@ -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;
+158
View File
@@ -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;
+69
View File
@@ -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;
+177
View File
@@ -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;