From a35e82731d2bcd1ed338f9f60b6286e477b80b07 Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Sun, 24 Aug 2025 15:58:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E4=BD=99=E6=97=A5=E5=BF=97=E6=B8=85?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86=E4=BD=9C?= =?UTF-8?q?=E5=93=81=E6=97=A0=E6=B3=95=E5=88=9B=E5=BB=BA=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/download.js | 6 - backend/server.js | 9 +- backend/services/artist.js | 9 +- backend/services/artwork.js | 157 +++++-------- backend/services/download-executor.js | 57 +++-- backend/services/download.js | 314 +++++++++++--------------- backend/services/file-manager.js | 50 +++- backend/services/history-manager.js | 2 +- backend/services/task-manager.js | 26 +-- backend/utils/file-utils.js | 134 +++++++++++ 10 files changed, 410 insertions(+), 354 deletions(-) create mode 100644 backend/utils/file-utils.js diff --git a/backend/routes/download.js b/backend/routes/download.js index 4645c20..9c9b42b 100644 --- a/backend/routes/download.js +++ b/backend/routes/download.js @@ -8,7 +8,6 @@ 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', @@ -17,8 +16,6 @@ router.post('/artwork/:id', async (req, res) => { 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, @@ -27,7 +24,6 @@ router.post('/artwork/:id', async (req, res) => { } const downloadService = req.backend.getDownloadService(); - console.log('开始调用下载服务...'); const result = await downloadService.downloadArtwork(parseInt(id), { size, quality, @@ -35,8 +31,6 @@ router.post('/artwork/:id', async (req, res) => { skipExisting }); - console.log('下载服务返回结果:', result); - if (result.success) { res.json({ success: true, diff --git a/backend/server.js b/backend/server.js index 59c5698..8d72164 100644 --- a/backend/server.js +++ b/backend/server.js @@ -22,7 +22,7 @@ const proxyConfig = require('./config'); // 自定义日志中间件 function customLogger(req, res, next) { - // 过滤掉静态资源请求 + // 过滤掉静态资源请求和图片代理请求 const isStaticResource = req.path.startsWith('/assets/') || req.path.startsWith('/downloads/') || req.path.includes('.js') || @@ -38,8 +38,11 @@ function customLogger(req, res, next) { req.path.includes('.ttf') || req.path.includes('.eot'); - // 只记录API请求和重要请求 - if (!isStaticResource) { + // 过滤掉图片代理请求 + const isImageProxy = req.path === '/api/proxy/image'; + + // 只记录API请求和重要请求,排除静态资源和图片代理 + if (!isStaticResource && !isImageProxy) { const start = Date.now(); // 原始响应结束方法 diff --git a/backend/services/artist.js b/backend/services/artist.js index 77f7235..0503497 100644 --- a/backend/services/artist.js +++ b/backend/services/artist.js @@ -15,7 +15,6 @@ class ArtistService { const response = await this.makeRequest('GET', '/v1/user/detail', { user_id: artistId }); return { - success: true, data: response.user, }; @@ -41,7 +40,7 @@ class ArtistService { account: response.user.account, profile_image_urls: response.user.profile_image_urls, comment: response.user.comment, - is_followed: response.user.is_followed || false + is_followed: response.user.is_followed || false, }; return { @@ -72,9 +71,7 @@ class ArtistService { const response = await this.makeRequest('GET', `/v1/user/illusts?${stringify(params)}`); - console.log('Artworks response keys:', Object.keys(response)); - console.log('Artworks count:', response.illusts?.length || 0); - console.log('Next URL:', response.next_url); + // 获取作者作品列表 return { success: true, @@ -337,7 +334,7 @@ class ArtistService { } try { - console.log(`发送API请求: ${method} ${endpoint}`); + // 发送API请求 const response = await axios(config); return response.data; } catch (error) { diff --git a/backend/services/artwork.js b/backend/services/artwork.js index 3373ba7..a9896aa 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -13,27 +13,22 @@ class ArtworkService { async getArtworkDetail(artworkId, options = {}) { try { const { include_user = true, include_series = false } = options; - + const params = { include_user, - include_series + include_series, }; - const response = await this.makeRequest( - 'GET', - `/v1/illust/detail?${stringify(params)}`, - { illust_id: artworkId } - ); + const response = await this.makeRequest('GET', `/v1/illust/detail?${stringify(params)}`, { illust_id: artworkId }); return { success: true, - data: response.illust + data: response.illust, }; - } catch (error) { return { success: false, - error: error.message + error: error.message, }; } } @@ -43,14 +38,10 @@ class ArtworkService { */ async getArtworkPreview(artworkId) { try { - const response = await this.makeRequest( - 'GET', - '/v1/illust/detail', - { illust_id: artworkId } - ); + const response = await this.makeRequest('GET', '/v1/illust/detail', { illust_id: artworkId }); const artwork = response.illust; - + // 构建预览信息 const preview = { id: artwork.id, @@ -59,7 +50,7 @@ class ArtworkService { user: { id: artwork.user.id, name: artwork.user.name, - account: artwork.user.account + account: artwork.user.account, }, image_urls: artwork.image_urls, tags: artwork.tags.map(tag => tag.name), @@ -74,18 +65,17 @@ class ArtworkService { total_view: artwork.total_view, is_muted: artwork.is_muted, meta_single_page: artwork.meta_single_page, - meta_pages: artwork.meta_pages + meta_pages: artwork.meta_pages, }; return { success: true, - data: preview + data: preview, }; - } catch (error) { return { success: false, - error: error.message + error: error.message, }; } } @@ -95,11 +85,7 @@ class ArtworkService { */ async getArtworkImages(artworkId, size = 'medium') { try { - const response = await this.makeRequest( - 'GET', - '/v1/illust/detail', - { illust_id: artworkId } - ); + const response = await this.makeRequest('GET', '/v1/illust/detail', { illust_id: artworkId }); const artwork = response.illust; const images = []; @@ -111,7 +97,7 @@ class ArtworkService { original: artwork.meta_single_page.original_image_url, large: artwork.meta_single_page.large_image_url, medium: artwork.image_urls.medium, - square_medium: artwork.image_urls.square_medium + square_medium: artwork.image_urls.square_medium, }); } else if (artwork.meta_pages && artwork.meta_pages.length > 0) { // 多页作品 @@ -121,7 +107,7 @@ class ArtworkService { original: page.image_urls.original, large: page.image_urls.large, medium: page.image_urls.medium, - square_medium: page.image_urls.square_medium + square_medium: page.image_urls.square_medium, }); }); } @@ -132,14 +118,13 @@ class ArtworkService { artwork_id: artworkId, total_pages: artwork.page_count, images: images, - selected_size: size - } + selected_size: size, + }, }; - } catch (error) { return { success: false, - error: error.message + error: error.message, }; } } @@ -149,43 +134,35 @@ class ArtworkService { */ async searchArtworks(searchOptions) { try { - const { - keyword, - tags, - type = 'all', - sort = 'date_desc', - duration = 'all', - offset = 0, - limit = 30 - } = searchOptions; + const { keyword, tags, type = 'all', sort = 'date_desc', duration = 'all', offset = 0, limit = 30 } = searchOptions; // 验证搜索参数 if ((!keyword || keyword.trim() === '') && (!tags || tags.length === 0)) { return { success: false, - error: 'Search keyword or tags are required' + error: 'Search keyword or tags are required', }; } // 映射搜索参数到Pixiv API格式 const searchTargetMap = { - 'all': 'partial_match_for_tags', - 'art': 'partial_match_for_tags', - 'manga': 'partial_match_for_tags', - 'novel': 'partial_match_for_tags' + all: 'partial_match_for_tags', + art: 'partial_match_for_tags', + manga: 'partial_match_for_tags', + novel: 'partial_match_for_tags', }; const sortMap = { - 'date_desc': 'date_desc', - 'date_asc': 'date_asc', - 'popular_desc': 'popular_desc' + date_desc: 'date_desc', + date_asc: 'date_asc', + popular_desc: 'popular_desc', }; const durationMap = { - 'all': null, // 不传递duration参数表示全部时间 - 'within_last_day': 'within_last_day', - 'within_last_week': 'within_last_week', - 'within_last_month': 'within_last_month' + all: null, // 不传递duration参数表示全部时间 + within_last_day: 'within_last_day', + within_last_week: 'within_last_week', + within_last_month: 'within_last_month', }; // 构建搜索关键词 @@ -202,7 +179,7 @@ class ArtworkService { search_target: searchTargetMap[type] || 'partial_match_for_tags', sort: sortMap[sort] || 'date_desc', offset: parseInt(offset) || 0, - filter: 'for_ios' + filter: 'for_ios', }; // 只有当duration不是'all'时才添加duration参数 @@ -210,12 +187,9 @@ class ArtworkService { params.duration = durationMap[duration]; } - console.log('Search params:', params); + // 搜索参数已设置 - const response = await this.makeRequest( - 'GET', - `/v1/search/illust?${stringify(params)}` - ); + const response = await this.makeRequest('GET', `/v1/search/illust?${stringify(params)}`); return { success: true, @@ -223,17 +197,16 @@ class ArtworkService { artworks: response.illusts || [], next_url: response.next_url, search_span_limit: response.search_span_limit, - total: response.illusts ? response.illusts.length : 0 - } + total: response.illusts ? response.illusts.length : 0, + }, }; - } catch (error) { console.error('Search error:', error.message); console.error('Search error details:', error.response?.data); - + return { success: false, - error: error.message || 'Search failed' + error: error.message || 'Search failed', }; } } @@ -243,38 +216,29 @@ class ArtworkService { */ async getRecommendedArtworks(options = {}) { try { - const { - offset = 0, - limit = 30, - include_ranking_illusts = true, - include_privacy_policy = false - } = options; + const { offset = 0, limit = 30, include_ranking_illusts = true, include_privacy_policy = false } = options; const params = { offset, include_ranking_illusts, include_privacy_policy, - filter: 'for_ios' + filter: 'for_ios', }; - const response = await this.makeRequest( - 'GET', - `/v1/illust/recommended?${stringify(params)}` - ); + const response = await this.makeRequest('GET', `/v1/illust/recommended?${stringify(params)}`); return { success: true, data: { artworks: response.illusts, next_url: response.next_url, - ranking_illusts: response.ranking_illusts || [] - } + ranking_illusts: response.ranking_illusts || [], + }, }; - } catch (error) { return { success: false, - error: error.message + error: error.message, }; } } @@ -284,26 +248,17 @@ class ArtworkService { */ async getRankingArtworks(options = {}) { try { - const { - mode = 'day', - content = 'illust', - filter = 'for_ios', - offset = 0, - limit = 30 - } = options; + const { mode = 'day', content = 'illust', filter = 'for_ios', offset = 0, limit = 30 } = options; const params = { mode, content, filter, offset, - limit + limit, }; - const response = await this.makeRequest( - 'GET', - `/v1/illust/ranking?${stringify(params)}` - ); + const response = await this.makeRequest('GET', `/v1/illust/ranking?${stringify(params)}`); return { success: true, @@ -311,14 +266,13 @@ class ArtworkService { artworks: response.illusts, next_url: response.next_url, mode, - date: response.date - } + date: response.date, + }, }; - } catch (error) { return { success: false, - error: error.message + error: error.message, }; } } @@ -333,19 +287,19 @@ class ArtworkService { } const headers = { - 'Authorization': `Bearer ${this.auth.accessToken}`, + Authorization: `Bearer ${this.auth.accessToken}`, 'Accept-Language': 'en-us', 'App-OS': 'android', 'App-OS-Version': '9.0', 'App-Version': '5.0.234', - 'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)' + 'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)', }; const config = { method, url: `${this.baseURL}${endpoint}`, headers, - timeout: 60000 // 增加到60秒 + timeout: 60000, // 增加到60秒 }; if (data) { @@ -356,8 +310,7 @@ class ArtworkService { } } - console.log(`Making request to: ${config.url}`); - console.log('Request config:', { method, endpoint, data }); + // 发送API请求 const response = await axios(config); return response.data; @@ -367,11 +320,11 @@ class ArtworkService { endpoint, error: error.message, status: error.response?.status, - data: error.response?.data + data: error.response?.data, }); throw error; } } } -module.exports = ArtworkService; \ No newline at end of file +module.exports = ArtworkService; diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index e470644..dd396e1 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -34,10 +34,12 @@ class DownloadExecutor { 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 safeTitle = this.fileManager.createSafeDirectoryName(artwork.title || 'Untitled'); + const fileName = `${safeTitle}_${artwork.id}_${index + 1}${this.fileManager.getFileExtension(imageUrl)}`; const filePath = path.join(artworkDir, fileName); // 如果文件已存在,跳过下载 @@ -49,19 +51,22 @@ class DownloadExecutor { results.push({ success: true, file: fileName, skipped: true }); continue; } - + try { + // 确保目录存在 + await this.fileManager.ensureDirectory(path.dirname(filePath)); + 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); + console.error(`下载图片失败 ${index + 1}: ${error.message}`); this.progressManager.notifyProgressUpdate(task.id, task); results.push({ success: false, error: error.message }); } @@ -91,17 +96,10 @@ class DownloadExecutor { failed_files: task.failed_files, start_time: task.start_time, end_time: task.end_time, - status: task.status + status: task.status, }; - - await this.historyManager.addHistoryItem(historyItem); - - console.log('下载完成,历史记录已保存:', { - taskId: task.id, - historyLength: this.historyManager.history.length, - tasksCount: this.taskManager.tasks.size - }); + await this.historyManager.addHistoryItem(historyItem); } catch (error) { console.error('异步下载执行失败:', error); task.status = 'failed'; @@ -117,7 +115,7 @@ class DownloadExecutor { */ async executeBatchDownload(task, artworkIds, options) { const { concurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options; - + try { const results = []; @@ -126,9 +124,9 @@ class DownloadExecutor { if (task.status === 'cancelled') { break; } - + const batch = task.filtered_ids.slice(i, i + concurrent); - const batchPromises = batch.map(async (artworkId) => { + const batchPromises = batch.map(async artworkId => { try { // 这里需要调用主下载服务的方法,暂时返回模拟结果 task.completed++; @@ -147,7 +145,7 @@ class DownloadExecutor { 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)); @@ -160,7 +158,6 @@ class DownloadExecutor { task.results = results; await this.taskManager.saveTasks(); this.progressManager.notifyProgressUpdate(task.id, task); - } catch (error) { task.status = 'failed'; task.error = error.message; @@ -175,7 +172,7 @@ class DownloadExecutor { */ async executeArtistDownload(task, newArtworks, options) { const { maxConcurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options; - + try { const results = []; @@ -184,9 +181,9 @@ class DownloadExecutor { if (task.status === 'cancelled') { break; } - + const batch = newArtworks.slice(i, i + maxConcurrent); - const batchPromises = batch.map(async (artwork) => { + const batchPromises = batch.map(async artwork => { try { // 这里需要调用主下载服务的方法,暂时返回模拟结果 task.completed++; @@ -205,7 +202,7 @@ class DownloadExecutor { 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)); @@ -218,7 +215,6 @@ class DownloadExecutor { task.results = results; await this.taskManager.saveTasks(); this.progressManager.notifyProgressUpdate(task.id, task); - } catch (error) { task.status = 'failed'; task.error = error.message; @@ -233,7 +229,7 @@ class DownloadExecutor { */ async executeRankingDownload(task, newArtworks, options) { const { maxConcurrent = 3, size = 'original', quality = 'high', format = 'auto' } = options; - + try { const results = []; @@ -242,9 +238,9 @@ class DownloadExecutor { if (task.status === 'cancelled') { break; } - + const batch = newArtworks.slice(i, i + maxConcurrent); - const batchPromises = batch.map(async (artwork) => { + const batchPromises = batch.map(async artwork => { try { // 这里需要调用主下载服务的方法,暂时返回模拟结果 task.completed++; @@ -263,7 +259,7 @@ class DownloadExecutor { 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)); @@ -276,7 +272,6 @@ class DownloadExecutor { task.results = results; await this.taskManager.saveTasks(); this.progressManager.notifyProgressUpdate(task.id, task); - } catch (error) { task.status = 'failed'; task.error = error.message; @@ -287,4 +282,4 @@ class DownloadExecutor { } } -module.exports = DownloadExecutor; \ No newline at end of file +module.exports = DownloadExecutor; diff --git a/backend/services/download.js b/backend/services/download.js index 03b4f46..ec1d23b 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -15,10 +15,10 @@ class DownloadService { this.auth = auth; this.artworkService = new ArtworkService(auth); this.artistService = new ArtistService(auth); - + // 检测是否在pkg打包环境中运行 const isPkg = process.pkg !== undefined; - + if (isPkg) { // 在打包环境中,使用可执行文件所在目录 this.dataPath = path.join(process.cwd(), 'data'); @@ -26,19 +26,14 @@ class DownloadService { // 在开发环境中,使用相对路径 this.dataPath = path.join(__dirname, '../../data'); } - + // 初始化各个管理器 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.downloadExecutor = new DownloadExecutor(this.fileManager, this.taskManager, this.progressManager, this.historyManager); + this.initialized = false; } @@ -51,13 +46,13 @@ class DownloadService { const downloadPath = await this.fileManager.getDownloadPath(); await this.fileManager.ensureDirectory(downloadPath); await this.fileManager.ensureDirectory(this.dataPath); - + // 初始化各个管理器 await this.taskManager.init(); await this.historyManager.init(); - + this.initialized = true; - console.log('下载服务初始化完成,下载路径:', downloadPath); + // 下载服务初始化完成 } catch (error) { console.error('下载服务初始化失败:', error); this.initialized = false; @@ -87,17 +82,17 @@ class DownloadService { if (!task) { return { success: false, error: '任务不存在' }; } - + return { success: true, - data: task + data: task, }; } async getAllTasks() { return { success: true, - data: this.taskManager.getAllTasks() + data: this.taskManager.getAllTasks(), }; } @@ -106,12 +101,12 @@ class DownloadService { if (!task) { return { success: false, error: '任务不存在' }; } - + await this.taskManager.updateTask(taskId, { status: 'cancelled', - end_time: new Date() + end_time: new Date(), }); - + this.progressManager.notifyProgressUpdate(taskId, task); return { success: true }; } @@ -121,7 +116,7 @@ class DownloadService { if (!task) { return { success: false, error: '任务不存在' }; } - + await this.taskManager.updateTask(taskId, { status: 'paused' }); this.progressManager.notifyProgressUpdate(taskId, task); return { success: true }; @@ -132,14 +127,14 @@ class DownloadService { 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 }); } @@ -149,7 +144,7 @@ class DownloadService { const result = this.historyManager.getDownloadHistory(offset, limit); return { success: true, - data: result + data: result, }; } @@ -159,24 +154,22 @@ class DownloadService { const files = []; 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 this.fileManager.getFileInfo(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 this.fileManager.getFileInfo(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) - ); - + const imageFiles = artworkFiles.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)); + if (imageFiles.length > 0) { files.push({ artist: artist, @@ -184,14 +177,14 @@ class DownloadService { path: artworkPath, files: imageFiles, total_size: await this.fileManager.getDirectorySize(artworkPath), - created_at: artworkStat.created + created_at: artworkStat.created, }); } } } } } - + return files.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); } catch (error) { console.error('获取下载文件列表失败:', error); @@ -203,32 +196,30 @@ class DownloadService { try { const downloadedIds = new Set(); const downloadPath = await this.fileManager.getDownloadPath(); - + // 扫描下载目录获取所有已下载的作品ID const artists = await this.fileManager.listDirectory(downloadPath); - + for (const artist of artists) { const artistPath = path.join(downloadPath, artist); const artistStat = await this.fileManager.getFileInfo(artistPath); - + if (artistStat.exists && artistStat.isDirectory) { const artworks = await this.fileManager.listDirectory(artistPath); - + for (const artwork of artworks) { // 检查是否是作品目录(包含数字ID) const artworkMatch = artwork.match(/^(\d+)_(.+)$/); if (artworkMatch) { const artworkId = artworkMatch[1]; - + // 检查作品目录是否包含图片文件 const artworkPath = path.join(artistPath, artwork); const artworkStat = await this.fileManager.getFileInfo(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) - ); + const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)); if (imageFiles.length > 0) { downloadedIds.add(parseInt(artworkId)); } @@ -237,7 +228,7 @@ class DownloadService { } } } - + return Array.from(downloadedIds); } catch (error) { console.error('获取已下载作品ID列表失败:', error); @@ -247,68 +238,54 @@ class DownloadService { async isArtworkDownloaded(artworkId) { try { - console.log(`开始检查作品 ${artworkId} 的下载状态...`); const downloadPath = await this.fileManager.getDownloadPath(); - console.log(`下载路径: ${downloadPath}`); - + // 扫描所有作者目录 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}`); + if (!(await this.fileManager.fileExists(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} 个图片文件`); - + const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file) && file !== 'artwork_info.json'); + 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; } } } - - console.log(`作品 ${artworkId} 未找到`); + return false; } catch (error) { console.error('检查作品下载状态失败:', error); @@ -321,22 +298,19 @@ class DownloadService { */ async downloadArtwork(artworkId, options = {}) { const { size = 'original', quality = 'high', format = 'auto', skipExisting = true } = options; - + try { // 检查是否已下载 - if (skipExisting && await this.isArtworkDownloaded(artworkId)) { - console.log(`作品 ${artworkId} 已存在且完整,跳过下载`); + if (skipExisting && (await this.isArtworkDownloaded(artworkId))) { return { success: true, data: { task_id: null, artwork_id: artworkId, skipped: true, - message: '作品已存在且完整,跳过下载' - } + message: '作品已存在且完整,跳过下载', + }, }; - } else if (skipExisting) { - console.log(`作品 ${artworkId} 目录存在但不完整,将重新下载`); } // 获取作品信息 @@ -346,27 +320,27 @@ class DownloadService { } const artwork = artworkResult.data; - + // 确保作品信息完整 if (!artwork || !artwork.user || !artwork.title) { throw new Error('作品信息不完整'); } - + const artistName = this.fileManager.createSafeDirectoryName(artwork.user.name || 'Unknown Artist'); const artworkTitle = this.fileManager.createSafeDirectoryName(artwork.title || 'Untitled'); - + // 创建作品目录 const downloadPath = await this.fileManager.getDownloadPath(); const artistDir = path.join(downloadPath, artistName); const artworkDirName = `${artworkId}_${artworkTitle}`; const artworkDir = path.join(artistDir, artworkDirName); - + // 如果是重新下载,先删除现有目录 - if (!skipExisting && await this.fileManager.directoryExists(artworkDir)) { + if (!skipExisting && (await this.fileManager.directoryExists(artworkDir))) { console.log(`删除现有作品目录: ${artworkDir}`); await this.fileManager.removeDirectory(artworkDir); } - + await this.fileManager.ensureDirectory(artworkDir); // 获取图片URL @@ -376,7 +350,7 @@ class DownloadService { } const images = imagesResult.data.images; - + // 创建任务记录 const task = this.taskManager.createTask('artwork', { artwork_id: artworkId, @@ -384,14 +358,14 @@ class DownloadService { artwork_title: artworkTitle, total_files: images.length, completed_files: 0, - failed_files: 0 + failed_files: 0, }); - + await this.taskManager.saveTasks(); // 立即返回任务ID,异步执行下载 this.downloadExecutor.executeArtworkDownload(task, images, size, artworkDir, artwork); - + return { success: true, data: { @@ -400,15 +374,14 @@ class DownloadService { artist_name: artistName, artwork_title: artworkTitle, status: 'downloading', - message: '下载任务已创建,正在后台执行' - } + message: '下载任务已创建,正在后台执行', + }, }; - } catch (error) { console.error('下载作品失败:', error); return { success: false, - error: error.message + error: error.message, }; } } @@ -418,20 +391,18 @@ class DownloadService { */ async downloadMultipleArtworks(artworkIds, options = {}) { const { concurrent = 3, size = 'original', quality = 'high', format = 'auto', skipExisting = true } = options; - + try { // 检查重复下载 let filteredIds = artworkIds; let skippedCount = 0; - + if (skipExisting) { const downloadedIds = await this.getDownloadedArtworkIds(); const downloadedSet = new Set(downloadedIds); - + filteredIds = artworkIds.filter(id => !downloadedSet.has(id)); skippedCount = artworkIds.length - filteredIds.length; - - console.log(`批量下载: 总共 ${artworkIds.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${filteredIds.length} 个作品`); } // 创建任务记录 @@ -442,18 +413,18 @@ class DownloadService { completed: 0, failed: 0, skipped: skippedCount, - results: [] + results: [], }); - + await this.taskManager.saveTasks(); // 如果没有需要下载的作品,直接返回 if (filteredIds.length === 0) { await this.taskManager.updateTask(task.id, { status: 'completed', - end_time: new Date() + end_time: new Date(), }); - + return { success: true, data: { @@ -462,14 +433,14 @@ class DownloadService { completed_artworks: 0, failed_artworks: 0, skipped_artworks: skippedCount, - message: '所有作品都已下载完成' - } + message: '所有作品都已下载完成', + }, }; } // 异步执行批量下载 this.downloadExecutor.executeBatchDownload(task, artworkIds, options); - + return { success: true, data: { @@ -477,15 +448,14 @@ class DownloadService { total_artworks: task.total, completed_artworks: task.completed, failed_artworks: task.failed, - message: '批量下载任务已创建,正在后台执行' - } + message: '批量下载任务已创建,正在后台执行', + }, }; - } catch (error) { console.error('批量下载失败:', error); return { success: false, - error: error.message + error: error.message, }; } } @@ -494,17 +464,8 @@ class DownloadService { * 下载作者作品 */ async downloadArtistArtworks(artistId, options = {}) { - const { - type = 'art', - limit = 50, - size = 'original', - quality = 'high', - format = 'auto', - skipExisting = true, - maxConcurrent = 3, - pageSize = 30 - } = options; - + const { type = 'art', limit = 50, size = 'original', quality = 'high', format = 'auto', skipExisting = true, maxConcurrent = 3, pageSize = 30 } = options; + try { // 创建任务记录 const task = this.taskManager.createTask('artist', { @@ -513,9 +474,9 @@ class DownloadService { completed: 0, failed: 0, skipped: 0, - results: [] + results: [], }); - + await this.taskManager.saveTasks(); // 获取已下载的作品ID @@ -526,12 +487,12 @@ class DownloadService { let allArtworks = []; let offset = 0; let hasMore = true; - + while (hasMore && allArtworks.length < limit) { const artworksResult = await this.artistService.getArtistArtworks(artistId, { type, offset: offset, - limit: Math.min(pageSize, limit - allArtworks.length) + limit: Math.min(pageSize, limit - allArtworks.length), }); if (!artworksResult.success) { @@ -544,36 +505,34 @@ class DownloadService { } else { allArtworks.push(...artworks); offset += artworks.length; - + // 基于 next_url 判断是否还有更多页面 hasMore = !!artworksResult.data.next_url; - + // 添加延迟避免请求过于频繁 await new Promise(resolve => setTimeout(resolve, 500)); } } // 过滤已下载的作品 - const newArtworks = skipExisting - ? allArtworks.filter(artwork => !downloadedSet.has(artwork.id)) - : allArtworks; - + const newArtworks = skipExisting ? allArtworks.filter(artwork => !downloadedSet.has(artwork.id)) : allArtworks; + const skippedCount = allArtworks.length - newArtworks.length; - + await this.taskManager.updateTask(task.id, { skipped: skippedCount, - total: newArtworks.length + total: newArtworks.length, }); - console.log(`作者作品下载: 总共 ${allArtworks.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${newArtworks.length} 个作品`); + // 作者作品下载统计 // 如果没有需要下载的作品,直接返回 if (newArtworks.length === 0) { await this.taskManager.updateTask(task.id, { status: 'completed', - end_time: new Date() + end_time: new Date(), }); - + return { success: true, data: { @@ -583,14 +542,14 @@ class DownloadService { completed_artworks: 0, failed_artworks: 0, skipped_artworks: skippedCount, - message: '所有作品都已下载完成' - } + message: '所有作品都已下载完成', + }, }; } // 异步执行作者作品下载 this.downloadExecutor.executeArtistDownload(task, newArtworks, options); - + return { success: true, data: { @@ -599,15 +558,14 @@ class DownloadService { total_artworks: task.total, completed_artworks: task.completed, failed_artworks: task.failed, - message: '作者作品下载任务已创建,正在后台执行' - } + message: '作者作品下载任务已创建,正在后台执行', + }, }; - } catch (error) { console.error('作者作品下载失败:', error); return { success: false, - error: error.message + error: error.message, }; } } @@ -616,18 +574,8 @@ class DownloadService { * 下载排行榜作品 */ async downloadRankingArtworks(options = {}) { - const { - mode = 'day', - type = 'art', - limit = 50, - size = 'original', - quality = 'high', - format = 'auto', - skipExisting = true, - maxConcurrent = 3, - pageSize = 30 - } = options; - + const { mode = 'day', type = 'art', limit = 50, size = 'original', quality = 'high', format = 'auto', skipExisting = true, maxConcurrent = 3, pageSize = 30 } = options; + try { // 创建任务记录 const task = this.taskManager.createTask('ranking', { @@ -637,9 +585,9 @@ class DownloadService { completed: 0, failed: 0, skipped: 0, - results: [] + results: [], }); - + await this.taskManager.saveTasks(); // 获取已下载的作品ID @@ -650,11 +598,11 @@ class DownloadService { let allArtworks = []; let offset = 0; let hasMore = true; - + while (hasMore && allArtworks.length < limit) { const rankingResult = await this.getRankingArtworks(mode, type, { offset: offset, - limit: Math.min(pageSize, limit - allArtworks.length) + limit: Math.min(pageSize, limit - allArtworks.length), }); if (!rankingResult.success) { @@ -667,36 +615,34 @@ class DownloadService { } else { allArtworks.push(...artworks); offset += artworks.length; - + // 基于 next_url 判断是否还有更多页面 hasMore = !!rankingResult.data.next_url; - + // 添加延迟避免请求过于频繁 await new Promise(resolve => setTimeout(resolve, 500)); } } // 过滤已下载的作品 - const newArtworks = skipExisting - ? allArtworks.filter(artwork => !downloadedSet.has(artwork.id)) - : allArtworks; - + const newArtworks = skipExisting ? allArtworks.filter(artwork => !downloadedSet.has(artwork.id)) : allArtworks; + const skippedCount = allArtworks.length - newArtworks.length; - + await this.taskManager.updateTask(task.id, { skipped: skippedCount, - total: newArtworks.length + total: newArtworks.length, }); - console.log(`排行榜作品下载: 总共 ${allArtworks.length} 个作品,跳过 ${skippedCount} 个已下载的作品,需要下载 ${newArtworks.length} 个作品`); + // 排行榜作品下载统计 // 如果没有需要下载的作品,直接返回 if (newArtworks.length === 0) { await this.taskManager.updateTask(task.id, { status: 'completed', - end_time: new Date() + end_time: new Date(), }); - + return { success: true, data: { @@ -707,14 +653,14 @@ class DownloadService { completed_artworks: 0, failed_artworks: 0, skipped_artworks: skippedCount, - message: '所有作品都已下载完成' - } + message: '所有作品都已下载完成', + }, }; } // 异步执行排行榜作品下载 this.downloadExecutor.executeRankingDownload(task, newArtworks, options); - + return { success: true, data: { @@ -724,15 +670,14 @@ class DownloadService { total_artworks: task.total, completed_artworks: task.completed, failed_artworks: task.failed, - message: '排行榜作品下载任务已创建,正在后台执行' - } + message: '排行榜作品下载任务已创建,正在后台执行', + }, }; - } catch (error) { console.error('排行榜作品下载失败:', error); return { success: false, - error: error.message + error: error.message, }; } } @@ -742,36 +687,33 @@ class DownloadService { */ async getRankingArtworks(mode, type, options = {}) { const { offset = 0, limit = 30 } = options; - + try { // 使用作品服务来获取排行榜数据 const artworkService = new (require('./artwork'))(this.auth); - + const result = await artworkService.getRankingArtworks({ mode, content: type, offset, - limit + limit, }); - + return { success: true, data: { artworks: result.artworks, - next_url: result.next_url || null - } + next_url: result.next_url || null, + }, }; - } catch (error) { console.error('获取排行榜失败:', error); return { success: false, - error: error.message + error: error.message, }; } } - - } -module.exports = DownloadService; \ No newline at end of file +module.exports = DownloadService; diff --git a/backend/services/file-manager.js b/backend/services/file-manager.js index 72dd181..aa5edaf 100644 --- a/backend/services/file-manager.js +++ b/backend/services/file-manager.js @@ -3,6 +3,7 @@ const fs = require('fs-extra'); const path = require('path'); const crypto = require('crypto'); const ConfigManager = require('../config/config-manager'); +const FileUtils = require('../utils/file-utils'); /** * 文件管理器 - 负责文件下载、检查和目录管理 @@ -107,9 +108,13 @@ class FileManager { return new Promise((resolve, reject) => { writer.on('finish', resolve); - writer.on('error', (error) => { + writer.on('error', async (error) => { // 下载失败时删除文件 - fs.unlink(filePath, () => {}); + try { + await this.safeDeleteFile(filePath); + } catch (removeError) { + console.warn('清理失败文件时出错:', removeError.message); + } reject(error); }); }); @@ -149,14 +154,35 @@ class FileManager { * 创建安全的目录名 */ createSafeDirectoryName(name) { - return name.replace(/[<>:"/\\|?*]/g, '_'); + if (!name) return 'Untitled'; + + // 移除或替换Windows文件系统不允许的字符 + let safeName = name.replace(/[<>:"/\\|?*]/g, '_'); + + // 移除前后空格和点 + safeName = safeName.trim().replace(/^\.+|\.+$/g, ''); + + // 如果处理后为空,使用默认名称 + if (!safeName) { + safeName = 'Untitled'; + } + + // 限制长度,避免路径过长 + if (safeName.length > 100) { + safeName = safeName.substring(0, 100); + } + + return safeName; } /** * 确保目录存在 */ async ensureDirectory(dirPath) { - await fs.ensureDir(dirPath); + const success = await FileUtils.safeEnsureDir(dirPath); + if (!success) { + throw new Error(`目录创建失败: ${dirPath}`); + } } /** @@ -204,11 +230,23 @@ class FileManager { * 删除文件 */ async deleteFile(filePath) { - if (await fs.pathExists(filePath)) { - await fs.unlink(filePath); + try { + if (await fs.pathExists(filePath)) { + await fs.unlink(filePath); + } + } catch (error) { + console.error(`文件删除失败: ${filePath}`, error.message); + // 不抛出错误,避免影响其他操作 } } + /** + * 安全删除文件(兼容 pkg 打包) + */ + async safeDeleteFile(filePath) { + return await FileUtils.safeDeleteFile(filePath); + } + /** * 检查文件是否存在 */ diff --git a/backend/services/history-manager.js b/backend/services/history-manager.js index ba32c23..b767ef7 100644 --- a/backend/services/history-manager.js +++ b/backend/services/history-manager.js @@ -20,7 +20,7 @@ class HistoryManager { await fs.ensureDir(this.dataPath); await this.loadHistory(); this.initialized = true; - console.log('历史记录管理器初始化完成'); + // 历史记录管理器初始化完成 } catch (error) { console.error('历史记录管理器初始化失败:', error); this.initialized = false; diff --git a/backend/services/task-manager.js b/backend/services/task-manager.js index 7367af8..11b1b4a 100644 --- a/backend/services/task-manager.js +++ b/backend/services/task-manager.js @@ -21,7 +21,7 @@ class TaskManager { await fs.ensureDir(this.dataPath); await this.loadTasks(); this.initialized = true; - console.log('任务管理器初始化完成'); + // 任务管理器初始化完成 } catch (error) { console.error('任务管理器初始化失败:', error); this.initialized = false; @@ -36,7 +36,7 @@ class TaskManager { 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') { @@ -75,9 +75,9 @@ class TaskManager { start_time: new Date(), end_time: null, error: null, - ...data + ...data, }; - + this.tasks.set(taskId, task); return task; } @@ -97,7 +97,7 @@ class TaskManager { if (!task) { return false; } - + Object.assign(task, updates); await this.saveTasks(); return true; @@ -134,19 +134,19 @@ class TaskManager { 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; } @@ -161,17 +161,17 @@ class TaskManager { completed: 0, failed: 0, cancelled: 0, - partial: 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 +module.exports = TaskManager; diff --git a/backend/utils/file-utils.js b/backend/utils/file-utils.js new file mode 100644 index 0000000..afa38f5 --- /dev/null +++ b/backend/utils/file-utils.js @@ -0,0 +1,134 @@ +const fs = require('fs-extra'); +const path = require('path'); + +/** + * 文件操作工具类 - 确保与 pkg 打包兼容 + */ +class FileUtils { + /** + * 安全删除文件(兼容 pkg 打包) + */ + static async safeDeleteFile(filePath) { + try { + // 首先尝试使用 fs-extra + if (await fs.pathExists(filePath)) { + await fs.remove(filePath); + return true; + } + } catch (error) { + try { + // 降级到原生 fs + const nativeFs = require('fs').promises; + await nativeFs.unlink(filePath); + return true; + } catch (nativeError) { + console.error(`文件删除失败: ${filePath}`, nativeError.message); + return false; + } + } + return false; + } + + /** + * 安全创建目录(兼容 pkg 打包) + */ + static async safeEnsureDir(dirPath) { + try { + // 首先尝试使用 fs-extra + await fs.ensureDir(dirPath); + return true; + } catch (error) { + try { + // 降级到原生 fs + const nativeFs = require('fs').promises; + await nativeFs.mkdir(dirPath, { recursive: true }); + return true; + } catch (nativeError) { + console.error(`目录创建失败: ${dirPath}`, nativeError.message); + return false; + } + } + } + + /** + * 安全检查文件是否存在(兼容 pkg 打包) + */ + static async safePathExists(filePath) { + try { + // 首先尝试使用 fs-extra + return await fs.pathExists(filePath); + } catch (error) { + try { + // 降级到原生 fs + const nativeFs = require('fs').promises; + await nativeFs.access(filePath); + return true; + } catch (nativeError) { + return false; + } + } + } + + /** + * 安全读取目录(兼容 pkg 打包) + */ + static async safeReadDir(dirPath) { + try { + // 首先尝试使用 fs-extra + return await fs.readdir(dirPath); + } catch (error) { + try { + // 降级到原生 fs + const nativeFs = require('fs').promises; + return await nativeFs.readdir(dirPath); + } catch (nativeError) { + console.error(`读取目录失败: ${dirPath}`, nativeError.message); + return []; + } + } + } + + /** + * 安全写入 JSON 文件(兼容 pkg 打包) + */ + static async safeWriteJson(filePath, data, options = {}) { + try { + // 首先尝试使用 fs-extra + await fs.writeJson(filePath, data, options); + return true; + } catch (error) { + try { + // 降级到原生 fs + const nativeFs = require('fs').promises; + const jsonString = JSON.stringify(data, null, options.spaces || 2); + await nativeFs.writeFile(filePath, jsonString, 'utf8'); + return true; + } catch (nativeError) { + console.error(`JSON 写入失败: ${filePath}`, nativeError.message); + return false; + } + } + } + + /** + * 检测是否在 pkg 打包环境中运行 + */ + static isPkgEnvironment() { + return process.pkg !== undefined; + } + + /** + * 获取当前运行环境信息 + */ + static getEnvironmentInfo() { + return { + isPkg: this.isPkgEnvironment(), + nodeVersion: process.version, + platform: process.platform, + arch: process.arch, + pkgVersion: process.pkg ? process.pkg.version : null, + }; + } +} + +module.exports = FileUtils;