From c714860c96b855e15cc2949ce89d319ef9adbfc7 Mon Sep 17 00:00:00 2001 From: alger Date: Wed, 4 Mar 2026 20:59:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=9C=AC=E5=9C=B0=E9=9F=B3=E4=B9=90):=20?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E9=98=B6=E6=AE=B5=E7=9B=B4=E6=8E=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8mtime=E5=81=9A=E5=A2=9E=E9=87=8F=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/modules/localMusicScanner.ts | 68 ++++++++++++++++++++++++ src/preload/index.d.ts | 4 ++ src/preload/index.ts | 3 ++ src/renderer/store/modules/localMusic.ts | 60 +++++++-------------- src/renderer/types/electron.d.ts | 4 ++ 5 files changed, 98 insertions(+), 41 deletions(-) diff --git a/src/main/modules/localMusicScanner.ts b/src/main/modules/localMusicScanner.ts index a013758..4d3f733 100644 --- a/src/main/modules/localMusicScanner.ts +++ b/src/main/modules/localMusicScanner.ts @@ -34,6 +34,11 @@ type LocalMusicMeta = { modifiedTime: number; }; +type ScannedMusicFile = { + path: string; + modifiedTime: number; +}; + /** * 判断文件扩展名是否为支持的音频格式 * @param ext 文件扩展名(含点号,如 .mp3) @@ -146,6 +151,58 @@ async function scanMusicFiles(folderPath: string): Promise { return results; } +/** + * 递归扫描指定文件夹,返回包含修改时间的音乐文件信息 + * @param folderPath 要扫描的文件夹路径 + * @returns 音乐文件信息列表 + */ +async function scanMusicFilesWithStats(folderPath: string): Promise { + const results: ScannedMusicFile[] = []; + + if (!fs.existsSync(folderPath)) { + throw new Error(`文件夹不存在: ${folderPath}`); + } + + const stat = await fs.promises.stat(folderPath); + if (!stat.isDirectory()) { + throw new Error(`路径不是文件夹: ${folderPath}`); + } + + async function walkDirectory(dirPath: string): Promise { + try { + const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + await walkDirectory(fullPath); + } else if (entry.isFile()) { + const ext = path.extname(entry.name); + if (!isSupportedFormat(ext)) { + continue; + } + + try { + const fileStat = await fs.promises.stat(fullPath); + results.push({ + path: fullPath, + modifiedTime: fileStat.mtimeMs + }); + } catch (error) { + console.error(`读取文件信息失败: ${fullPath}`, error); + } + } + } + } catch (error) { + console.error(`扫描目录失败: ${dirPath}`, error); + } + } + + await walkDirectory(folderPath); + return results; +} + /** * 解析单个音乐文件的元数据 * 解析失败时使用 fallback 默认值(文件名作标题),不抛出异常 @@ -232,6 +289,17 @@ export function initializeLocalMusicScanner(): void { } }); + // 扫描指定文件夹中的音乐文件(包含修改时间) + ipcMain.handle('scan-local-music-with-stats', async (_, folderPath: string) => { + try { + const files = await scanMusicFilesWithStats(folderPath); + return { files, count: files.length }; + } catch (error: any) { + console.error('扫描本地音乐(含文件信息)失败:', error); + return { error: error.message || '扫描失败' }; + } + }); + // 批量解析音乐文件元数据 ipcMain.handle('parse-local-music-metadata', async (_, filePaths: string[]) => { try { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index fdada61..fedeb27 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -30,6 +30,10 @@ interface API { lxMusicHttpCancel: (requestId: string) => Promise; /** 扫描指定文件夹中的本地音乐文件 */ scanLocalMusic: (folderPath: string) => Promise<{ files: string[]; count: number }>; + /** 扫描指定文件夹中的本地音乐文件(包含修改时间) */ + scanLocalMusicWithStats: ( + folderPath: string + ) => Promise<{ files: { path: string; modifiedTime: number }[]; count: number }>; /** 批量解析本地音乐文件元数据 */ parseLocalMusicMetadata: ( filePaths: string[] diff --git a/src/preload/index.ts b/src/preload/index.ts index eba6440..b0325a5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -54,6 +54,7 @@ const api = { 'cache-lyric', 'clear-lyric-cache', 'scan-local-music', + 'scan-local-music-with-stats', 'parse-local-music-metadata' ]; if (validChannels.includes(channel)) { @@ -72,6 +73,8 @@ const api = { // 本地音乐扫描相关 scanLocalMusic: (folderPath: string) => ipcRenderer.invoke('scan-local-music', folderPath), + scanLocalMusicWithStats: (folderPath: string) => + ipcRenderer.invoke('scan-local-music-with-stats', folderPath), parseLocalMusicMetadata: (filePaths: string[]) => ipcRenderer.invoke('parse-local-music-metadata', filePaths) }; diff --git a/src/renderer/store/modules/localMusic.ts b/src/renderer/store/modules/localMusic.ts index 2f0da7e..2e1d276 100644 --- a/src/renderer/store/modules/localMusic.ts +++ b/src/renderer/store/modules/localMusic.ts @@ -8,7 +8,7 @@ import { ref } from 'vue'; import useIndexedDB from '@/hooks/IndexDBHook'; import type { LocalMusicEntry } from '@/types/localMusic'; -import { getChangedFiles, removeStaleEntries } from '@/utils/localMusicUtils'; +import { removeStaleEntries } from '@/utils/localMusicUtils'; const { message } = createDiscreteApi(['message']); @@ -120,12 +120,16 @@ export const useLocalMusicStore = defineStore( // 加载当前缓存数据用于增量对比 const cachedEntries = await localDB.getAllData(LOCAL_MUSIC_STORE); + const cachedMap = new Map(); + for (const entry of cachedEntries) { + cachedMap.set(entry.filePath, entry); + } // 遍历每个文件夹进行扫描 for (const folderPath of folderPaths.value) { try { - // 1. 调用 IPC 扫描文件夹,获取文件路径列表 - const result = await window.api.scanLocalMusic(folderPath); + // 1. 调用 IPC 扫描文件夹,获取文件路径与修改时间 + const result = await window.api.scanLocalMusicWithStats(folderPath); // 检查是否返回错误 if ((result as any).error) { @@ -137,51 +141,25 @@ export const useLocalMusicStore = defineStore( const { files } = result; scanProgress.value += files.length; - // 2. 增量扫描:对比缓存,找出需要重新解析的文件 - const cachedMap = new Map(); - for (const entry of cachedEntries) { - cachedMap.set(entry.filePath, entry); + // 2. 增量扫描:基于修改时间筛选需重新解析的文件 + const parseTargets: string[] = []; + for (const file of files) { + const cached = cachedMap.get(file.path); + if (!cached || cached.modifiedTime !== file.modifiedTime) { + parseTargets.push(file.path); + } } - // 缓存中不存在的新文件,一定需要解析 - const newFiles = files.filter((f) => !cachedMap.has(f)); - // 缓存中已存在的文件,需要检查修改时间是否变更 - const existingFiles = files.filter((f) => cachedMap.has(f)); - - // 3. 解析新文件的元数据并存入 IndexedDB - if (newFiles.length > 0) { - const newMetas = await window.api.parseLocalMusicMetadata(newFiles); - for (const meta of newMetas) { + // 3. 仅解析新增或变更文件,避免对未变更文件重复解析元数据 + if (parseTargets.length > 0) { + const metas = await window.api.parseLocalMusicMetadata(parseTargets); + for (const meta of metas) { const entry: LocalMusicEntry = { ...meta, id: generateId(meta.filePath) }; await localDB.saveData(LOCAL_MUSIC_STORE, entry); - } - } - - // 4. 对已有文件进行增量对比,仅重新解析修改时间变更的文件 - if (existingFiles.length > 0) { - // 解析已有文件的元数据以获取最新修改时间 - const existingMetas = await window.api.parseLocalMusicMetadata(existingFiles); - const existingWithTime = existingMetas.map((meta) => ({ - path: meta.filePath, - modifiedTime: meta.modifiedTime - })); - - // 使用 getChangedFiles 对比修改时间,找出变更文件 - const changedFilePaths = getChangedFiles(existingWithTime, cachedEntries); - const changedSet = new Set(changedFilePaths); - - // 对于修改时间变更的文件,直接使用已解析的元数据更新缓存(避免重复解析) - for (const meta of existingMetas) { - if (changedSet.has(meta.filePath)) { - const entry: LocalMusicEntry = { - ...meta, - id: generateId(meta.filePath) - }; - await localDB.saveData(LOCAL_MUSIC_STORE, entry); - } + cachedMap.set(entry.filePath, entry); } } } catch (error) { diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index f06477a..cf6f3a8 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -20,6 +20,10 @@ export interface IElectronAPI { }; /** 扫描指定文件夹中的本地音乐文件 */ scanLocalMusic: (_folderPath: string) => Promise<{ files: string[]; count: number }>; + /** 扫描指定文件夹中的本地音乐文件(包含修改时间) */ + scanLocalMusicWithStats: ( + _folderPath: string + ) => Promise<{ files: { path: string; modifiedTime: number }[]; count: number }>; /** 批量解析本地音乐文件元数据 */ parseLocalMusicMetadata: (_filePaths: string[]) => Promise; }