Skip to content

Commit d34aead

Browse files
authored
Merge pull request #640 from imsyy/dev-local
✨ feat: 新增本地缓存功能
2 parents b5bfe22 + f03ea10 commit d34aead

34 files changed

Lines changed: 2185 additions & 590 deletions

electron/main/ipc/ipc-cache.ts

Lines changed: 43 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
11
import { ipcMain } from "electron";
2-
import { existsSync, mkdirSync } from "fs";
3-
import { readdir, readFile, rm, stat, writeFile } from "fs/promises";
4-
import { join, resolve } from "path";
5-
import { useStore } from "../store";
6-
import type Store from "electron-store";
7-
import type { StoreType } from "../store";
2+
import { CacheService, type CacheResourceType, type CacheListItem } from "../services/CacheService";
3+
import { MusicCacheService } from "../services/MusicCacheService";
84
import { processLog } from "../logger";
95

10-
/**
11-
* 缓存资源类型
12-
* - music: 音乐缓存
13-
* - lyrics: 歌词缓存
14-
* - local-data: 本地音乐数据缓存
15-
* - playlist-data: 歌单数据缓存
16-
*/
17-
type CacheResourceType = "music" | "lyrics" | "local-data" | "playlist-data";
18-
196
/**
207
* 缓存 IPC 通用返回结果
218
* @template T 返回数据类型
@@ -29,87 +16,6 @@ type CacheIpcResult<T = any> = {
2916
message?: string;
3017
};
3118

32-
/**
33-
* 缓存列表项信息
34-
*/
35-
type CacheListItem = {
36-
/** 缓存 key(文件名或相对路径) */
37-
key: string;
38-
/** 文件大小(字节) */
39-
size: number;
40-
/** 最后修改时间(毫秒时间戳) */
41-
mtime: number;
42-
};
43-
44-
/**
45-
* 不同缓存类型对应的子目录映射
46-
*/
47-
const CACHE_SUB_DIR: Record<CacheResourceType, string> = {
48-
music: "music",
49-
lyrics: "lyrics",
50-
"local-data": "local-data",
51-
"playlist-data": "playlist-data",
52-
};
53-
54-
/**
55-
* 确保缓存根目录及各子目录存在
56-
* @param basePath 缓存根路径
57-
*/
58-
const ensureCacheDirs = (basePath: string): void => {
59-
if (!existsSync(basePath)) mkdirSync(basePath, { recursive: true });
60-
Object.values(CACHE_SUB_DIR).forEach((sub) => {
61-
const dir = join(basePath, sub);
62-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
63-
});
64-
};
65-
66-
/**
67-
* 从 Store 中获取缓存根路径
68-
* @param store Electron Store 实例
69-
* @returns 缓存根路径
70-
* @throws 当未配置 cachePath 时抛出异常
71-
*/
72-
const getCacheBasePath = (store: Store<StoreType>): string => {
73-
const base = store.get("cachePath") as string | undefined;
74-
if (!base) {
75-
throw new Error("cachePath 未配置");
76-
}
77-
return base;
78-
};
79-
80-
/**
81-
* 解析并校验缓存文件路径,防止路径穿越
82-
* @param basePath 缓存根路径
83-
* @param type 缓存资源类型
84-
* @param key 缓存 key(文件名或相对路径)
85-
* @returns 目录与最终文件路径
86-
*/
87-
const resolveSafePath = (basePath: string, type: CacheResourceType, key: string) => {
88-
const dir = join(basePath, CACHE_SUB_DIR[type]);
89-
const target = resolve(dir, key);
90-
if (!target.startsWith(resolve(dir))) {
91-
throw new Error("非法的缓存 key");
92-
}
93-
return { dir, target };
94-
};
95-
96-
/**
97-
* 将多种类型的数据转换为 Buffer
98-
* @param data 输入数据(Buffer / Uint8Array / ArrayBuffer / string / Node Buffer JSON)
99-
* @returns 对应的 Buffer
100-
* @throws 不支持的类型时抛出异常
101-
*/
102-
const toBuffer = (data: any): Buffer => {
103-
if (Buffer.isBuffer(data)) return data;
104-
if (data instanceof Uint8Array) return Buffer.from(data);
105-
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
106-
if (typeof data === "string") return Buffer.from(data, "utf-8");
107-
if (data?.type === "Buffer" && Array.isArray(data?.data)) {
108-
return Buffer.from(data.data);
109-
}
110-
throw new Error("不支持的缓存写入数据类型");
111-
};
112-
11319
/**
11420
* 通用错误捕获包装器,为 IPC 返回统一结果结构
11521
* @param action 实际执行的异步逻辑
@@ -129,39 +35,18 @@ const withErrorCatch = async <T>(action: () => Promise<T>): Promise<CacheIpcResu
12935
* 初始化缓存相关 IPC 事件
13036
*/
13137
const initCacheIpc = (): void => {
132-
const store = useStore();
133-
if (!store) return;
38+
const cacheService = CacheService.getInstance();
39+
const musicCacheService = MusicCacheService.getInstance();
13440

135-
try {
136-
const basePath = getCacheBasePath(store);
137-
ensureCacheDirs(basePath);
138-
} catch (error) {
139-
processLog.error("❌ 初始化缓存目录失败:", error);
140-
}
41+
// 初始化缓存服务
42+
cacheService.init();
14143

14244
// 列出指定类型下的缓存文件
14345
ipcMain.handle(
14446
"cache-list",
14547
(_event, type: CacheResourceType): Promise<CacheIpcResult<CacheListItem[]>> => {
14648
return withErrorCatch(async () => {
147-
const basePath = getCacheBasePath(store);
148-
ensureCacheDirs(basePath);
149-
const dir = join(basePath, CACHE_SUB_DIR[type]);
150-
const files = await readdir(dir, { withFileTypes: true });
151-
const items: CacheListItem[] = [];
152-
153-
for (const file of files) {
154-
if (!file.isFile()) continue;
155-
const filePath = join(dir, file.name);
156-
const info = await stat(filePath);
157-
items.push({
158-
key: file.name,
159-
size: info.size,
160-
mtime: info.mtimeMs,
161-
});
162-
}
163-
164-
return items;
49+
return await cacheService.list(type);
16550
});
16651
},
16752
);
@@ -171,10 +56,7 @@ const initCacheIpc = (): void => {
17156
"cache-get",
17257
(_event, type: CacheResourceType, key: string): Promise<CacheIpcResult<Buffer>> => {
17358
return withErrorCatch(async () => {
174-
const basePath = getCacheBasePath(store);
175-
ensureCacheDirs(basePath);
176-
const { target } = resolveSafePath(basePath, type, key);
177-
return await readFile(target);
59+
return await cacheService.get(type, key);
17860
});
17961
},
18062
);
@@ -189,12 +71,7 @@ const initCacheIpc = (): void => {
18971
data: Buffer | Uint8Array | ArrayBuffer | string,
19072
): Promise<CacheIpcResult<null>> => {
19173
return withErrorCatch(async () => {
192-
const basePath = getCacheBasePath(store);
193-
ensureCacheDirs(basePath);
194-
const { dir, target } = resolveSafePath(basePath, type, key);
195-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
196-
const buffer = toBuffer(data);
197-
await writeFile(target, buffer);
74+
await cacheService.put(type, key, data);
19875
return null;
19976
});
20077
},
@@ -205,10 +82,7 @@ const initCacheIpc = (): void => {
20582
"cache-remove",
20683
(_event, type: CacheResourceType, key: string): Promise<CacheIpcResult<null>> => {
20784
return withErrorCatch(async () => {
208-
const basePath = getCacheBasePath(store);
209-
ensureCacheDirs(basePath);
210-
const { target } = resolveSafePath(basePath, type, key);
211-
await rm(target, { force: true });
85+
await cacheService.remove(type, key);
21286
return null;
21387
});
21488
},
@@ -219,14 +93,43 @@ const initCacheIpc = (): void => {
21993
"cache-clear",
22094
(_event, type: CacheResourceType): Promise<CacheIpcResult<null>> => {
22195
return withErrorCatch(async () => {
222-
const basePath = getCacheBasePath(store);
223-
const dir = join(basePath, CACHE_SUB_DIR[type]);
224-
await rm(dir, { recursive: true, force: true });
225-
ensureCacheDirs(basePath);
96+
await cacheService.clear(type);
22697
return null;
22798
});
22899
},
229100
);
101+
102+
// 获取所有缓存类型的总大小
103+
ipcMain.handle("cache-size", (): Promise<CacheIpcResult<number>> => {
104+
return withErrorCatch(async () => {
105+
return await cacheService.getSize();
106+
});
107+
});
108+
109+
// 检查是否存在音乐缓存
110+
ipcMain.handle("music-cache-check", async (_event, id: number | string, quality?: string) => {
111+
try {
112+
return await musicCacheService.hasCache(id, quality);
113+
} catch (error) {
114+
processLog.error("Check music cache failed:", error);
115+
return null;
116+
}
117+
});
118+
119+
// 下载并缓存音乐
120+
ipcMain.handle(
121+
"music-cache-download",
122+
async (_event, id: number | string, url: string, quality?: string) => {
123+
try {
124+
const qualitySuffix = quality || "standard";
125+
const path = await musicCacheService.cacheMusic(id, url, qualitySuffix);
126+
return { success: true, path };
127+
} catch (error: any) {
128+
processLog.error("Download music cache failed:", error);
129+
return { success: false, message: error.message };
130+
}
131+
},
132+
);
230133
};
231134

232135
export default initCacheIpc;

0 commit comments

Comments
 (0)