diff --git a/backend/services/download-registry.js b/backend/services/download-registry.js index 2ba3b1d..1831d2e 100644 --- a/backend/services/download-registry.js +++ b/backend/services/download-registry.js @@ -1,6 +1,8 @@ const path = require('path'); const fs = require('fs-extra'); const { defaultLogger } = require('../utils/logger'); +const ConfigManager = require('../config/config-manager'); +const artworkUtils = require('../utils/artwork-utils'); // 创建logger实例 const logger = defaultLogger.child('DownloadRegistry'); @@ -12,14 +14,14 @@ const logger = defaultLogger.child('DownloadRegistry'); class DownloadRegistry { constructor(dataPath) { this.dataPath = dataPath; - this.registryPath = path.join(dataPath, 'download_registry.json'); + 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, ...] } } + artists: {}, + lastUpdated: null }; this.loaded = false; + this.configManager = new ConfigManager(); } /** @@ -185,6 +187,44 @@ class DownloadRegistry { return false; } + /** + * 从作品目录名中提取作品ID + * @param {string} artworkDir - 作品目录名 + * @returns {number|null} 作品ID,如果无法提取则返回null + */ + async extractArtworkIdFromDir(artworkDir) { + return await artworkUtils.extractArtworkIdFromDir(artworkDir); + } + + /** + * 检查作品是否已在注册表中注册 + * @param {string} artistName - 作者名称 + * @param {string} artworkDir - 作品目录名 + * @returns {boolean} 是否已注册 + */ + async isArtworkRegistered(artistName, artworkDir) { + if (!this.loaded) { + await this.loadRegistry(); + } + + // 使用动态方法从作品目录名中提取作品ID + const artworkId = await this.extractArtworkIdFromDir(artworkDir); + if (!artworkId) { + logger.warn(`无法从作品目录名中提取作品ID: ${artworkDir}`); + return false; + } + + const normalizedArtistName = this.normalizeArtistName(artistName); + + // 检查艺术家是否存在 + if (!this.registry.artists[normalizedArtistName]) { + return false; + } + + // 检查作品是否在艺术家的作品列表中 + return this.registry.artists[normalizedArtistName].artworks.includes(artworkId); + } + /** * 获取已下载的作品ID列表 * @returns {number[]} 作品ID数组 @@ -387,10 +427,10 @@ class DownloadRegistry { const isRegistered = await this.isArtworkRegistered(artistDir, artworkDir); if (!isRegistered) { - // 获取作品信息并添加到注册表 - const artworkInfo = await fileManager.getArtworkInfo(artistDir, artworkDir); - if (artworkInfo) { - await this.addArtwork(artistDir, artworkDir, artworkInfo); + // 从作品目录名中提取作品ID并添加到注册表 + const artworkId = await this.extractArtworkIdFromDir(artworkDir); + if (artworkId) { + await this.addArtwork(artistDir, artworkId); stats.addedArtworks++; logger.debug(`添加作品到注册表: ${artistDir}/${artworkDir}`); } @@ -451,8 +491,8 @@ class DownloadRegistry { const artworkEntries = await fileManager.listDirectory(artistPath); for (const entry of artworkEntries) { - const match = entry.match(/^(\d+)_(.+)$/); - if (match && parseInt(match[1]) === artworkId) { + const extractedArtworkId = await artworkUtils.extractArtworkIdFromDir(entry); + if (extractedArtworkId && extractedArtworkId === artworkId) { const artworkPath = path.join(artistPath, entry); const infoPath = path.join(artworkPath, 'artwork_info.json'); diff --git a/backend/services/download.js b/backend/services/download.js index 872a748..14b7b55 100644 --- a/backend/services/download.js +++ b/backend/services/download.js @@ -10,6 +10,7 @@ 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'); +const artworkUtils = require('../utils/artwork-utils'); // 创建logger实例 const logger = defaultLogger.child('DownloadService'); @@ -784,11 +785,9 @@ class DownloadService { const artworks = await this.fileManager.listDirectory(artistPath); for (const artwork of artworks) { - // 检查是否是作品目录(包含数字ID) - const artworkMatch = artwork.match(/^(\d+)_(.+)$/); - if (artworkMatch) { - const artworkId = artworkMatch[1]; - + // 使用工具函数检查是否是作品目录并提取ID + const artworkId = await artworkUtils.extractArtworkIdFromDir(artwork); + if (artworkId) { // 检查作品目录是否包含图片文件 const artworkPath = path.join(artistPath, artwork); const artworkStat = await this.fileManager.getFileInfo(artworkPath); @@ -797,7 +796,7 @@ class DownloadService { const files = await this.fileManager.listDirectory(artworkPath); const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)); if (imageFiles.length > 0) { - downloadedIds.add(parseInt(artworkId)); + downloadedIds.add(artworkId); } } } diff --git a/backend/services/repository.js b/backend/services/repository.js index 5eb76e1..2158dcb 100644 --- a/backend/services/repository.js +++ b/backend/services/repository.js @@ -5,6 +5,7 @@ const { exec } = require('child_process') const ConfigManager = require('../config/config-manager') const execAsync = promisify(exec) const { defaultLogger } = require('../utils/logger'); +const artworkUtils = require('../utils/artwork-utils'); // 创建logger实例 const logger = defaultLogger.child('RepositoryService'); @@ -465,9 +466,9 @@ class RepositoryService { for (const artworkEntry of artworkEntries) { if (!artworkEntry.isDirectory()) continue - // 检查是否是目标作品目录(包含数字ID) - const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) - if (artworkMatch && artworkMatch[1] === artworkId.toString()) { + // 使用工具函数检查是否是目标作品目录 + const extractedArtworkId = await artworkUtils.extractArtworkIdFromDir(artworkEntry.name); + if (extractedArtworkId && extractedArtworkId === parseInt(artworkId)) { const artworkPath = path.join(artistPath, artworkEntry.name) // 检查作品信息文件 - 这是最可靠的判断标准 @@ -769,9 +770,10 @@ class RepositoryService { const artistDir = artwork.artistPath try { const artistEntries = await fs.readdir(artistDir, { withFileTypes: true }) - const hasArtworks = artistEntries.some(entry => - entry.isDirectory() && entry.name.match(/^\d+_/) - ) + const hasArtworks = artistEntries.some(async (entry) => { + if (!entry.isDirectory()) return false; + return await artworkUtils.isArtworkDirectory(entry.name); + }); if (!hasArtworks) { await fs.rmdir(artistDir) @@ -816,11 +818,11 @@ class RepositoryService { for (const artworkEntry of artworkEntries) { if (!artworkEntry.isDirectory()) continue - // 检查是否是目标作品目录(包含数字ID) - const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) - if (artworkMatch && artworkMatch[1] === artworkId.toString()) { + // 使用工具函数检查是否是目标作品目录 + const extractedArtworkId = await artworkUtils.extractArtworkIdFromDir(artworkEntry.name); + if (extractedArtworkId && extractedArtworkId === parseInt(artworkId)) { const artworkPath = path.join(artistPath, artworkEntry.name) - const title = artworkMatch[2] + const title = await artworkUtils.extractTitleFromDir(artworkEntry.name) || 'Unknown Title'; // 找到目标作品,返回基本信息(不需要扫描文件详情) return { diff --git a/backend/utils/artwork-utils.js b/backend/utils/artwork-utils.js new file mode 100644 index 0000000..ae39256 --- /dev/null +++ b/backend/utils/artwork-utils.js @@ -0,0 +1,178 @@ +const ConfigManager = require('../config/config-manager'); +const { defaultLogger } = require('./logger'); + +const logger = defaultLogger.child('ArtworkUtils'); + +class ArtworkUtils { + constructor() { + this.configManager = new ConfigManager(); + this._cachedConfig = null; + this._configCacheTime = 0; + this.CACHE_DURATION = 30000; // 30秒缓存 + } + + /** + * 获取配置(带缓存) + * @returns {Promise} 配置对象 + */ + async getConfig() { + const now = Date.now(); + if (this._cachedConfig && (now - this._configCacheTime) < this.CACHE_DURATION) { + return this._cachedConfig; + } + + this._cachedConfig = await this.configManager.readConfig(); + this._configCacheTime = now; + return this._cachedConfig; + } + + /** + * 从作品目录名中提取作品ID + * @param {string} artworkDir - 作品目录名 + * @returns {Promise} 作品ID,如果无法提取则返回null + */ + async extractArtworkIdFromDir(artworkDir) { + try { + // 获取配置中的命名模式 + const config = await this.getConfig(); + const namingPattern = config.namingPattern || "{artist_name}/{artwork_id}_{title}"; + + // 从命名模式中提取作品目录部分(去掉 {artist_name}/ 前缀) + let artworkPattern = namingPattern; + if (artworkPattern.includes('/')) { + artworkPattern = artworkPattern.split('/').pop(); // 取最后一部分 + } + + // 将命名模式转换为正则表达式 + // 替换占位符为对应的正则表达式组 + let regexPattern = artworkPattern + .replace(/\{artwork_id\}/g, '(\\d+)') // artwork_id 匹配数字 + .replace(/\{title\}/g, '(.+)') // title 匹配任意字符 + .replace(/\{artist_name\}/g, '(.+)') // artist_name 匹配任意字符(虽然在这里不应该出现) + .replace(/\{[^}]+\}/g, '(.+)'); // 其他占位符匹配任意字符 + + // 转义特殊字符 + regexPattern = regexPattern.replace(/[.*+?^${}()|[\]\\]/g, (match) => { + // 不转义我们添加的正则表达式组 + if (match === '(' || match === ')' || match === '\\') { + return match; + } + return '\\' + match; + }); + + // 创建正则表达式 + const regex = new RegExp(`^${regexPattern}$`); + const match = artworkDir.match(regex); + + if (match) { + // 找到 artwork_id 在模式中的位置 + const placeholders = artworkPattern.match(/\{[^}]+\}/g) || []; + const artworkIdIndex = placeholders.findIndex(placeholder => placeholder === '{artwork_id}'); + + if (artworkIdIndex !== -1 && match[artworkIdIndex + 1]) { + return parseInt(match[artworkIdIndex + 1]); + } + } + + // 如果动态解析失败,回退到默认格式 {artwork_id}_{title} + const fallbackMatch = artworkDir.match(/^(\d+)_(.+)$/); + if (fallbackMatch) { + logger.debug(`使用回退模式解析作品目录: ${artworkDir}`); + return parseInt(fallbackMatch[1]); + } + + return null; + } catch (error) { + logger.warn(`解析作品目录名失败: ${artworkDir}`, error.message); + + // 发生错误时回退到默认格式 + const fallbackMatch = artworkDir.match(/^(\d+)_(.+)$/); + if (fallbackMatch) { + return parseInt(fallbackMatch[1]); + } + + return null; + } + } + + /** + * 检查目录名是否匹配作品目录格式 + * @param {string} dirName - 目录名 + * @returns {Promise} 是否匹配作品目录格式 + */ + async isArtworkDirectory(dirName) { + const artworkId = await this.extractArtworkIdFromDir(dirName); + return artworkId !== null; + } + + /** + * 从作品目录名中提取标题 + * @param {string} artworkDir - 作品目录名 + * @returns {Promise} 作品标题,如果无法提取则返回null + */ + async extractTitleFromDir(artworkDir) { + try { + // 获取配置中的命名模式 + const config = await this.getConfig(); + const namingPattern = config.namingPattern || "{artist_name}/{artwork_id}_{title}"; + + // 从命名模式中提取作品目录部分(去掉 {artist_name}/ 前缀) + let artworkPattern = namingPattern; + if (artworkPattern.includes('/')) { + artworkPattern = artworkPattern.split('/').pop(); // 取最后一部分 + } + + // 将命名模式转换为正则表达式 + let regexPattern = artworkPattern + .replace(/\{artwork_id\}/g, '(\\d+)') // artwork_id 匹配数字 + .replace(/\{title\}/g, '(.+)') // title 匹配任意字符 + .replace(/\{artist_name\}/g, '(.+)') // artist_name 匹配任意字符 + .replace(/\{[^}]+\}/g, '(.+)'); // 其他占位符匹配任意字符 + + // 转义特殊字符 + regexPattern = regexPattern.replace(/[.*+?^${}()|[\]\\]/g, (match) => { + if (match === '(' || match === ')' || match === '\\') { + return match; + } + return '\\' + match; + }); + + // 创建正则表达式 + const regex = new RegExp(`^${regexPattern}$`); + const match = artworkDir.match(regex); + + if (match) { + // 找到 title 在模式中的位置 + const placeholders = artworkPattern.match(/\{[^}]+\}/g) || []; + const titleIndex = placeholders.findIndex(placeholder => placeholder === '{title}'); + + if (titleIndex !== -1 && match[titleIndex + 1]) { + return match[titleIndex + 1]; + } + } + + // 如果动态解析失败,回退到默认格式 {artwork_id}_{title} + const fallbackMatch = artworkDir.match(/^(\d+)_(.+)$/); + if (fallbackMatch) { + return fallbackMatch[2]; + } + + return null; + } catch (error) { + logger.warn(`解析作品标题失败: ${artworkDir}`, error.message); + + // 发生错误时回退到默认格式 + const fallbackMatch = artworkDir.match(/^(\d+)_(.+)$/); + if (fallbackMatch) { + return fallbackMatch[2]; + } + + return null; + } + } +} + +// 创建单例实例 +const artworkUtils = new ArtworkUtils(); + +module.exports = artworkUtils; \ No newline at end of file