fix(本地音乐): 扫描阶段直接使用mtime做增量判断

This commit is contained in:
alger
2026-03-04 20:59:34 +08:00
parent 92877d86e9
commit c714860c96
5 changed files with 98 additions and 41 deletions

View File

@@ -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<string[]> {
return results;
}
/**
* 递归扫描指定文件夹,返回包含修改时间的音乐文件信息
* @param folderPath 要扫描的文件夹路径
* @returns 音乐文件信息列表
*/
async function scanMusicFilesWithStats(folderPath: string): Promise<ScannedMusicFile[]> {
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<void> {
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 {

View File

@@ -30,6 +30,10 @@ interface API {
lxMusicHttpCancel: (requestId: string) => Promise<void>;
/** 扫描指定文件夹中的本地音乐文件 */
scanLocalMusic: (folderPath: string) => Promise<{ files: string[]; count: number }>;
/** 扫描指定文件夹中的本地音乐文件(包含修改时间) */
scanLocalMusicWithStats: (
folderPath: string
) => Promise<{ files: { path: string; modifiedTime: number }[]; count: number }>;
/** 批量解析本地音乐文件元数据 */
parseLocalMusicMetadata: (
filePaths: string[]

View File

@@ -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)
};

View File

@@ -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<string, LocalMusicEntry>();
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<string, LocalMusicEntry>();
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) {

View File

@@ -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<LocalMusicMeta[]>;
}