From 0c9f0318e9944d3ca0b58c027d3affc43e337ead Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:22:15 +0000 Subject: [PATCH 1/4] Initial plan From 985950abcf58e35504b3c734b012918d0707fd35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:24:44 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BD=AC=E6=8D=A2=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=20size=20=E5=92=8C=20duration=20=E7=9A=84?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: imsyy <42232682+imsyy@users.noreply.github.com> --- electron/main/ipc/ipc-file.ts | 10 +++------- electron/main/utils/format.ts | 4 ++++ src/views/Local/layout.vue | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/electron/main/ipc/ipc-file.ts b/electron/main/ipc/ipc-file.ts index 4a5def87d..8d8ee2d48 100644 --- a/electron/main/ipc/ipc-file.ts +++ b/electron/main/ipc/ipc-file.ts @@ -35,13 +35,9 @@ const handleLocalMusicSync = async ( try { const coverDir = getCoverDir(); // 刷新本地音乐库 - const allTracks = await localMusicService.refreshLibrary( - dirs, - (current, total) => { - event.sender.send("music-sync-progress", { current, total }); - }, - () => {}, - ); + const allTracks = await localMusicService.refreshLibrary(dirs, (current, total) => { + event.sender.send("music-sync-progress", { current, total }); + }); // 处理音乐封面路径 const finalTracks = processMusicList(allTracks, coverDir); // 分块发送 diff --git a/electron/main/utils/format.ts b/electron/main/utils/format.ts index f50fc108d..2f48cd44f 100644 --- a/electron/main/utils/format.ts +++ b/electron/main/utils/format.ts @@ -35,6 +35,10 @@ export const processMusicList = (tracks: MusicTrack[], coverDir: string) => { ...track, name: track.title, cover, + // 保持原始字节数,供前端使用 formatFileSize 处理 + size: track.size, + // 转换为毫秒 + duration: track.duration * 1000, // 码率映射到 quality 字段 quality: track.bitrate ?? 0, }; diff --git a/src/views/Local/layout.vue b/src/views/Local/layout.vue index df4aa0332..7bfe59021 100644 --- a/src/views/Local/layout.vue +++ b/src/views/Local/layout.vue @@ -305,9 +305,10 @@ const getMusicFolder = async (): Promise => { return paths.filter((p) => p && p.trim() !== ""); }; -// 全部音乐大小 +// 全部音乐大小(基于筛选后的数据) const allMusicSize = computed(() => { const totalBytes = listData.value.reduce((total, song) => (total += song?.size || 0), 0); + // 从字节转换为 GB return Number((totalBytes / (1024 * 1024 * 1024)).toFixed(2)); }); From f669747a2bb5864f986eff8a6a0f3a69645097ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:27:26 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20refactor:=20=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8=E5=92=8C=E5=8F=AF=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: imsyy <42232682+imsyy@users.noreply.github.com> --- electron/main/database/LocalMusicDB.ts | 19 ++++++++---- electron/main/services/LocalMusicService.ts | 26 ++++++++++++++++- electron/main/utils/format.ts | 32 +++++++++++++++++---- src/utils/color.ts | 8 ++++-- 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/electron/main/database/LocalMusicDB.ts b/electron/main/database/LocalMusicDB.ts index b55b7672b..4e9224814 100644 --- a/electron/main/database/LocalMusicDB.ts +++ b/electron/main/database/LocalMusicDB.ts @@ -75,6 +75,16 @@ export class LocalMusicDB { this.dbPath = dbPath; } + /** + * 转义 SQL LIKE 通配符 + * @param str 需要转义的字符串 + * @returns 转义后的字符串 + */ + private escapeLike(str: string): string { + // 使用 ^ 作为转义字符,同时转义 ^ 本身 + return str.replace(/\^/g, "^^").replace(/%/g, "^%").replace(/_/g, "^_"); + } + /** 初始化数据库 */ public init() { if (this.db) return; @@ -266,14 +276,11 @@ export class LocalMusicDB { // 确保路径以分隔符结尾,避免匹配到同名前缀的其他目录 const pathWithSep = dirPath.endsWith("/") || dirPath.endsWith("\\") ? dirPath : dirPath + "/"; - // 先统一路径分隔符 + // 统一路径分隔符并转义 LIKE 通配符 const unixBase = pathWithSep.replace(/\\/g, "/"); const winBase = pathWithSep.replace(/\//g, "\\"); - // 转义 LIKE 通配符(使用 ^ 作为转义字符,同时转义 ^ 本身) - const escapeLike = (s: string) => - s.replace(/\^/g, "^^").replace(/%/g, "^%").replace(/_/g, "^_"); - const unixPath = escapeLike(unixBase) + "%"; - const winPath = escapeLike(winBase) + "%"; + const unixPath = this.escapeLike(unixBase) + "%"; + const winPath = this.escapeLike(winBase) + "%"; // 使用 OR 查询并指定 ESCAPE 字符 return this.db .prepare("SELECT * FROM tracks WHERE path LIKE ? ESCAPE '^' OR path LIKE ? ESCAPE '^'") diff --git a/electron/main/services/LocalMusicService.ts b/electron/main/services/LocalMusicService.ts index 73b4226ee..5ce0af87d 100644 --- a/electron/main/services/LocalMusicService.ts +++ b/electron/main/services/LocalMusicService.ts @@ -9,6 +9,30 @@ import { loadNativeModule } from "../utils/native-loader"; type toolModule = typeof import("@native/tools"); const tools: toolModule = loadNativeModule("tools.node", "tools"); +/** 扫描进度事件 */ +interface ScanProgressEvent { + event: "progress"; + progress: { + current: number; + total: number; + }; +} + +/** 扫描批量数据事件 */ +interface ScanBatchEvent { + event: "batch"; + tracks: MusicTrack[]; +} + +/** 扫描结束事件 */ +interface ScanEndEvent { + event: "end"; + deletedPaths?: string[]; +} + +/** 扫描事件联合类型 */ +type ScanEvent = ScanProgressEvent | ScanBatchEvent | ScanEndEvent; + /** 本地音乐服务 */ export class LocalMusicService { /** 数据库实例 */ @@ -90,7 +114,7 @@ export class LocalMusicService { console.time("RustScanStream"); await new Promise((resolve, reject) => { tools - .scanMusicLibrary(dbPath, dirPaths, coverDir, (err, event) => { + .scanMusicLibrary(dbPath, dirPaths, coverDir, (err, event: ScanEvent | null) => { if (err) { processLog.error("[LocalMusicService] 原生模块扫描时出错:", err); return; diff --git a/electron/main/utils/format.ts b/electron/main/utils/format.ts index 2f48cd44f..5fa02ab3b 100644 --- a/electron/main/utils/format.ts +++ b/electron/main/utils/format.ts @@ -1,15 +1,18 @@ import { type MusicTrack } from "../database/LocalMusicDB"; import { join } from "path"; +/** 艺术家类型定义 */ +type Artist = string | { name?: string }; + /** * 获取艺术家名称 - * @param artists 艺术家数组 + * @param artists 艺术家数组或字符串 * @returns 艺术家名称数组 */ -export const getArtistNames = (artists: any): string[] => { +export const getArtistNames = (artists: Artist | Artist[]): string[] => { if (Array.isArray(artists)) { return artists - .map((ar: any) => (typeof ar === "string" ? ar : ar?.name || "")) + .map((ar) => (typeof ar === "string" ? ar : ar?.name || "")) .filter((name) => name && name.trim().length > 0); } if (typeof artists === "string" && artists.trim().length > 0) { @@ -18,13 +21,30 @@ export const getArtistNames = (artists: any): string[] => { return []; }; +/** 处理后的音乐项接口 */ +interface ProcessedMusicTrack extends Omit { + /** 歌曲名称(映射自 title) */ + name: string; + /** 封面路径(file:// 协议) */ + cover?: string; + /** 音乐质量(映射自 bitrate) */ + quality: number; + /** 文件大小(字节) */ + size: number; + /** 播放时长(毫秒) */ + duration: number; +} + /** - * 处理音乐列表 - * @param tracks 音乐列表 + * 处理音乐列表,转换为前端所需格式 + * @param tracks 原始音乐列表 * @param coverDir 封面目录 * @returns 处理后的音乐列表 */ -export const processMusicList = (tracks: MusicTrack[], coverDir: string) => { +export const processMusicList = ( + tracks: MusicTrack[], + coverDir: string, +): ProcessedMusicTrack[] => { return tracks.map((track) => { let cover: string | undefined; if (track.cover) { diff --git a/src/utils/color.ts b/src/utils/color.ts index 67bfb8668..4cd62fa25 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -192,17 +192,19 @@ export const getCoverColor = async (coverUrl: string) => { /** * 发送任务栏封面颜色 - * 从 statusStore.songCoverTheme 读取封面主色 + * 从 statusStore.songCoverTheme 读取封面主色并发送到任务栏 + * @returns void */ -export const sendTaskbarCoverColor = () => { +export const sendTaskbarCoverColor = (): void => { const settingStore = useSettingStore(); + // 如果未启用主题颜色跟随,则清除任务栏颜色 if (!settingStore.taskbarLyricUseThemeColor) { sendTaskbarThemeColor(null); return; } const statusStore = useStatusStore(); const coverTheme = statusStore.songCoverTheme; - // 检查亮暗模式数据是否存在 + // 确保亮暗模式的主色都存在 if (!coverTheme?.dark?.primary || !coverTheme?.light?.primary) return; const darkPrimary = coverTheme.dark.primary; const lightPrimary = coverTheme.light.primary; From 797a9cc25927be0378ab0770bee5e4aa7a8703d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:28:30 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=E6=8F=90?= =?UTF-8?q?=E5=8F=96=E5=85=B1=E4=BA=AB=E8=B7=AF=E5=BE=84=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=8C=E6=B6=88=E9=99=A4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: imsyy <42232682+imsyy@users.noreply.github.com> --- electron/main/ipc/ipc-file.ts | 9 +------ electron/main/services/LocalMusicService.ts | 12 ++------- electron/main/utils/paths.ts | 29 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 electron/main/utils/paths.ts diff --git a/electron/main/ipc/ipc-file.ts b/electron/main/ipc/ipc-file.ts index 8d8ee2d48..e6d07276a 100644 --- a/electron/main/ipc/ipc-file.ts +++ b/electron/main/ipc/ipc-file.ts @@ -5,9 +5,9 @@ import { ipcLog } from "../logger"; import { LocalMusicService } from "../services/LocalMusicService"; import { DownloadService } from "../services/DownloadService"; import { MusicMetadataService } from "../services/MusicMetadataService"; -import { useStore } from "../store"; import { chunkArray } from "../utils/helper"; import { processMusicList } from "../utils/format"; +import { getCoverDir } from "../utils/paths"; /** 本地音乐服务 */ const localMusicService = new LocalMusicService(); @@ -16,13 +16,6 @@ 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 调用事件 diff --git a/electron/main/services/LocalMusicService.ts b/electron/main/services/LocalMusicService.ts index 5ce0af87d..684f48d63 100644 --- a/electron/main/services/LocalMusicService.ts +++ b/electron/main/services/LocalMusicService.ts @@ -1,10 +1,9 @@ import { existsSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import { join } from "node:path"; import { LocalMusicDB, type MusicTrack } from "../database/LocalMusicDB"; import { processLog } from "../logger"; -import { useStore } from "../store"; import { loadNativeModule } from "../utils/native-loader"; +import { getLocalDataPaths } from "../utils/paths"; type toolModule = typeof import("@native/tools"); const tools: toolModule = loadNativeModule("tools.node", "tools"); @@ -46,14 +45,7 @@ export class LocalMusicService { /** 获取动态路径 */ get paths() { - const store = useStore(); - const localCachePath = join(store.get("cachePath"), "local-data"); - return { - dbPath: join(localCachePath, "library.db"), - jsonPath: join(localCachePath, "library.json"), - coverDir: join(localCachePath, "covers"), - cacheDir: localCachePath, - }; + return getLocalDataPaths(); } /** 初始化 */ diff --git a/electron/main/utils/paths.ts b/electron/main/utils/paths.ts new file mode 100644 index 000000000..7407db49d --- /dev/null +++ b/electron/main/utils/paths.ts @@ -0,0 +1,29 @@ +import { join } from "node:path"; +import { useStore } from "../store"; + +/** + * 获取本地数据相关的路径配置 + * @returns 包含各种路径的对象 + */ +export const getLocalDataPaths = () => { + const store = useStore(); + const localCachePath = join(store.get("cachePath"), "local-data"); + return { + /** 数据库文件路径 */ + dbPath: join(localCachePath, "library.db"), + /** 旧版 JSON 文件路径 */ + jsonPath: join(localCachePath, "library.json"), + /** 封面目录路径 */ + coverDir: join(localCachePath, "covers"), + /** 缓存根目录 */ + cacheDir: localCachePath, + }; +}; + +/** + * 获取封面目录路径 + * @returns 封面目录的完整路径 + */ +export const getCoverDir = (): string => { + return getLocalDataPaths().coverDir; +};