Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions electron/main/database/LocalMusicDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 '^'")
Expand Down
19 changes: 4 additions & 15 deletions electron/main/ipc/ipc-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 调用事件
Expand All @@ -35,13 +28,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);
// 分块发送
Expand Down
38 changes: 27 additions & 11 deletions electron/main/services/LocalMusicService.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
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");

/** 扫描进度事件 */
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 {
/** 数据库实例 */
Expand All @@ -22,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();
}

/** 初始化 */
Expand Down Expand Up @@ -90,7 +106,7 @@ export class LocalMusicService {
console.time("RustScanStream");
await new Promise<void>((resolve, reject) => {
tools
.scanMusicLibrary(dbPath, dirPaths, coverDir, (err, event) => {
.scanMusicLibrary(dbPath, dirPaths, coverDir, (err, event: ScanEvent | null) => {
if (err) {
processLog.error("[LocalMusicService] 原生模块扫描时出错:", err);
return;
Expand Down
36 changes: 30 additions & 6 deletions electron/main/utils/format.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -18,13 +21,30 @@ export const getArtistNames = (artists: any): string[] => {
return [];
};

/** 处理后的音乐项接口 */
interface ProcessedMusicTrack extends Omit<MusicTrack, "title"> {
/** 歌曲名称(映射自 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) {
Expand All @@ -35,6 +55,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,
};
Expand Down
29 changes: 29 additions & 0 deletions electron/main/utils/paths.ts
Original file line number Diff line number Diff line change
@@ -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;
};
8 changes: 5 additions & 3 deletions src/utils/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/views/Local/layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,10 @@ const getMusicFolder = async (): Promise<string[]> => {
return paths.filter((p) => p && p.trim() !== "");
};

// 全部音乐大小
// 全部音乐大小(基于筛选后的数据)
const allMusicSize = computed<number>(() => {
const totalBytes = listData.value.reduce((total, song) => (total += song?.size || 0), 0);
// 从字节转换为 GB
return Number((totalBytes / (1024 * 1024 * 1024)).toFixed(2));
});

Expand Down