mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 06:30:49 +08:00
fix(本地音乐): 扫描阶段直接使用mtime做增量判断
This commit is contained in:
@@ -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 {
|
||||
|
||||
4
src/preload/index.d.ts
vendored
4
src/preload/index.d.ts
vendored
@@ -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[]
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
4
src/renderer/types/electron.d.ts
vendored
4
src/renderer/types/electron.d.ts
vendored
@@ -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[]>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user