mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-25 00:37:24 +08:00
fix(本地音乐): 扫描阶段直接使用mtime做增量判断
This commit is contained in:
@@ -34,6 +34,11 @@ type LocalMusicMeta = {
|
|||||||
modifiedTime: number;
|
modifiedTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ScannedMusicFile = {
|
||||||
|
path: string;
|
||||||
|
modifiedTime: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断文件扩展名是否为支持的音频格式
|
* 判断文件扩展名是否为支持的音频格式
|
||||||
* @param ext 文件扩展名(含点号,如 .mp3)
|
* @param ext 文件扩展名(含点号,如 .mp3)
|
||||||
@@ -146,6 +151,58 @@ async function scanMusicFiles(folderPath: string): Promise<string[]> {
|
|||||||
return results;
|
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 默认值(文件名作标题),不抛出异常
|
* 解析失败时使用 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[]) => {
|
ipcMain.handle('parse-local-music-metadata', async (_, filePaths: string[]) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Vendored
+4
@@ -30,6 +30,10 @@ interface API {
|
|||||||
lxMusicHttpCancel: (requestId: string) => Promise<void>;
|
lxMusicHttpCancel: (requestId: string) => Promise<void>;
|
||||||
/** 扫描指定文件夹中的本地音乐文件 */
|
/** 扫描指定文件夹中的本地音乐文件 */
|
||||||
scanLocalMusic: (folderPath: string) => Promise<{ files: string[]; count: number }>;
|
scanLocalMusic: (folderPath: string) => Promise<{ files: string[]; count: number }>;
|
||||||
|
/** 扫描指定文件夹中的本地音乐文件(包含修改时间) */
|
||||||
|
scanLocalMusicWithStats: (
|
||||||
|
folderPath: string
|
||||||
|
) => Promise<{ files: { path: string; modifiedTime: number }[]; count: number }>;
|
||||||
/** 批量解析本地音乐文件元数据 */
|
/** 批量解析本地音乐文件元数据 */
|
||||||
parseLocalMusicMetadata: (
|
parseLocalMusicMetadata: (
|
||||||
filePaths: string[]
|
filePaths: string[]
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const api = {
|
|||||||
'cache-lyric',
|
'cache-lyric',
|
||||||
'clear-lyric-cache',
|
'clear-lyric-cache',
|
||||||
'scan-local-music',
|
'scan-local-music',
|
||||||
|
'scan-local-music-with-stats',
|
||||||
'parse-local-music-metadata'
|
'parse-local-music-metadata'
|
||||||
];
|
];
|
||||||
if (validChannels.includes(channel)) {
|
if (validChannels.includes(channel)) {
|
||||||
@@ -72,6 +73,8 @@ const api = {
|
|||||||
|
|
||||||
// 本地音乐扫描相关
|
// 本地音乐扫描相关
|
||||||
scanLocalMusic: (folderPath: string) => ipcRenderer.invoke('scan-local-music', folderPath),
|
scanLocalMusic: (folderPath: string) => ipcRenderer.invoke('scan-local-music', folderPath),
|
||||||
|
scanLocalMusicWithStats: (folderPath: string) =>
|
||||||
|
ipcRenderer.invoke('scan-local-music-with-stats', folderPath),
|
||||||
parseLocalMusicMetadata: (filePaths: string[]) =>
|
parseLocalMusicMetadata: (filePaths: string[]) =>
|
||||||
ipcRenderer.invoke('parse-local-music-metadata', filePaths)
|
ipcRenderer.invoke('parse-local-music-metadata', filePaths)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ref } from 'vue';
|
|||||||
|
|
||||||
import useIndexedDB from '@/hooks/IndexDBHook';
|
import useIndexedDB from '@/hooks/IndexDBHook';
|
||||||
import type { LocalMusicEntry } from '@/types/localMusic';
|
import type { LocalMusicEntry } from '@/types/localMusic';
|
||||||
import { getChangedFiles, removeStaleEntries } from '@/utils/localMusicUtils';
|
import { removeStaleEntries } from '@/utils/localMusicUtils';
|
||||||
|
|
||||||
const { message } = createDiscreteApi(['message']);
|
const { message } = createDiscreteApi(['message']);
|
||||||
|
|
||||||
@@ -120,12 +120,16 @@ export const useLocalMusicStore = defineStore(
|
|||||||
|
|
||||||
// 加载当前缓存数据用于增量对比
|
// 加载当前缓存数据用于增量对比
|
||||||
const cachedEntries = await localDB.getAllData(LOCAL_MUSIC_STORE);
|
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) {
|
for (const folderPath of folderPaths.value) {
|
||||||
try {
|
try {
|
||||||
// 1. 调用 IPC 扫描文件夹,获取文件路径列表
|
// 1. 调用 IPC 扫描文件夹,获取文件路径与修改时间
|
||||||
const result = await window.api.scanLocalMusic(folderPath);
|
const result = await window.api.scanLocalMusicWithStats(folderPath);
|
||||||
|
|
||||||
// 检查是否返回错误
|
// 检查是否返回错误
|
||||||
if ((result as any).error) {
|
if ((result as any).error) {
|
||||||
@@ -137,51 +141,25 @@ export const useLocalMusicStore = defineStore(
|
|||||||
const { files } = result;
|
const { files } = result;
|
||||||
scanProgress.value += files.length;
|
scanProgress.value += files.length;
|
||||||
|
|
||||||
// 2. 增量扫描:对比缓存,找出需要重新解析的文件
|
// 2. 增量扫描:基于修改时间筛选需重新解析的文件
|
||||||
const cachedMap = new Map<string, LocalMusicEntry>();
|
const parseTargets: string[] = [];
|
||||||
for (const entry of cachedEntries) {
|
for (const file of files) {
|
||||||
cachedMap.set(entry.filePath, entry);
|
const cached = cachedMap.get(file.path);
|
||||||
|
if (!cached || cached.modifiedTime !== file.modifiedTime) {
|
||||||
|
parseTargets.push(file.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存中不存在的新文件,一定需要解析
|
// 3. 仅解析新增或变更文件,避免对未变更文件重复解析元数据
|
||||||
const newFiles = files.filter((f) => !cachedMap.has(f));
|
if (parseTargets.length > 0) {
|
||||||
// 缓存中已存在的文件,需要检查修改时间是否变更
|
const metas = await window.api.parseLocalMusicMetadata(parseTargets);
|
||||||
const existingFiles = files.filter((f) => cachedMap.has(f));
|
for (const meta of metas) {
|
||||||
|
|
||||||
// 3. 解析新文件的元数据并存入 IndexedDB
|
|
||||||
if (newFiles.length > 0) {
|
|
||||||
const newMetas = await window.api.parseLocalMusicMetadata(newFiles);
|
|
||||||
for (const meta of newMetas) {
|
|
||||||
const entry: LocalMusicEntry = {
|
const entry: LocalMusicEntry = {
|
||||||
...meta,
|
...meta,
|
||||||
id: generateId(meta.filePath)
|
id: generateId(meta.filePath)
|
||||||
};
|
};
|
||||||
await localDB.saveData(LOCAL_MUSIC_STORE, entry);
|
await localDB.saveData(LOCAL_MUSIC_STORE, entry);
|
||||||
}
|
cachedMap.set(entry.filePath, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Vendored
+4
@@ -20,6 +20,10 @@ export interface IElectronAPI {
|
|||||||
};
|
};
|
||||||
/** 扫描指定文件夹中的本地音乐文件 */
|
/** 扫描指定文件夹中的本地音乐文件 */
|
||||||
scanLocalMusic: (_folderPath: string) => Promise<{ files: string[]; count: number }>;
|
scanLocalMusic: (_folderPath: string) => Promise<{ files: string[]; count: number }>;
|
||||||
|
/** 扫描指定文件夹中的本地音乐文件(包含修改时间) */
|
||||||
|
scanLocalMusicWithStats: (
|
||||||
|
_folderPath: string
|
||||||
|
) => Promise<{ files: { path: string; modifiedTime: number }[]; count: number }>;
|
||||||
/** 批量解析本地音乐文件元数据 */
|
/** 批量解析本地音乐文件元数据 */
|
||||||
parseLocalMusicMetadata: (_filePaths: string[]) => Promise<LocalMusicMeta[]>;
|
parseLocalMusicMetadata: (_filePaths: string[]) => Promise<LocalMusicMeta[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user