import { Inject, Injectable, Logger } from '@nestjs/common'; import * as path from 'path'; import * as fs from 'fs/promises'; import { PathService } from '../../core/path/path.service'; import { OpenListService } from '../../core/openlist/openlist.service'; import { AppConfigService } from '../../config/config.service'; @Injectable() export class MemeService { private readonly logger = new Logger(MemeService.name); private readonly updateMs = 150 * 60 * 1000; // 15min constructor( @Inject(PathService) private readonly pathService: PathService, @Inject(OpenListService) private readonly openListService: OpenListService, @Inject(AppConfigService) private readonly configService: AppConfigService, ) { this.startAutoUpdate(); } private startAutoUpdate() { setInterval(async () => { const memePath = path.join(this.pathService.get('meme')); const remoteMemePath = this.configService.get('OPENLIST_API_MEME_PATH'); if (remoteMemePath) { this.logger.log('定时检查表情仓库更新..'); try { const remoteFiles = await this.openListService.listFiles(remoteMemePath); if (remoteFiles.code === 200 && remoteFiles.data.content) { let remoteFileList = remoteFiles.data.content; const localFiles = await this.getLocalFileList(memePath); //this.logger.debug(localFiles); await this.compareAndDownloadFiles( memePath, localFiles, remoteFileList, remoteMemePath, ); } else { this.logger.error('获取远程表情仓库文件失败..'); } } catch (error) { this.logger.error('定时检查表情仓库更新失败..', error); } } else { this.logger.warn('未配置远程表情包地址..'); } }, this.updateMs); } /** * 获取本地目录的文件列表 * @param dir 本地路径 * @private */ private async getLocalFileList(dir: string): Promise { const files: string[] = []; const dirs: string[] = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { dirs.push(fullPath); } else if (/\.(jpg|jpeg|png|gif|webp)$/i.test(entry.name)) { files.push(fullPath); } } for (const subDir of dirs) { const subFiles = await this.getLocalFileList(subDir); files.push(...subFiles); } } catch (error) { this.logger.error(`读取本地目录失败: ${dir}`, error); } return files; } /** * 比较本地文件和远程文件 下载缺失的文件 * @param localPath 本地路径 * @param localFiles 本地文件列表 * @param remoteFiles 远程文件列表 * @param remoteMemePath 远程基准路径 * @private */ private async compareAndDownloadFiles( localPath: string, localFiles: string[], remoteFiles: any[], remoteMemePath: string, ) { for (const remoteFile of remoteFiles) { let relativePath = path.relative(remoteMemePath, remoteFile.path); relativePath = relativePath.replace(/D:\\alist\\crystelf\\meme/g, ''); const localFilePath = path.join( localPath, relativePath.replace(/\\/g, '/'), ); if (remoteFile.is_dir) { try { const localDirPath = path.dirname(localFilePath); await fs.mkdir(localDirPath, { recursive: true }); this.logger.log(`文件夹已创建: ${localDirPath}`); const subRemoteFiles = await this.openListService.listFiles( remoteFile.path, ); if (subRemoteFiles.code === 200 && subRemoteFiles.data.content) { await this.compareAndDownloadFiles( localPath, [], subRemoteFiles.data.content, remoteMemePath, ); } } catch (error) { this.logger.error(`创建文件夹失败: ${remoteFile.path}`, error); } } else { if (!localFiles.includes(localFilePath)) { this.logger.log(`文件缺失: ${remoteFile.path}, 开始下载..`); try { await this.openListService.downloadFile( remoteFile.path, localFilePath, ); this.logger.log(`文件下载成功: ${remoteFile.path}`); } catch (error) { this.logger.error(`下载文件失败: ${remoteFile.path}`, error); } } } } } /** * 获取表情路径 * @param character 角色 * @param status 状态 */ public async getRandomMemePath( character?: string, status?: string, ): Promise { const baseDir = path.join(this.pathService.get('meme')); try { if (!character) { return this.getRandomFileRecursive(baseDir); } const characterDir = path.join(baseDir, character); if (!status) { return this.getRandomFileRecursive(characterDir); } const statusDir = path.join(characterDir, status); return this.getRandomFileFromDir(statusDir); } catch (e) { this.logger.error(`获取表情包失败: ${e.message}`); return null; } } /** * 从目录中随机获取一张图片 * @param dir 绝对目录 * @private */ private async getRandomFileFromDir(dir: string): Promise { try { const files = await fs.readdir(dir); const images = files.filter((f) => /\.(jpg|jpeg|png|gif|webp)$/i.test(f)); if (images.length === 0) return null; const randomFile = images[Math.floor(Math.random() * images.length)]; return path.join(dir, randomFile); } catch { return null; } } /** * 随机选择一张图片 * @param dir 绝对路径 * @private */ private async getRandomFileRecursive(dir: string): Promise { try { const entries = await fs.readdir(dir, { withFileTypes: true }); const files: string[] = []; const dirs: string[] = []; for (const entry of entries) { if (entry.isDirectory()) { dirs.push(path.join(dir, entry.name)); } else if (/\.(jpg|jpeg|png|gif|webp)$/i.test(entry.name)) { files.push(path.join(dir, entry.name)); } } let allFiles = [...files]; for (const subDir of dirs) { const subFile = await this.getRandomFileRecursive(subDir); if (subFile) allFiles.push(subFile); } if (allFiles.length === 0) return null; return allFiles[Math.floor(Math.random() * allFiles.length)]; } catch { return null; } } }