Skip to content

Commit fe882f7

Browse files
committed
✨ feat: 缓存采用 SQLite 实现
1 parent 672d62d commit fe882f7

7 files changed

Lines changed: 680 additions & 238 deletions

File tree

electron/main/database/CacheDB.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Database from "better-sqlite3";
2+
import { existsSync, mkdirSync } from "fs";
3+
import { dirname } from "path";
4+
5+
/** 缓存条目 */
6+
export interface CacheEntry {
7+
key: string;
8+
type: string;
9+
data: Buffer;
10+
size: number;
11+
mtime: number;
12+
atime: number;
13+
}
14+
15+
/** 缓存数据库 */
16+
export class CacheDB {
17+
private db: Database.Database;
18+
19+
constructor(dbPath: string) {
20+
const dir = dirname(dbPath);
21+
if (!existsSync(dir)) {
22+
mkdirSync(dir, { recursive: true });
23+
}
24+
this.db = new Database(dbPath);
25+
this.init();
26+
}
27+
28+
private init() {
29+
this.db.pragma("journal_mode = WAL");
30+
this.db.exec(`
31+
CREATE TABLE IF NOT EXISTS kv_cache (
32+
key TEXT PRIMARY KEY,
33+
type TEXT NOT NULL,
34+
data BLOB,
35+
size INTEGER,
36+
mtime INTEGER,
37+
atime INTEGER
38+
);
39+
CREATE INDEX IF NOT EXISTS idx_type ON kv_cache(type);
40+
CREATE INDEX IF NOT EXISTS idx_atime ON kv_cache(atime);
41+
`);
42+
}
43+
44+
close() {
45+
this.db.close();
46+
}
47+
48+
/** 获取缓存 */
49+
get(key: string): CacheEntry | undefined {
50+
const stmt = this.db.prepare("SELECT * FROM kv_cache WHERE key = ?");
51+
const entry = stmt.get(key) as CacheEntry | undefined;
52+
if (entry) {
53+
// 更新访问时间
54+
this.db.prepare("UPDATE kv_cache SET atime = ? WHERE key = ?").run(Date.now(), key);
55+
}
56+
return entry;
57+
}
58+
59+
/** 写入缓存 */
60+
put(key: string, type: string, data: Buffer) {
61+
const now = Date.now();
62+
const stmt = this.db.prepare(`
63+
INSERT OR REPLACE INTO kv_cache (key, type, data, size, mtime, atime)
64+
VALUES (@key, @type, @data, @size, @mtime, @atime)
65+
`);
66+
stmt.run({
67+
key,
68+
type,
69+
data,
70+
size: data.length,
71+
mtime: now,
72+
atime: now,
73+
});
74+
}
75+
76+
/** 删除缓存 */
77+
remove(key: string) {
78+
this.db.prepare("DELETE FROM kv_cache WHERE key = ?").run(key);
79+
}
80+
81+
/** 清空某类缓存 */
82+
clear(type: string) {
83+
this.db.prepare("DELETE FROM kv_cache WHERE type = ?").run(type);
84+
}
85+
86+
/** 列出某类缓存 */
87+
list(type: string): Omit<CacheEntry, "data">[] {
88+
return this.db
89+
.prepare("SELECT key, type, size, mtime, atime FROM kv_cache WHERE type = ?")
90+
.all(type) as Omit<CacheEntry, "data">[];
91+
}
92+
93+
/** 获取某类缓存总大小 */
94+
getSize(type: string): number {
95+
const result = this.db
96+
.prepare("SELECT SUM(size) as total FROM kv_cache WHERE type = ?")
97+
.get(type) as { total: number };
98+
return result.total || 0;
99+
}
100+
101+
/** 获取所有缓存总大小 */
102+
getTotalSize(): number {
103+
const result = this.db.prepare("SELECT SUM(size) as total FROM kv_cache").get() as {
104+
total: number;
105+
};
106+
return result.total || 0;
107+
}
108+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import Database from "better-sqlite3";
2+
import { existsSync } from "fs";
3+
import { readFile, rename } from "fs/promises";
4+
5+
/** 当前本地音乐库 DB 版本,用于控制缓存结构升级 */
6+
const CURRENT_DB_VERSION = 2;
7+
8+
/** 音乐数据接口 */
9+
export interface MusicTrack {
10+
/** 文件id */
11+
id: string;
12+
/** 文件路径 */
13+
path: string;
14+
/** 文件标题 */
15+
title: string;
16+
/** 文件艺术家 */
17+
artist: string;
18+
/** 文件专辑 */
19+
album: string;
20+
/** 文件时长 */
21+
duration: number;
22+
/** 文件封面 */
23+
cover?: string;
24+
/** 文件修改时间 */
25+
mtime: number;
26+
/** 文件大小 */
27+
size: number;
28+
/** 文件码率(bps) */
29+
bitrate?: number;
30+
}
31+
32+
/** 旧版 JSON DB 接口 */
33+
interface LegacyMusicLibraryDB {
34+
version: number;
35+
tracks: Record<string, MusicTrack>;
36+
}
37+
38+
/** 本地音乐数据库管理类 */
39+
export class LocalMusicDB {
40+
private db: Database.Database | null = null;
41+
private dbPath: string;
42+
43+
constructor(dbPath: string) {
44+
this.dbPath = dbPath;
45+
}
46+
47+
/** 初始化数据库 */
48+
public init() {
49+
if (this.db) return;
50+
51+
try {
52+
this.db = new Database(this.dbPath);
53+
this.db.pragma("journal_mode = WAL");
54+
55+
this.db.exec(`
56+
CREATE TABLE IF NOT EXISTS tracks (
57+
id TEXT PRIMARY KEY,
58+
path TEXT NOT NULL UNIQUE,
59+
title TEXT,
60+
artist TEXT,
61+
album TEXT,
62+
duration REAL,
63+
cover TEXT,
64+
mtime REAL,
65+
size INTEGER,
66+
bitrate REAL
67+
);
68+
CREATE TABLE IF NOT EXISTS meta (
69+
key TEXT PRIMARY KEY,
70+
value TEXT
71+
);
72+
`);
73+
74+
// 检查版本
75+
const versionStmt = this.db.prepare("SELECT value FROM meta WHERE key = ?");
76+
const versionRow = versionStmt.get("version") as { value: string } | undefined;
77+
if (!versionRow) {
78+
this.db
79+
.prepare("INSERT INTO meta (key, value) VALUES (?, ?)")
80+
.run("version", CURRENT_DB_VERSION.toString());
81+
}
82+
} catch (e) {
83+
console.error("Failed to initialize SQLite DB:", e);
84+
throw e;
85+
}
86+
}
87+
88+
/** 关闭数据库 */
89+
public close() {
90+
if (this.db) {
91+
this.db.close();
92+
this.db = null;
93+
}
94+
}
95+
96+
/** 从 JSON 迁移数据 (如果存在) */
97+
public async migrateFromJsonIfNeeded(jsonPath: string) {
98+
if (!this.db) return;
99+
100+
// 检查是否已经有数据 (如果有数据则不迁移)
101+
const countStmt = this.db.prepare("SELECT COUNT(*) as count FROM tracks");
102+
const result = countStmt.get() as { count: number };
103+
if (result.count > 0) return;
104+
105+
if (existsSync(jsonPath)) {
106+
try {
107+
console.log("Migrating local music library from JSON to SQLite...");
108+
const data = await readFile(jsonPath, "utf-8");
109+
const parsed = JSON.parse(data) as LegacyMusicLibraryDB;
110+
111+
if (parsed.tracks) {
112+
this.addTracks(Object.values(parsed.tracks));
113+
console.log(`Migrated ${Object.keys(parsed.tracks).length} tracks.`);
114+
}
115+
116+
// 迁移完成后重命名 JSON 文件备份
117+
await rename(jsonPath, `${jsonPath}.bak`);
118+
} catch (e) {
119+
console.error("Failed to migrate from JSON:", e);
120+
}
121+
}
122+
}
123+
124+
/** 获取单曲 */
125+
public getTrack(path: string): MusicTrack | undefined {
126+
if (!this.db) return undefined;
127+
return this.db.prepare("SELECT * FROM tracks WHERE path = ?").get(path) as
128+
| MusicTrack
129+
| undefined;
130+
}
131+
132+
/** 批量添加/更新歌曲 */
133+
public addTracks(tracks: MusicTrack[]) {
134+
if (!this.db || tracks.length === 0) return;
135+
136+
const insertStmt = this.db.prepare(`
137+
INSERT OR REPLACE INTO tracks (id, path, title, artist, album, duration, cover, mtime, size, bitrate)
138+
VALUES (@id, @path, @title, @artist, @album, @duration, @cover, @mtime, @size, @bitrate)
139+
`);
140+
141+
const transaction = this.db.transaction((tracks: MusicTrack[]) => {
142+
for (const track of tracks) {
143+
insertStmt.run(track);
144+
}
145+
});
146+
147+
transaction(tracks);
148+
}
149+
150+
/** 批量删除歌曲 */
151+
public deleteTracks(paths: string[]) {
152+
if (!this.db || paths.length === 0) return;
153+
154+
const deleteStmt = this.db.prepare("DELETE FROM tracks WHERE path = ?");
155+
156+
const transaction = this.db.transaction((paths: string[]) => {
157+
for (const path of paths) {
158+
deleteStmt.run(path);
159+
}
160+
});
161+
162+
transaction(paths);
163+
}
164+
165+
/** 清空所有歌曲 */
166+
public clearTracks() {
167+
if (!this.db) return;
168+
this.db.prepare("DELETE FROM tracks").run();
169+
}
170+
171+
/** 获取所有歌曲路径 */
172+
public getAllPaths(): string[] {
173+
if (!this.db) return [];
174+
const rows = this.db.prepare("SELECT path FROM tracks").all() as { path: string }[];
175+
return rows.map((row) => row.path);
176+
}
177+
178+
/** 获取所有歌曲 */
179+
public getAllTracks(): MusicTrack[] {
180+
if (!this.db) return [];
181+
return this.db.prepare("SELECT * FROM tracks").all() as MusicTrack[];
182+
}
183+
}

0 commit comments

Comments
 (0)