-
Notifications
You must be signed in to change notification settings - Fork 1.2k
🦄 refactor: 下载管理接入 Rust 目录扫描 & 任务栏歌词跟随歌曲封面颜色 #864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,3 @@ | ||||||||||||||||||||
| import type { MusicTrack } from "../database/LocalMusicDB"; | ||||||||||||||||||||
| import { app, dialog, ipcMain, shell } from "electron"; | ||||||||||||||||||||
| import { access, mkdir, unlink, writeFile } from "node:fs/promises"; | ||||||||||||||||||||
| import { dirname, isAbsolute, join, relative, resolve } from "node:path"; | ||||||||||||||||||||
|
|
@@ -8,6 +7,7 @@ import { DownloadService } from "../services/DownloadService"; | |||||||||||||||||||
| import { MusicMetadataService } from "../services/MusicMetadataService"; | ||||||||||||||||||||
| import { useStore } from "../store"; | ||||||||||||||||||||
| import { chunkArray } from "../utils/helper"; | ||||||||||||||||||||
| import { processMusicList } from "../utils/format"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** 本地音乐服务 */ | ||||||||||||||||||||
| const localMusicService = new LocalMusicService(); | ||||||||||||||||||||
|
|
@@ -16,6 +16,13 @@ const downloadService = new DownloadService(); | |||||||||||||||||||
| /** 音乐元数据服务 */ | ||||||||||||||||||||
| const musicMetadataService = new MusicMetadataService(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** 获取封面目录路径 */ | ||||||||||||||||||||
| const getCoverDir = (): string => { | ||||||||||||||||||||
| const store = useStore(); | ||||||||||||||||||||
| const localCachePath = join(store.get("cachePath"), "local-data"); | ||||||||||||||||||||
| return join(localCachePath, "covers"); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * 处理本地音乐同步(批量流式传输) | ||||||||||||||||||||
| * @param event IPC 调用事件 | ||||||||||||||||||||
|
|
@@ -26,25 +33,7 @@ const handleLocalMusicSync = async ( | |||||||||||||||||||
| dirs: string[], | ||||||||||||||||||||
| ): Promise<{ success: boolean; message?: string }> => { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| // 获取封面目录路径 | ||||||||||||||||||||
| const store = useStore(); | ||||||||||||||||||||
| const localCachePath = join(store.get("cachePath"), "local-data"); | ||||||||||||||||||||
| const coverDir = join(localCachePath, "covers"); | ||||||||||||||||||||
| /** | ||||||||||||||||||||
| * 处理音乐封面路径 | ||||||||||||||||||||
| * @param tracks 音乐列表 | ||||||||||||||||||||
| * @returns 处理后的音乐列表 | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| const processTracksCover = (tracks: MusicTrack[]) => { | ||||||||||||||||||||
| return tracks.map((track) => { | ||||||||||||||||||||
| let coverPath: string | undefined; | ||||||||||||||||||||
| if (track.cover) { | ||||||||||||||||||||
| const fullPath = join(coverDir, track.cover); | ||||||||||||||||||||
| coverPath = `file://${fullPath.replace(/\\/g, "/")}`; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return { ...track, cover: coverPath }; | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| const coverDir = getCoverDir(); | ||||||||||||||||||||
| // 刷新本地音乐库 | ||||||||||||||||||||
| const allTracks = await localMusicService.refreshLibrary( | ||||||||||||||||||||
| dirs, | ||||||||||||||||||||
|
|
@@ -54,7 +43,7 @@ const handleLocalMusicSync = async ( | |||||||||||||||||||
| () => {}, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| // 处理音乐封面路径 | ||||||||||||||||||||
| const finalTracks = processTracksCover(allTracks); | ||||||||||||||||||||
| const finalTracks = processMusicList(allTracks, coverDir); | ||||||||||||||||||||
| // 分块发送 | ||||||||||||||||||||
| const CHUNK_SIZE = 1000; | ||||||||||||||||||||
| for (const chunk of chunkArray(finalTracks, CHUNK_SIZE)) { | ||||||||||||||||||||
|
|
@@ -120,9 +109,17 @@ const initFileIpc = (): void => { | |||||||||||||||||||
| // 本地音乐同步(批量流式传输) | ||||||||||||||||||||
| ipcMain.handle("local-music-sync", handleLocalMusicSync); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // 遍历音乐文件 | ||||||||||||||||||||
| ipcMain.handle("get-music-files", async (_, dirPath: string) => { | ||||||||||||||||||||
| return musicMetadataService.scanDirectory(dirPath); | ||||||||||||||||||||
| // 获取已下载音乐 | ||||||||||||||||||||
| ipcMain.handle("get-downloaded-songs", async (_event, dirPath: string) => { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const coverDir = getCoverDir(); | ||||||||||||||||||||
| // 扫描指定目录 | ||||||||||||||||||||
| const tracks = await localMusicService.scanDirectory(dirPath); | ||||||||||||||||||||
| return processMusicList(tracks, coverDir); | ||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||
| console.error("Failed to get downloaded songs:", err); | ||||||||||||||||||||
| return []; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // 获取音乐元信息 | ||||||||||||||||||||
|
|
@@ -131,7 +128,7 @@ const initFileIpc = (): void => { | |||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // 修改音乐元信息 | ||||||||||||||||||||
| ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => { | ||||||||||||||||||||
| ipcMain.handle("set-music-metadata", async (_, path: string, metadata) => { | ||||||||||||||||||||
| return musicMetadataService.setMetadata(path, metadata); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
Comment on lines
+131
to
133
|
||||||||||||||||||||
| ipcMain.handle("set-music-metadata", async (_, path: string, metadata) => { | |
| return musicMetadataService.setMetadata(path, metadata); | |
| }); | |
| ipcMain.handle( | |
| "set-music-metadata", | |
| async (_, path: string, metadata: Parameters<MusicMetadataService["setMetadata"]>[1]) => { | |
| return musicMetadataService.setMetadata(path, metadata); | |
| }, | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,6 @@ export class LocalMusicService { | |
| /** 初始化 */ | ||
| private async ensureInitialized(): Promise<void> { | ||
| const { dbPath, jsonPath, coverDir } = this.paths; | ||
|
|
||
| // 如果路径变了,强制重新初始化 | ||
| if (this.lastDbPath && this.lastDbPath !== dbPath) { | ||
| this.initPromise = null; | ||
|
|
@@ -45,87 +44,77 @@ export class LocalMusicService { | |
| } | ||
| } | ||
| this.lastDbPath = dbPath; | ||
|
|
||
| if (this.initPromise) { | ||
| return this.initPromise; | ||
| } | ||
|
|
||
| if (this.initPromise) return this.initPromise; | ||
| this.initPromise = (async () => { | ||
| if (!existsSync(coverDir)) { | ||
| await mkdir(coverDir, { recursive: true }); | ||
| } | ||
|
|
||
| if (!this.db) { | ||
| this.db = new LocalMusicDB(dbPath); | ||
| this.db.init(); | ||
| } | ||
|
|
||
| await this.db.migrateFromJsonIfNeeded(jsonPath); | ||
| })(); | ||
|
|
||
| return this.initPromise; | ||
| } | ||
|
|
||
| /** | ||
| * 刷新所有库文件夹 | ||
| * 内部扫描方法 | ||
| * @param dirPaths 文件夹路径数组 | ||
| * @param ignoreDelete 是否忽略删除操作(默认为 false) | ||
| * @param onProgress 进度回调 | ||
| * @param onTracksBatch 批量track回调(用于流式传输,每批发送多个tracks) | ||
| * @param onTracksBatch 批量track回调 | ||
| */ | ||
| async refreshLibrary( | ||
| private async _scan( | ||
| dirPaths: string[], | ||
| ignoreDelete: boolean = false, | ||
| onProgress?: (current: number, total: number) => void, | ||
| onTracksBatch?: (tracks: MusicTrack[]) => void, | ||
| ) { | ||
| const { dbPath, coverDir } = this.paths; | ||
|
|
||
| // 运行锁:如果正在刷新,抛出特定错误 | ||
| // 运行锁 | ||
| if (this.isRefreshing) { | ||
| throw new Error("SCAN_IN_PROGRESS"); | ||
| } | ||
|
|
||
| // 确保初始化完成 | ||
| await this.ensureInitialized(); | ||
| if (!this.db) throw new Error("DB not initialized"); | ||
|
|
||
| if (!dirPaths || dirPaths.length === 0) { | ||
| // 如果没有目录,清空数据库 | ||
| this.db.clearTracks(); | ||
| return []; | ||
| if (!ignoreDelete) { | ||
| this.db.clearTracks(); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| this.isRefreshing = true; | ||
|
|
||
| try { | ||
| console.time("RustScanStream"); | ||
|
|
||
| await new Promise<void>((resolve, reject) => { | ||
| tools | ||
| .scanMusicLibrary(dbPath, dirPaths, coverDir, (err, event) => { | ||
| if (err) { | ||
| processLog.error("[LocalMusicService] 原生模块扫描时出错:", err); | ||
| return; | ||
| } | ||
|
|
||
| if (!event) return; | ||
|
|
||
| // 处理事件 | ||
| try { | ||
| switch (event.event) { | ||
| // 进度更新 | ||
| case "progress": | ||
| if (event.progress) { | ||
| onProgress?.(event.progress.current, event.progress.total); | ||
| } | ||
| break; | ||
|
|
||
| // 批量数据 | ||
| case "batch": | ||
| if (event.tracks && event.tracks.length > 0) { | ||
| this.db?.addTracks(event.tracks); | ||
| onTracksBatch?.(event.tracks); | ||
| } | ||
| break; | ||
|
|
||
| // 扫描结束 | ||
| case "end": | ||
| if (event.deletedPaths && event.deletedPaths.length > 0) { | ||
| if (!ignoreDelete && event.deletedPaths && event.deletedPaths.length > 0) { | ||
| this.db?.deleteTracks(event.deletedPaths); | ||
| } | ||
| resolve(); | ||
|
Comment on lines
116
to
120
|
||
|
|
@@ -139,10 +128,7 @@ export class LocalMusicService { | |
| reject(err); | ||
| }); | ||
| }); | ||
|
|
||
| console.timeEnd("RustScanStream"); | ||
|
|
||
| return this.db.getAllTracks(); | ||
| } catch (err) { | ||
| processLog.error("[LocalMusicService]: 扫描失败", err); | ||
| throw err; | ||
|
|
@@ -151,6 +137,30 @@ export class LocalMusicService { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * 刷新所有库文件夹 | ||
| * @param dirPaths 文件夹路径数组 | ||
| * @param onProgress 进度回调 | ||
| * @param onTracksBatch 批量track回调 | ||
| */ | ||
| async refreshLibrary( | ||
| dirPaths: string[], | ||
| onProgress?: (current: number, total: number) => void, | ||
| onTracksBatch?: (tracks: MusicTrack[]) => void, | ||
| ) { | ||
| await this._scan(dirPaths, false, onProgress, onTracksBatch); | ||
| return this.db?.getAllTracks() || []; | ||
| } | ||
|
|
||
| /** | ||
| * 扫描指定目录 | ||
| * @param dirPath 目录路径 | ||
| */ | ||
| async scanDirectory(dirPath: string): Promise<MusicTrack[]> { | ||
| await this._scan([dirPath], true); | ||
| return this.db?.getTracksInPath(dirPath) || []; | ||
| } | ||
|
|
||
| /** 获取所有音乐 */ | ||
| async getAllTracks(): Promise<MusicTrack[]> { | ||
| await this.ensureInitialized(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.