diff --git a/.gitignore b/.gitignore index 6b5ca25..3cf4c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # 环境 node_modules/ + +test + # 下载文件夹 downloads/ data/ diff --git a/backend/config/cache-config.js b/backend/config/cache-config.js index c4fef05..6f54260 100644 --- a/backend/config/cache-config.js +++ b/backend/config/cache-config.js @@ -48,6 +48,9 @@ class CacheConfigManager { chunkSize: 1024 * 1024, // 1MB块大小 retryAttempts: 3, // 重试次数 retryDelay: 2000, // 重试延迟 + // 下载检测配置 + useRegistryCheck: true, // 是否使用注册表检测(默认启用) + fallbackToScan: false, // 检测失败时是否回退到扫盘检测 maxFileSize: 50 * 1024 * 1024, // 最大文件大小 50MB }, // 新增Windows特定配置 diff --git a/backend/core.js b/backend/core.js index 83465f4..6fa7bd9 100644 --- a/backend/core.js +++ b/backend/core.js @@ -8,7 +8,7 @@ const CONFIG_FILE_DIR = require('appdata-path').getAppDataPath('pmanager'); const CONFIG_FILE = Path.resolve(CONFIG_FILE_DIR, 'config.json'); // 创建logger实例 -const logger = defaultLogger.child('PixivBackend'); +const logger = defaultLogger.child('PixivCore'); // 默认配置 const defaultConfig = { diff --git a/backend/middleware/logger.js b/backend/middleware/logger.js index 2d2ca6b..1d1b99b 100644 --- a/backend/middleware/logger.js +++ b/backend/middleware/logger.js @@ -75,7 +75,7 @@ function loggerMiddleware(req, res, next) { const isArtistArtworksQuery = /^\/api\/artist\/\d+\/artworks/.test(req.path); // 过滤掉作品详情请求 - const isArtworkDetailQuery = /^\/api\/artwork\/\d+/.test(req.path); + const isArtworkDetailQuery = /^\/(?:api\/)?artwork\/\d+/.test(req.path); // 过滤掉仓库下载检查请求 const isRepositoryCheckDownloadedQuery = /^\/api\/repository\/check-downloaded\/\d+/.test(req.path); diff --git a/backend/routes/download.js b/backend/routes/download.js index 693509e..2cab2e1 100644 --- a/backend/routes/download.js +++ b/backend/routes/download.js @@ -753,4 +753,193 @@ router.get('/stats', async (req, res) => { } }); -module.exports = router; \ No newline at end of file +/** + * 导出下载注册表 + * GET /api/download/registry/export + */ +router.get('/registry/export', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const registryData = await downloadService.downloadRegistry.exportRegistry(); + + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Disposition', 'attachment; filename="download-registry.json"'); + res.json(registryData); + } catch (error) { + logger.error('导出下载注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 导入下载注册表 + * POST /api/download/registry/import + */ +router.post('/registry/import', async (req, res) => { + try { + const { registryData } = req.body; + + if (!registryData) { + return res.status(400).json({ + success: false, + error: '缺少注册表数据' + }); + } + + const downloadService = req.backend.getDownloadService(); + const result = await downloadService.downloadRegistry.importRegistry(registryData); + + res.json({ + success: true, + data: result + }); + } catch (error) { + logger.error('导入下载注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 手动全盘扫描更新注册表 + * POST /api/download/registry/rebuild + */ +router.post('/registry/rebuild', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const fileManager = downloadService.fileManager; + const result = await downloadService.downloadRegistry.rebuildFromFileSystem(fileManager); + + res.json({ + success: true, + data: result + }); + } catch (error) { + logger.error('重建下载注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取下载注册表统计信息 + * GET /api/download/registry/stats + */ +router.get('/registry/stats', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const stats = await downloadService.downloadRegistry.getStats(); + + res.json({ + success: true, + data: stats + }); + } catch (error) { + logger.error('获取下载注册表统计信息失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 清理下载注册表 + * POST /api/download/registry/cleanup + */ +router.post('/registry/cleanup', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const fileManager = downloadService.fileManager; + const result = await downloadService.downloadRegistry.cleanupRegistry(fileManager); + + res.json({ + success: true, + data: result + }); + } catch (error) { + logger.error('清理下载注册表失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 获取下载检测配置 + * GET /api/download/registry/config + */ +router.get('/registry/config', async (req, res) => { + try { + const downloadService = req.backend.getDownloadService(); + const config = await downloadService.cacheConfigManager.loadConfig(); + + // 提取下载相关的配置 + const downloadConfig = { + useRegistryCheck: config.download?.useRegistryCheck !== false, // 默认启用 + fallbackToScan: config.download?.fallbackToScan === true // 默认不启用 + }; + + res.json({ + success: true, + data: downloadConfig + }); + } catch (error) { + logger.error('获取下载检测配置失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +/** + * 更新下载检测配置 + * PUT /api/download/registry/config + */ +router.put('/registry/config', async (req, res) => { + try { + const { useRegistryCheck, fallbackToScan } = req.body; + + if (typeof useRegistryCheck !== 'boolean' || typeof fallbackToScan !== 'boolean') { + return res.status(400).json({ + success: false, + error: '配置参数必须是布尔值' + }); + } + + const downloadService = req.backend.getDownloadService(); + + // 更新配置 + const updatedConfig = await downloadService.cacheConfigManager.updateConfig({ + download: { + useRegistryCheck, + fallbackToScan + } + }); + + res.json({ + success: true, + data: { + useRegistryCheck: updatedConfig.download?.useRegistryCheck !== false, + fallbackToScan: updatedConfig.download?.fallbackToScan === true + } + }); + } catch (error) { + logger.error('更新下载检测配置失败:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/repository.js b/backend/routes/repository.js index 777e7f3..00fa3bb 100644 --- a/backend/routes/repository.js +++ b/backend/routes/repository.js @@ -141,7 +141,7 @@ router.get('/artworks/:artworkId', async (req, res) => { router.delete('/artworks/:artworkId', async (req, res) => { try { const { artworkId } = req.params - const result = await repositoryService.deleteArtwork(artworkId) + const result = await repositoryService.deleteArtwork(artworkId, req) res.json(ResponseUtil.success(result)) } catch (error) { res.status(500).json(ResponseUtil.error(error.message)) @@ -380,4 +380,4 @@ function getContentType(extension) { return contentTypes[extension.toLowerCase()] || 'application/octet-stream' } -module.exports = router \ No newline at end of file +module.exports = router \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index ac68906..262c74a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,6 +28,8 @@ class PixivServer { this.app = express(); this.backend = null; this.port = 3000; // 默认端口,会在init时重新设置 + this.logLevel = process.env.LOG_LEVEL || 'info'; // 获取日志级别 + this.isVerboseMode = ['debug', 'trace'].includes(this.logLevel.toLowerCase()); // 检查是否为详细模式 } /** @@ -39,6 +41,17 @@ class PixivServer { // 重新设置端口(从环境变量获取) this.port = process.env.PORT || 3000; + // 如果启用了详细模式,输出调试信息 + if (this.isVerboseMode) { + logger.info(`详细模式已启用 (日志级别: ${this.logLevel.toUpperCase()})`); + logger.debug('环境变量:', { + NODE_ENV: process.env.NODE_ENV, + PORT: process.env.PORT, + PROXY_PORT: process.env.PROXY_PORT, + LOG_LEVEL: process.env.LOG_LEVEL + }); + } + // 设置代理 proxyConfig.setEnvironmentVariables(); @@ -106,6 +119,11 @@ class PixivServer { if (this.backend.isLoggedIn) { logger.info(`用户: ${this.backend.config.user?.account}`); } + if (this.isVerboseMode) { + logger.info(`日志级别: ${this.logLevel.toUpperCase()}`); + logger.debug(`服务器端口: ${this.port}`); + logger.debug(`代理端口: ${process.env.PROXY_PORT || '未设置'}`); + } logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); }); } diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index 8efc91f..ed13b70 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -139,6 +139,19 @@ class DownloadExecutor { await this.taskManager.saveTasks(); this.progressManager.notifyProgressUpdate(task.id, task); + // 如果下载成功,更新下载注册表 + if (task.status === 'completed') { + try { + await this.downloadService.downloadRegistry.addArtwork(task.artist_name, task.artwork_id); + logger.debug('已更新下载注册表', { + artistName: task.artist_name, + artworkId: task.artwork_id + }); + } catch (error) { + logger.warn('更新下载注册表失败:', error.message); + } + } + // 添加到历史记录 const historyItem = { id: task.id, diff --git a/backend/services/download-registry.js b/backend/services/download-registry.js new file mode 100644 index 0000000..7dc27d0 --- /dev/null +++ b/backend/services/download-registry.js @@ -0,0 +1,557 @@ +const path = require('path'); +const fs = require('fs-extra'); +const { defaultLogger } = require('../utils/logger'); + +// 创建logger实例 +const logger = defaultLogger.child('DownloadRegistry'); + +/** + * 下载记录管理器 - 维护已下载作品的JSON记录 + * 用于快速检测作品是否已下载,支持导入导出和多设备同步 + */ +class DownloadRegistry { + constructor(dataPath) { + this.dataPath = dataPath; + this.registryPath = path.join(dataPath, 'download_registry.json'); + this.registry = { + version: '1.0.5', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + artists: {} // 格式: { artistName: { artworks: [artworkId1, artworkId2, ...] } } + }; + this.loaded = false; + } + + /** + * 初始化注册表 + */ + async init() { + try { + // 确保数据目录存在 + await fs.ensureDir(this.dataPath); + + // 加载现有注册表 + await this.loadRegistry(); + + logger.info(`下载记录注册表初始化完成,总共包含${Object.keys(this.registry.artists).length}个作者,${this.getTotalArtworkCount()}个工作品`); + } catch (error) { + logger.error('下载记录注册表初始化失败:', error); + throw error; + } + } + + /** + * 加载注册表文件 + */ + async loadRegistry() { + try { + if (await fs.pathExists(this.registryPath)) { + const data = await fs.readJson(this.registryPath); + + // 验证数据格式 + if (data && typeof data === 'object' && data.artists) { + this.registry = { + version: data.version || '1.0.0', + created_at: data.created_at || new Date().toISOString(), + updated_at: data.updated_at || new Date().toISOString(), + artists: data.artists || {} + }; + } else { + logger.warn('注册表文件格式不正确,使用默认格式'); + } + } else { + logger.info('注册表文件不存在,将创建新的注册表'); + } + + this.loaded = true; + } catch (error) { + logger.error('加载注册表文件失败:', error); + // 使用默认注册表 + this.loaded = true; + } + } + + /** + * 保存注册表到文件 + */ + async saveRegistry() { + try { + this.registry.updated_at = new Date().toISOString(); + await fs.writeJson(this.registryPath, this.registry, { spaces: 2 }); + logger.debug('注册表已保存到文件', { path: this.registryPath }); + } catch (error) { + logger.error('保存注册表失败:', error); + throw error; + } + } + + /** + * 添加已下载的作品记录 + * @param {string} artistName - 作者名称 + * @param {number|string} artworkId - 作品ID + */ + async addArtwork(artistName, artworkId) { + if (!this.loaded) { + await this.loadRegistry(); + } + + const normalizedArtistName = this.normalizeArtistName(artistName); + const normalizedArtworkId = parseInt(artworkId); + + if (!this.registry.artists[normalizedArtistName]) { + this.registry.artists[normalizedArtistName] = { + artworks: [] + }; + } + + // 检查是否已存在 + if (!this.registry.artists[normalizedArtistName].artworks.includes(normalizedArtworkId)) { + this.registry.artists[normalizedArtistName].artworks.push(normalizedArtworkId); + this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a); // 按ID倒序排列 + + await this.saveRegistry(); + logger.debug('添加作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } + } + + /** + * 移除作品记录 + * @param {string} artistName - 作者名称 + * @param {number|string} artworkId - 作品ID + */ + async removeArtwork(artistName, artworkId) { + if (!this.loaded) { + await this.loadRegistry(); + } + + const normalizedArtistName = this.normalizeArtistName(artistName); + const normalizedArtworkId = parseInt(artworkId); + + logger.debug('开始移除作品记录', { + originalArtistName: artistName, + normalizedArtistName: normalizedArtistName, + artworkId: normalizedArtworkId + }); + + if (this.registry.artists[normalizedArtistName]) { + const artworks = this.registry.artists[normalizedArtistName].artworks; + const index = artworks.indexOf(normalizedArtworkId); + + logger.debug('查找作品在注册表中的位置', { + artistName: normalizedArtistName, + artworkId: normalizedArtworkId, + index: index, + artworks: artworks + }); + + if (index !== -1) { + artworks.splice(index, 1); + + // 如果作者下没有作品了,删除作者记录 + if (artworks.length === 0) { + delete this.registry.artists[normalizedArtistName]; + logger.info('作者下无作品,删除作者记录', { artistName: normalizedArtistName }); + } + + await this.saveRegistry(); + logger.debug('成功移除作品记录', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } else { + logger.warn('作品在注册表中未找到', { artistName: normalizedArtistName, artworkId: normalizedArtworkId }); + } + } else { + logger.warn('作者在注册表中未找到', { artistName: normalizedArtistName }); + } + } + + /** + * 检查作品是否已下载 + * @param {number|string} artworkId - 作品ID + * @returns {boolean} 是否已下载 + */ + async isArtworkDownloaded(artworkId) { + if (!this.loaded) { + await this.loadRegistry(); + } + + const normalizedArtworkId = parseInt(artworkId); + + // 遍历所有作者查找作品 + for (const artistName in this.registry.artists) { + if (this.registry.artists[artistName].artworks.includes(normalizedArtworkId)) { + return true; + } + } + + return false; + } + + /** + * 获取已下载的作品ID列表 + * @returns {number[]} 作品ID数组 + */ + async getDownloadedArtworkIds() { + if (!this.loaded) { + await this.loadRegistry(); + } + + const artworkIds = new Set(); + + for (const artistName in this.registry.artists) { + for (const artworkId of this.registry.artists[artistName].artworks) { + artworkIds.add(artworkId); + } + } + + return Array.from(artworkIds).sort((a, b) => b - a); + } + + /** + * 获取指定作者的已下载作品 + * @param {string} artistName - 作者名称 + * @returns {number[]} 作品ID数组 + */ + async getArtistArtworks(artistName) { + if (!this.loaded) { + await this.loadRegistry(); + } + + const normalizedArtistName = this.normalizeArtistName(artistName); + + if (this.registry.artists[normalizedArtistName]) { + return [...this.registry.artists[normalizedArtistName].artworks]; + } + + return []; + } + + /** + * 获取所有已下载的作者列表 + * @returns {string[]} 作者名称数组 + */ + async getDownloadedArtists() { + if (!this.loaded) { + await this.loadRegistry(); + } + + return Object.keys(this.registry.artists); + } + + /** + * 获取统计信息 + */ + async getStats() { + if (!this.loaded) { + await this.loadRegistry(); + } + + const artists = Object.keys(this.registry.artists); + const totalArtworks = this.getTotalArtworkCount(); + + return { + artistCount: artists.length, + artworkCount: totalArtworks, + version: this.registry.version, + created_at: this.registry.created_at, + updated_at: this.registry.updated_at + }; + } + + /** + * 导出注册表数据 + * @returns {Object} 注册表数据 + */ + async exportRegistry() { + if (!this.loaded) { + await this.loadRegistry(); + } + + return { + ...this.registry, + exported_at: new Date().toISOString() + }; + } + + /** + * 导入注册表数据(增量导入,不覆盖现有数据) + * @param {Object} importData - 要导入的数据 + * @returns {Object} 导入结果统计 + */ + async importRegistry(importData) { + if (!this.loaded) { + await this.loadRegistry(); + } + + if (!importData || !importData.artists) { + throw new Error('导入数据格式不正确'); + } + + let addedArtists = 0; + let addedArtworks = 0; + let skippedArtworks = 0; + + for (const artistName in importData.artists) { + const normalizedArtistName = this.normalizeArtistName(artistName); + const importArtworks = importData.artists[artistName].artworks || []; + + if (!this.registry.artists[normalizedArtistName]) { + this.registry.artists[normalizedArtistName] = { artworks: [] }; + addedArtists++; + } + + const existingArtworks = new Set(this.registry.artists[normalizedArtistName].artworks); + + for (const artworkId of importArtworks) { + const normalizedArtworkId = parseInt(artworkId); + if (!existingArtworks.has(normalizedArtworkId)) { + this.registry.artists[normalizedArtistName].artworks.push(normalizedArtworkId); + addedArtworks++; + } else { + skippedArtworks++; + } + } + + // 排序 + this.registry.artists[normalizedArtistName].artworks.sort((a, b) => b - a); + } + + await this.saveRegistry(); + + const result = { + addedArtists, + addedArtworks, + skippedArtworks, + totalArtists: Object.keys(this.registry.artists).length, + totalArtworks: this.getTotalArtworkCount() + }; + + logger.info('注册表导入完成', result); + return result; + } + + /** + * 从文件系统扫描并重建注册表 + * @param {Object} fileManager - 文件管理器实例 + * @returns {Object} 扫描结果统计 + */ + async rebuildFromFileSystem(fileManager) { + try { + logger.info('开始从文件系统扫描并添加新作品到注册表...'); + + if (!this.loaded) { + await this.loadRegistry(); + } + + let scannedArtists = 0; + let scannedArtworks = 0; + let addedArtworks = 0; + let skippedArtworks = 0; + + const downloadPath = await fileManager.getDownloadPath(); + logger.debug(`扫描下载路径: ${downloadPath}`); + + const artists = await fileManager.listDirectory(downloadPath); + logger.debug(`找到 ${artists.length} 个作者目录`); + + for (const artist of artists) { + try { + const artistPath = path.join(downloadPath, artist); + const artistStat = await fileManager.getFileInfo(artistPath); + + if (artistStat.exists && artistStat.isDirectory) { + scannedArtists++; + logger.debug(`扫描作者: ${artist}`); + + const artworks = await fileManager.listDirectory(artistPath); + + for (const artwork of artworks) { + try { + const artworkPath = path.join(artistPath, artwork); + const artworkStat = await fileManager.getFileInfo(artworkPath); + + if (artworkStat.exists && artworkStat.isDirectory) { + scannedArtworks++; + + // 检查是否是作品目录(包含数字ID) + const artworkMatch = artwork.match(/^(\d+)_(.+)$/); + if (artworkMatch) { + const artworkId = parseInt(artworkMatch[1]); + + // 检查作品是否已经在注册表中 + const isAlreadyRegistered = await this.isArtworkDownloaded(artworkId); + if (isAlreadyRegistered) { + skippedArtworks++; + continue; // 跳过已注册的作品 + } + + // 检查作品信息文件和图片文件 + const infoPath = path.join(artworkPath, 'artwork_info.json'); + let artworkInfo; + try { + const infoContent = await fs.readFile(infoPath, 'utf8'); + artworkInfo = JSON.parse(infoContent); + } catch (error) { + logger.debug(`读取作品信息文件失败: ${infoPath}`, error); + continue; // 跳过没有信息文件的目录 + } + + // 检查是否有图片文件 + const files = await fileManager.listDirectory(artworkPath); + const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)); + + if (imageFiles.length > 0) { + // 检查图片数量是否与artwork_info.json中记录的一致 + const expectedImageCount = artworkInfo.page_count || 1; + if (imageFiles.length >= expectedImageCount) { + // 添加到注册表(只添加新的) + await this.addArtwork(artist, artworkId); + addedArtworks++; + logger.debug(`添加作品到注册表: ${artist} - ${artworkId}`); + } else { + logger.debug(`作品图片数量不足: ${artist} - ${artworkId}, 期望: ${expectedImageCount}, 实际: ${imageFiles.length}`); + } + } else { + logger.debug(`作品目录无图片文件: ${artworkPath}`); + } + } + } + } catch (error) { + logger.debug(`处理作品目录 ${artwork} 时出错:`, error); + continue; // 跳过有问题的作品目录 + } + } + } + } catch (error) { + logger.debug(`处理作者目录 ${artist} 时出错:`, error); + continue; // 跳过有问题的作者目录 + } + } + + const result = { + scannedArtists, + scannedArtworks, + addedArtworks, + skippedArtworks, + totalRegisteredArtists: Object.keys(this.registry.artists).length, + totalRegisteredArtworks: this.getTotalArtworkCount() + }; + + logger.info('注册表扫描完成', result); + return result; + } catch (error) { + logger.error('注册表扫描失败:', error); + throw error; + } + } + + /** + * 清理注册表(移除不存在的记录) + * @param {Object} fileManager - 文件管理器实例 + * @returns {Object} 清理结果统计 + */ + async cleanupRegistry(fileManager) { + try { + if (!this.loaded) { + await this.loadRegistry(); + } + + logger.info('开始清理注册表...'); + + let removedArtists = 0; + let removedArtworks = 0; + const downloadPath = await fileManager.getDownloadPath(); + + for (const artistName in this.registry.artists) { + const artworks = [...this.registry.artists[artistName].artworks]; + let validArtworks = []; + + for (const artworkId of artworks) { + // 检查作品目录是否存在 + let found = false; + + try { + const artistPath = path.join(downloadPath, artistName); + if (await fileManager.directoryExists(artistPath)) { + const artworkEntries = await fileManager.listDirectory(artistPath); + + for (const entry of artworkEntries) { + const match = entry.match(/^(\d+)_(.+)$/); + if (match && parseInt(match[1]) === artworkId) { + const artworkPath = path.join(artistPath, entry); + const infoPath = path.join(artworkPath, 'artwork_info.json'); + + // 检查信息文件是否存在 + if (await fs.pathExists(infoPath)) { + found = true; + break; + } + } + } + } + } catch (error) { + logger.debug(`检查作品 ${artworkId} 时出错:`, error); + } + + if (found) { + validArtworks.push(artworkId); + } else { + removedArtworks++; + logger.debug(`移除无效作品记录: ${artistName} - ${artworkId}`); + } + } + + if (validArtworks.length > 0) { + this.registry.artists[artistName].artworks = validArtworks; + } else { + delete this.registry.artists[artistName]; + removedArtists++; + logger.debug(`移除空作者记录: ${artistName}`); + } + } + + await this.saveRegistry(); + + const result = { + removedArtists, + removedArtworks, + remainingArtists: Object.keys(this.registry.artists).length, + remainingArtworks: this.getTotalArtworkCount() + }; + + logger.info('注册表清理完成', result); + return result; + } catch (error) { + logger.error('注册表清理失败:', error); + throw error; + } + } + + /** + * 标准化作者名称(处理特殊字符) + */ + normalizeArtistName(artistName) { + if (!artistName || typeof artistName !== 'string') { + return 'Unknown Artist'; + } + return artistName.trim(); + } + + /** + * 获取总作品数量 + */ + getTotalArtworkCount() { + let total = 0; + for (const artistName in this.registry.artists) { + total += this.registry.artists[artistName].artworks.length; + } + return total; + } + + /** + * 获取注册表文件路径 + */ + getRegistryPath() { + return this.registryPath; + } +} + +module.exports = DownloadRegistry; \ No newline at end of file diff --git a/backend/services/download.js b/backend/services/download.js index 4900438..3719e5a 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -6,6 +6,7 @@ const FileManager = require('./file-manager'); const ProgressManager = require('./progress-manager'); const HistoryManager = require('./history-manager'); const DownloadExecutor = require('./download-executor'); +const DownloadRegistry = require('./download-registry'); const CacheConfigManager = require('../config/cache-config'); const fs = require('fs-extra'); // Added for fs-extra const { defaultLogger } = require('../utils/logger'); @@ -40,6 +41,7 @@ class DownloadService { this.taskManager = new TaskManager(this.dataPath); this.progressManager = new ProgressManager(); this.historyManager = new HistoryManager(this.dataPath); + this.downloadRegistry = new DownloadRegistry(this.dataPath); // 先创建下载执行器,稍后在init方法中设置downloadService引用 this.downloadExecutor = new DownloadExecutor(this.fileManager, this.taskManager, this.progressManager, this.historyManager, this); @@ -78,6 +80,7 @@ class DownloadService { // 初始化各个管理器 await this.taskManager.init(); await this.historyManager.init(); + await this.downloadRegistry.init(); this.initialized = true; // 下载服务初始化完成 @@ -463,6 +466,40 @@ class DownloadService { } async isArtworkDownloaded(artworkId) { + try { + // 获取配置,决定使用哪种检测方式 + const cacheConfig = await this.cacheConfigManager.loadConfig(); + const useRegistryCheck = cacheConfig.download?.useRegistryCheck !== false; // 默认启用 + const fallbackToScan = cacheConfig.download?.fallbackToScan === true; // 默认不启用 + + // 优先使用注册表检测(如果启用) + if (useRegistryCheck) { + try { + const isDownloaded = await this.downloadRegistry.isArtworkDownloaded(artworkId); + if (isDownloaded || !fallbackToScan) { + return isDownloaded; + } + // 如果注册表显示未下载但启用了回退,继续使用扫盘检测 + } catch (error) { + logger.warn('注册表检测失败,使用扫盘检测:', error.message); + if (!fallbackToScan) { + return false; + } + } + } + + // 使用原有的扫盘检测逻辑 + return await this.isArtworkDownloadedByScan(artworkId); + } catch (error) { + logger.error('检查作品下载状态失败:', error); + return false; + } + } + + /** + * 通过扫描文件系统检测作品是否已下载(原有逻辑) + */ + async isArtworkDownloadedByScan(artworkId) { try { const downloadPath = await this.fileManager.getDownloadPath(); @@ -520,7 +557,7 @@ class DownloadService { return false; } catch (error) { - logger.error('检查作品下载状态失败:', error); + logger.error('扫盘检查作品下载状态失败:', error); return false; } } @@ -1077,6 +1114,79 @@ class DownloadService { }; } } + + /** + * 删除已下载的文件 + * @param {string} artist - 作者名称 + * @param {string} artwork - 作品目录名称 + * @returns {Object} 删除结果 + */ + async deleteDownloadedFiles(artist, artwork) { + + try { + const downloadPath = await this.fileManager.getDownloadPath(); + const artworkPath = path.join(downloadPath, artist, artwork); + + // 检查作品目录是否存在 + const artworkStat = await this.fileManager.getFileInfo(artworkPath); + if (!artworkStat.exists) { + return { + success: false, + error: '作品目录不存在' + }; + } + + // 从作品目录名称中提取作品ID + const artworkMatch = artwork.match(/^(\d+)_(.+)$/); + if (!artworkMatch) { + return { + success: false, + error: '无效的作品目录格式' + }; + } + + const artworkId = parseInt(artworkMatch[1]); + + // 删除作品目录 + await this.fileManager.removeDirectory(artworkPath); + + // 从注册表中移除作品记录 + try { + await this.downloadRegistry.removeArtwork(artist, artworkId); + logger.debug('已从下载注册表中移除作品', { + artistName: artist, + artworkId: artworkId + }); + } catch (error) { + logger.warn('从下载注册表中移除作品失败:', error.message); + } + + // 检查作者目录是否为空,如果为空则删除 + const artistPath = path.join(downloadPath, artist); + try { + const artistEntries = await this.fileManager.listDirectory(artistPath); + const hasArtworks = artistEntries.some(entry => entry.match(/^\d+_/)); + + if (!hasArtworks) { + await this.fileManager.removeDirectory(artistPath); + logger.debug('已删除空的作者目录', { artistName: artist }); + } + } catch (error) { + logger.warn(`检查作者目录失败: ${error.message}`); + } + + return { + success: true, + message: '作品删除成功' + }; + } catch (error) { + logger.error('删除作品失败:', error); + return { + success: false, + error: error.message + }; + } + } } module.exports = DownloadService; diff --git a/backend/services/repository.js b/backend/services/repository.js index 9418159..5eb76e1 100644 --- a/backend/services/repository.js +++ b/backend/services/repository.js @@ -738,7 +738,7 @@ class RepositoryService { } // 删除作品 - async deleteArtwork(artworkId) { + async deleteArtwork(artworkId, req) { try { // 优化:直接通过文件系统查找,避免全仓库扫描 const artwork = await this.findArtworkByIdOptimized(artworkId) @@ -746,6 +746,23 @@ class RepositoryService { throw new Error('作品不存在') } + // 从注册表中移除作品记录 + try { + // 使用共享的下载服务实例,而不是创建新实例 + const downloadService = req.backend?.getDownloadService(); + if (downloadService) { + await downloadService.downloadRegistry.removeArtwork(artwork.artist, artworkId); + logger.debug('已从下载注册表中移除作品', { + artistName: artwork.artist, + artworkId: artworkId + }); + } else { + logger.warn('无法获取下载服务实例,跳过注册表更新'); + } + } catch (error) { + logger.warn('从下载注册表中移除作品失败:', error.message); + } + await fs.rm(artwork.path, { recursive: true, force: true }) // 优化:直接检查作者目录是否为空,避免重复扫描 diff --git a/backend/start.js b/backend/start.js index 301e26c..3b083bc 100644 --- a/backend/start.js +++ b/backend/start.js @@ -11,10 +11,6 @@ if (!process.env.UV_THREADPOOL_SIZE) { } const PixivServer = require('./server'); -const { defaultLogger } = require('./utils/logger'); - -// 创建logger实例 -const logger = defaultLogger.child('Start'); // 解析命令行参数 function parseArguments() { @@ -35,7 +31,12 @@ function parseArguments() { if (!isNaN(port)) { options.serverPort = port; } - } + } else if (arg.startsWith('--log-level=')) { + const level = arg.split('=')[1].toUpperCase(); + if (['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(level)) { + options.logLevel = level; + } + } // 处理 --key value 格式(向后兼容) else if (arg === '--proxy-port' && i + 1 < args.length) { const port = parseInt(args[i + 1]); @@ -49,6 +50,12 @@ function parseArguments() { options.serverPort = port; } i++; // 跳过下一个参数 + } else if (arg === '--log-level' && i + 1 < args.length) { + const level = args[i + 1].toUpperCase(); + if (['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(level)) { + options.logLevel = level; + } + i++; // 跳过下一个参数 } } @@ -61,6 +68,15 @@ const cliOptions = parseArguments(); // 设置环境变量 process.env.NODE_ENV = process.env.NODE_ENV || 'development'; +// 设置日志级别环境变量 +if (cliOptions.logLevel) { + process.env.LOG_LEVEL = cliOptions.logLevel.toLowerCase(); +} + +// 在设置环境变量后导入logger +const { defaultLogger } = require('./utils/logger'); +const logger = defaultLogger.child('Start'); + // 如果提供了代理端口,设置环境变量 if (cliOptions.proxyPort) { process.env.PROXY_PORT = cliOptions.proxyPort.toString(); @@ -73,6 +89,11 @@ if (cliOptions.serverPort) { logger.info(`服务器端口已设置为: ${cliOptions.serverPort}`); } +// 输出日志级别信息 +if (cliOptions.logLevel) { + logger.info(`日志级别: ${cliOptions.logLevel}`); +} + logger.info('启动 Pixiv 后端服务器...'); // 创建服务器实例 diff --git a/backend/test-login.js b/backend/test-login.js deleted file mode 100644 index 7f730a3..0000000 --- a/backend/test-login.js +++ /dev/null @@ -1,203 +0,0 @@ -const PixivBackend = require('./core'); -const proxyConfig = require('./config'); -const readline = require('readline'); -const { defaultLogger } = require('./utils/logger'); - -// 创建logger实例 -const logger = defaultLogger.child('TestLogin'); - - -// 创建命令行交互接口 -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); - -// 询问用户输入 -function askQuestion(question) { - return new Promise((resolve) => { - rl.question(question, (answer) => { - resolve(answer); - }); - }); -} - -// 测试登录流程 -async function testLogin() { - logger.info('=== Pixiv 登录测试脚本 ===\n'); - - try { - // 1. 设置代理环境变量 - logger.info('1. 设置代理配置...'); - proxyConfig.setEnvironmentVariables(); - - // 2. 初始化后端 - logger.info('\n2. 初始化 Pixiv 后端...'); - const backend = new PixivBackend(); - await backend.init(); - - // 3. 检查登录状态 - logger.info('\n3. 检查当前登录状态...'); - const loginStatus = backend.getLoginStatus(); - logger.info('登录状态:', loginStatus); - - if (loginStatus.isLoggedIn) { - logger.info('✅ 已登录,用户:', loginStatus.username); - return; - } - - // 4. 获取登录URL - logger.info('\n4. 获取登录URL...'); - const loginData = backend.getLoginUrl(); - logger.info('请访问以下URL进行登录:'); - logger.info(loginData.login_url); - logger.info('\n登录完成后,请复制回调URL中的code参数'); - - // 5. 等待用户输入授权码 - const code = await askQuestion('\n请输入授权码 (code参数): '); - - if (!code || code.trim() === '') { - logger.info('❌ 未输入授权码,测试终止'); - return; - } - - // 6. 处理登录回调 - logger.info('\n5. 处理登录回调...'); - const loginResult = await backend.handleLoginCallback(code.trim()); - - if (loginResult.success) { - logger.info('✅ 登录成功!'); - logger.info('用户信息:', loginResult.user); - - // 7. 再次检查登录状态 - logger.info('\n6. 验证登录状态...'); - const finalStatus = backend.getLoginStatus(); - logger.info('最终登录状态:', finalStatus); - - // 8. 测试获取用户信息 - logger.info('\n7. 测试获取用户信息...'); - const auth = backend.getAuth(); - const userInfo = await auth.getUserInfo(); - - if (userInfo.success) { - logger.info('✅ 获取用户信息成功:', userInfo.user); - } else { - logger.info('❌ 获取用户信息失败:', userInfo.error); - } - - } else { - logger.info('❌ 登录失败:', loginResult.error); - } - - } catch (error) { - logger.error('❌ 测试过程中发生错误:', error.message); - logger.error('错误详情:', error); - } finally { - // 清理资源 - rl.close(); - logger.info('\n=== 测试完成 ==='); - } -} - -// 测试重新登录功能 -async function testRelogin() { - logger.info('=== 测试重新登录功能 ===\n'); - - try { - // 设置代理 - proxyConfig.setEnvironmentVariables(); - - // 初始化后端 - const backend = new PixivBackend(); - await backend.init(); - - // 检查是否有保存的登录信息 - const loginStatus = backend.getLoginStatus(); - - if (loginStatus.isLoggedIn) { - logger.info('✅ 检测到已保存的登录信息'); - logger.info('用户:', loginStatus.username); - logger.info('用户ID:', loginStatus.user_id); - } else { - logger.info('❌ 没有保存的登录信息,无法测试重新登录'); - } - - } catch (error) { - logger.error('❌ 重新登录测试失败:', error.message); - } -} - -// 测试登出功能 -async function testLogout() { - logger.info('=== 测试登出功能 ===\n'); - - try { - // 设置代理 - proxyConfig.setEnvironmentVariables(); - - // 初始化后端 - const backend = new PixivBackend(); - await backend.init(); - - // 执行登出 - const logoutResult = backend.logout(); - - if (logoutResult.success) { - logger.info('✅ 登出成功'); - - // 验证登出状态 - const loginStatus = backend.getLoginStatus(); - logger.info('登出后状态:', loginStatus); - } else { - logger.info('❌ 登出失败'); - } - - } catch (error) { - logger.error('❌ 登出测试失败:', error.message); - } -} - -// 主函数 -async function main() { - logger.info('请选择测试功能:'); - logger.info('1. 测试完整登录流程'); - logger.info('2. 测试重新登录'); - logger.info('3. 测试登出'); - logger.info('4. 运行所有测试'); - - const choice = await askQuestion('\n请输入选择 (1-4): '); - - switch (choice.trim()) { - case '1': - await testLogin(); - break; - case '2': - await testRelogin(); - break; - case '3': - await testLogout(); - break; - case '4': - logger.info('\n=== 运行所有测试 ===\n'); - await testLogin(); - logger.info('\n' + '='.repeat(50) + '\n'); - await testRelogin(); - logger.info('\n' + '='.repeat(50) + '\n'); - await testLogout(); - break; - default: - logger.info('❌ 无效选择'); - rl.close(); - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - main().catch(logger.error); -} - -module.exports = { - testLogin, - testRelogin, - testLogout -}; \ No newline at end of file diff --git a/backend/utils/logger.js b/backend/utils/logger.js index f5bd9c2..cceeb07 100644 --- a/backend/utils/logger.js +++ b/backend/utils/logger.js @@ -41,7 +41,7 @@ const ModuleColors = { 'Server': '\x1b[32m', // 绿色 'API': '\x1b[32m', // 绿色 'Start': '\x1b[34m', // 蓝色 - 'PixivBackend': '\x1b[35m', // 紫色 + 'PixivCore': '\x1b[35m', // 紫色 'PixivAuth': '\x1b[36m', // 青色 'TaskManager': '\x1b[33m', // 黄色 'ImageCache': '\x1b[92m', // 亮绿色 @@ -54,7 +54,9 @@ const ModuleColors = { 'ErrorHandler': '\x1b[91m', // 亮红色 'FileManager': '\x1b[36m', // 青色 'ProgressManager': '\x1b[35m', // 紫色 + 'DownloadRegistry': '\x1b[94m', // 亮蓝色 'WatchlistManager': '\x1b[94m', // 亮蓝色 + 'CacheConfigManager': '\x1b[94m', // 亮蓝色 'UpdateRoute': '\x1b[93m', // 亮黄色 'ArtistService': '\x1b[95m', // 亮紫色 'DownloadService': '\x1b[96m', // 亮青色 @@ -152,7 +154,24 @@ class Logger { let formattedMessage = `[${timeStr}] [${levelName}] [${this.module}] ${message}`; if (data !== null && data !== undefined) { - if (typeof data === 'object') { + if (data instanceof Error) { + // 特殊处理 Error 对象 + formattedMessage += `\n Error: ${data.message}`; + if (data.stack) { + formattedMessage += `\n Stack: ${data.stack}`; + } + // 如果有其他可枚举属性,也包含进来 + const errorProps = Object.getOwnPropertyNames(data).filter(prop => + prop !== 'message' && prop !== 'stack' && prop !== 'name' + ); + if (errorProps.length > 0) { + const additionalProps = {}; + errorProps.forEach(prop => { + additionalProps[prop] = data[prop]; + }); + formattedMessage += `\n Additional: ${JSON.stringify(additionalProps, null, 2)}`; + } + } else if (typeof data === 'object') { formattedMessage += ` ${JSON.stringify(data, null, 2)}`; } else { formattedMessage += ` ${data}`; diff --git a/package.json b/package.json index 5562449..0117c2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixiv-manager", - "version": "1.0.4", + "version": "1.0.5", "description": "Pixiv 下载浏览管理器", "main": "backend/start.js", "bin": "backend/start.js", @@ -15,9 +15,7 @@ ] }, "scripts": { - "start": "node backend/start.js", "dev": "node backend/start.js", - "test": "node backend/test-login.js", "build": "pkg . && node scripts/add-icon.js", "bp": "npm run build && node scripts/create-portable.js" }, diff --git a/start.bat b/start.bat index f6d9843..c58f6cc 100644 --- a/start.bat +++ b/start.bat @@ -12,6 +12,16 @@ set PROXY_PORT= :: ======================================== set SERVER_PORT=3000 +:: ======================================== +:: 日志级别配置 - 可选值: ERROR, WARN, INFO, DEBUG, TRACE +:: ERROR: 只显示错误信息 +:: WARN: 显示警告及以上级别信息 +:: INFO: 显示一般信息及以上级别信息 (默认) +:: DEBUG: 显示调试信息及以上级别信息 +:: TRACE: 显示所有级别信息 (最详细) +:: ======================================== +set LOG_LEVEL= + echo. echo Pixiv Manager 启动中... echo. @@ -20,7 +30,8 @@ cd /d "%~dp0" echo 当前代理端口: %PROXY_PORT% echo 当前服务器端口: %SERVER_PORT% -echo 如需修改端口,请用记事本打开此文件,修改对应的端口号 +echo 日志级别: %LOG_LEVEL% +echo 如需修改端口或日志级别,请用记事本打开此文件,修改对应的配置 echo. echo 启动后端服务器... @@ -29,13 +40,13 @@ echo. echo 提示: 按 Ctrl+C 停止服务器 echo. -:: 启动服务器并传递代理端口和服务器端口 +:: 启动服务器并传递代理端口、服务器端口和日志级别 if "%PROXY_PORT%"=="" ( - node backend/start.js --server-port=%SERVER_PORT% + node backend/start.js --server-port=%SERVER_PORT% --log-level=%LOG_LEVEL% ) else ( - node backend/start.js --proxy-port=%PROXY_PORT% --server-port=%SERVER_PORT% + node backend/start.js --proxy-port=%PROXY_PORT% --server-port=%SERVER_PORT% --log-level=%LOG_LEVEL% ) echo. echo 服务器已停止 -pause \ No newline at end of file +pause \ No newline at end of file diff --git a/ui/src/App.vue b/ui/src/App.vue index f207b06..8c262d0 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -8,6 +8,7 @@ import { useUpdateStore } from '@/stores/update' import SettingsWidget from '@/components/common/SettingsWidget.vue' import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue' import WatchlistWidget from '@/components/common/WatchlistWidget.vue' +import RegistryWidget from '@/components/common/RegistryWidget.vue' import UpdateChecker from '@/components/common/UpdateChecker.vue' const route = useRoute() @@ -96,6 +97,9 @@ onMounted(async () => { + + + diff --git a/ui/src/assets/icons/actions.ts b/ui/src/assets/icons/actions.ts index c770d1e..ea67ce4 100644 --- a/ui/src/assets/icons/actions.ts +++ b/ui/src/assets/icons/actions.ts @@ -20,5 +20,5 @@ export const actionIcons = { 'empty2': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z', 'refresh': 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z', 'cleanup': 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z', - + 'down':"M18.414 10.656a2 2 0 0 0-2.828 0L14 12.242V5a2 2 0 0 0-4 0v7.242l-1.586-1.586a2 2 0 1 0-2.828 2.828L12 19.898l6.414-6.414a2 2 0 0 0 0-2.828" } \ No newline at end of file diff --git a/ui/src/components/common/RegistryWidget.vue b/ui/src/components/common/RegistryWidget.vue new file mode 100644 index 0000000..77affde --- /dev/null +++ b/ui/src/components/common/RegistryWidget.vue @@ -0,0 +1,557 @@ + + + + + \ No newline at end of file diff --git a/ui/src/services/download.ts b/ui/src/services/download.ts index 54f9454..64d22e4 100644 --- a/ui/src/services/download.ts +++ b/ui/src/services/download.ts @@ -213,6 +213,40 @@ class DownloadService { eventSource.close(); }; } + /** + * 获取下载注册表统计信息 + */ + async getRegistryStats() { + return apiService.get('/api/download/registry/stats'); + } + + /** + * 导出下载注册表 + */ + async exportRegistry() { + return apiService.get('/api/download/registry/export'); + } + + /** + * 导入下载注册表 + */ + async importRegistry(registryData: any) { + return apiService.post('/api/download/registry/import', { registryData }); + } + + /** + * 重建下载注册表 + */ + async rebuildRegistry() { + return apiService.post('/api/download/registry/rebuild'); + } + + /** + * 清理下载注册表 + */ + async cleanupRegistry() { + return apiService.post('/api/download/registry/cleanup'); + } } -export default new DownloadService(); \ No newline at end of file +export default new DownloadService(); \ No newline at end of file diff --git a/ui/src/stores/registry.ts b/ui/src/stores/registry.ts new file mode 100644 index 0000000..664e872 --- /dev/null +++ b/ui/src/stores/registry.ts @@ -0,0 +1,227 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import downloadService from '@/services/download'; +import { getApiBaseUrl } from '@/services/api'; + +export interface RegistryStats { + totalArtists: number; + totalArtworks: number; + lastUpdated: string; +} + +export interface RegistryConfig { + useRegistryCheck: boolean; + fallbackToScan: boolean; +} + +export const useRegistryStore = defineStore('registry', () => { + // 状态 + const stats = ref(null); + const config = ref({ + useRegistryCheck: true, + fallbackToScan: false + }); + const loading = ref(false); + const error = ref(null); + + // 获取注册表统计信息 + const fetchStats = async () => { + try { + loading.value = true; + error.value = null; + + const response = await downloadService.getRegistryStats(); + if (response.success) { + // 映射API响应数据到组件期望的格式 + stats.value = { + totalArtists: response.data.artistCount || 0, + totalArtworks: response.data.artworkCount || 0, + lastUpdated: response.data.updated_at || response.data.created_at || '' + }; + } else { + throw new Error(response.error || '获取统计信息失败'); + } + } catch (err: any) { + error.value = err.message || '获取统计信息失败'; + console.error('获取注册表统计信息失败:', err); + } finally { + loading.value = false; + } + }; + + // 导出注册表 + const exportRegistry = async () => { + try { + loading.value = true; + error.value = null; + + const response = await downloadService.exportRegistry(); + + // 创建下载链接 + const blob = new Blob([JSON.stringify(response, null, 2)], { + type: 'application/json' + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `download-registry-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + return { success: true }; + } catch (err: any) { + error.value = err.message || '导出注册表失败'; + console.error('导出注册表失败:', err); + return { success: false, error: error.value }; + } finally { + loading.value = false; + } + }; + + // 导入注册表 + const importRegistry = async (file: File) => { + try { + loading.value = true; + error.value = null; + + const text = await file.text(); + const registryData = JSON.parse(text); + + const response = await downloadService.importRegistry(registryData); + if (response.success) { + // 刷新统计信息 + await fetchStats(); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '导入注册表失败'); + } + } catch (err: any) { + error.value = err.message || '导入注册表失败'; + console.error('导入注册表失败:', err); + return { success: false, error: error.value }; + } finally { + loading.value = false; + } + }; + + // 重建注册表 + const rebuildRegistry = async () => { + try { + loading.value = true; + error.value = null; + + const response = await downloadService.rebuildRegistry(); + if (response.success) { + // 刷新统计信息 + await fetchStats(); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '重建注册表失败'); + } + } catch (err: any) { + error.value = err.message || '重建注册表失败'; + console.error('重建注册表失败:', err); + return { success: false, error: error.value }; + } finally { + loading.value = false; + } + }; + + // 清理注册表 + const cleanupRegistry = async () => { + try { + loading.value = true; + error.value = null; + + const response = await downloadService.cleanupRegistry(); + if (response.success) { + // 刷新统计信息 + await fetchStats(); + return { success: true, data: response.data }; + } else { + throw new Error(response.error || '清理注册表失败'); + } + } catch (err: any) { + error.value = err.message || '清理注册表失败'; + console.error('清理注册表失败:', err); + return { success: false, error: error.value }; + } finally { + loading.value = false; + } + }; + + // 获取配置 + const fetchConfig = async () => { + try { + const response = await fetch(`${getApiBaseUrl()}/api/download/registry/config`); + const result = await response.json(); + + if (result.success && result.data) { + config.value = result.data; + return result.data; + } else { + throw new Error(result.error || '获取配置失败'); + } + } catch (err: any) { + error.value = err.message || '获取配置失败'; + console.error('获取配置失败:', err); + throw err; + } + }; + + // 更新配置 + const updateConfig = async (newConfig: Partial) => { + try { + loading.value = true; + error.value = null; + + const response = await fetch(`${getApiBaseUrl()}/api/download/registry/config`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newConfig), + }); + + const result = await response.json(); + + if (result.success && result.data) { + config.value = { ...config.value, ...result.data }; + return { success: true, data: result.data }; + } else { + throw new Error(result.error || '更新配置失败'); + } + } catch (err: any) { + error.value = err.message || '更新配置失败'; + console.error('更新配置失败:', err); + return { success: false, error: error.value }; + } finally { + loading.value = false; + } + }; + + // 清除错误 + const clearError = () => { + error.value = null; + }; + + return { + // 状态 + stats, + config, + loading, + error, + + // 方法 + fetchStats, + exportRegistry, + importRegistry, + rebuildRegistry, + cleanupRegistry, + fetchConfig, + updateConfig, + clearError + }; +}); \ No newline at end of file diff --git a/ui/src/stores/repository.ts b/ui/src/stores/repository.ts index a60a20b..41cb961 100644 --- a/ui/src/stores/repository.ts +++ b/ui/src/stores/repository.ts @@ -1,5 +1,6 @@ import { defineStore } from 'pinia' import { ref } from 'vue' +import { getApiBaseUrl } from '@/services/api' export interface RepositoryConfig { downloadDir: string @@ -183,7 +184,16 @@ export const useRepositoryStore = defineStore('repository', () => { // 检查作品是否已下载 const checkArtworkDownloaded = async (artworkId: number) => { - return await apiCall(`/check-downloaded/${artworkId}`) + // 使用新的下载检测API,支持注册表检测和回退机制 + const response = await fetch(`${getApiBaseUrl()}/api/download/check/${artworkId}`) + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + const result = await response.json() + if (!result.success) { + throw new Error(result.error || 'API调用失败') + } + return result.data } // 检查目录是否存在 @@ -223,4 +233,4 @@ export const useRepositoryStore = defineStore('repository', () => { checkDirectoryExists, migrateFromOldToNew, } -}) \ No newline at end of file +}) \ No newline at end of file diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index 85081b4..545ff61 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -258,7 +258,7 @@ const handleDownload = async () => { // 清理下载状态 downloading.value = true; - // 如果已经下载过,则强制重新下载(跳过现有文件检查) + // 如果已经下载过,则强制重新下载(不跳过现有文件) const skipExisting = !isDownloaded.value; const response = await downloadService.downloadArtwork(artwork.value.id, { skipExisting