Skip to content
Merged
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
11 changes: 11 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare module 'vue' {
AutoCloseDialog: typeof import('./src/components/modals/AutoCloseDialog.vue')['default']
BackgroundImagePicker: typeof import('./src/components/settings/custom/BackgroundImagePicker.vue')['default']
BottomSpectrum: typeof import('./src/components/player/FullPlayer/BottomSpectrum.vue')['default']
CacheManager: typeof import('./src/components/settings/custom/CacheManager.vue')['default']
ComboboxAnchor: typeof import('reka-ui')['ComboboxAnchor']
ComboboxContent: typeof import('reka-ui')['ComboboxContent']
ComboboxEmpty: typeof import('reka-ui')['ComboboxEmpty']
Expand All @@ -37,6 +38,7 @@ declare module 'vue' {
ContextMenuSubTrigger: typeof import('reka-ui')['ContextMenuSubTrigger']
ContextMenuTrigger: typeof import('reka-ui')['ContextMenuTrigger']
CoverList: typeof import('./src/components/list/CoverList.vue')['default']
DbCacheManager: typeof import('./src/components/settings/custom/DbCacheManager.vue')['default']
DeviceSelector: typeof import('./src/components/settings/custom/DeviceSelector.vue')['default']
DialogClose: typeof import('reka-ui')['DialogClose']
DialogContent: typeof import('reka-ui')['DialogContent']
Expand All @@ -56,6 +58,7 @@ declare module 'vue' {
DropdownMenuTrigger: typeof import('reka-ui')['DropdownMenuTrigger']
EqualizerDialog: typeof import('./src/components/modals/EqualizerDialog.vue')['default']
ExcludeLyricsConfig: typeof import('./src/components/settings/custom/ExcludeLyricsConfig.vue')['default']
FileCacheManager: typeof import('./src/components/settings/custom/FileCacheManager.vue')['default']
FontConfig: typeof import('./src/components/settings/custom/FontConfig.vue')['default']
FullPlayer: typeof import('./src/components/player/FullPlayer/index.vue')['default']
HotkeyConfig: typeof import('./src/components/settings/custom/HotkeyConfig.vue')['default']
Expand Down Expand Up @@ -90,6 +93,7 @@ declare module 'vue' {
IconLucideMusic: typeof import('~icons/lucide/music')['default']
IconLucidePause: typeof import('~icons/lucide/pause')['default']
IconLucidePlay: typeof import('~icons/lucide/play')['default']
IconLucidePlus: typeof import('~icons/lucide/plus')['default']
IconLucidePower: typeof import('~icons/lucide/power')['default']
IconLucidePuzzle: typeof import('~icons/lucide/puzzle')['default']
IconLucideRefreshCw: typeof import('~icons/lucide/refresh-cw')['default']
Expand All @@ -98,6 +102,7 @@ declare module 'vue' {
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideSearchX: typeof import('~icons/lucide/search-x')['default']
IconLucideSettings: typeof import('~icons/lucide/settings')['default']
IconLucideSettings2: typeof import('~icons/lucide/settings2')['default']
IconLucideShuffle: typeof import('~icons/lucide/shuffle')['default']
IconLucideSkipBack: typeof import('~icons/lucide/skip-back')['default']
IconLucideSkipForward: typeof import('~icons/lucide/skip-forward')['default']
Expand All @@ -115,6 +120,10 @@ declare module 'vue' {
Lyrics: typeof import('./src/components/player/Lyrics/index.vue')['default']
LyricSourceOrderConfig: typeof import('./src/components/settings/custom/LyricSourceOrderConfig.vue')['default']
NavHeader: typeof import('./src/layouts/components/NavHeader.vue')['default']
NumberFieldDecrement: typeof import('reka-ui')['NumberFieldDecrement']
NumberFieldIncrement: typeof import('reka-ui')['NumberFieldIncrement']
NumberFieldInput: typeof import('reka-ui')['NumberFieldInput']
NumberFieldRoot: typeof import('reka-ui')['NumberFieldRoot']
PlayerBackground: typeof import('./src/components/player/FullPlayer/PlayerBackground.vue')['default']
PlayerBar: typeof import('./src/components/player/PlayerBar.vue')['default']
PlayerControls: typeof import('./src/components/player/PlayerControls.vue')['default']
Expand Down Expand Up @@ -166,6 +175,8 @@ declare module 'vue' {
SLogo: typeof import('./src/components/ui/SLogo.vue')['default']
SMarquee: typeof import('./src/components/ui/SMarquee.vue')['default']
SMenu: typeof import('./src/components/ui/SMenu.vue')['default']
SNumberInput: typeof import('./src/components/ui/SNumberInput.vue')['default']
SongCacheSizeLimit: typeof import('./src/components/settings/custom/SongCacheSizeLimit.vue')['default']
SongList: typeof import('./src/components/list/SongList.vue')['default']
SpeedDialog: typeof import('./src/components/modals/SpeedDialog.vue')['default']
SPopover: typeof import('./src/components/ui/SPopover.vue')['default']
Expand Down
10 changes: 5 additions & 5 deletions electron/main/apis/musicbrainz.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { net } from "electron";
import fs from "node:fs";
import path from "node:path";
import { artistCacheDir } from "@main/utils/config";
import { getArtistCacheDir } from "@main/utils/config";
import { toCacheUrl } from "@main/utils/protocol";

/** MusicBrainz User-Agent */
Expand Down Expand Up @@ -34,8 +34,8 @@ const getNotFoundFileName = (artistName: string): string => {

/** 确保缓存目录存在 */
const ensureCacheDir = (): void => {
if (!fs.existsSync(artistCacheDir)) {
fs.mkdirSync(artistCacheDir, { recursive: true });
if (!fs.existsSync(getArtistCacheDir())) {
fs.mkdirSync(getArtistCacheDir(), { recursive: true });
}
};

Expand Down Expand Up @@ -92,13 +92,13 @@ const fetchArtistAvatarCore = async (artistName: string): Promise<string | null>
// 确保目录存在
ensureCacheDir();
const cacheFileName = getCacheFileName(name);
const cachePath = path.join(artistCacheDir, cacheFileName);
const cachePath = path.join(getArtistCacheDir(), cacheFileName);
// 已缓存
if (fs.existsSync(cachePath)) {
return toCacheUrl(cachePath) ?? null;
}
// 已标记为未找到(7 天过期)
const notFoundPath = path.join(artistCacheDir, getNotFoundFileName(name));
const notFoundPath = path.join(getArtistCacheDir(), getNotFoundFileName(name));
if (fs.existsSync(notFoundPath)) {
const stat = fs.statSync(notFoundPath);
if (Date.now() - stat.mtimeMs < 7 * 24 * 60 * 60 * 1000) return null;
Expand Down
3 changes: 3 additions & 0 deletions electron/main/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { registerIpcHandlers } from "@main/ipc";
import { init as initMedia, shutdown as shutdownMedia } from "@main/services/media";
import { initGlobalHotkey } from "@main/services/globalHotkey";
import { initDatabase, closeDatabase } from "@main/database";
import { init as initSongCache } from "@main/services/songCache";
import { pluginRegistry } from "@main/plugins/registry";
import { registerCacheScheme, handleCacheProtocol } from "@main/utils/protocol";
import { coreLog, initLogger } from "@main/utils/logger";
Expand Down Expand Up @@ -58,6 +59,8 @@ export const initApp = (): void => {
});
// 初始化数据库
initDatabase();
// 启动歌曲缓存服务
initSongCache();
// 注册 IPC
registerIpcHandlers();
// 初始化系统媒体控件
Expand Down
12 changes: 12 additions & 0 deletions electron/main/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ export const initDatabase = (): void => {
fetched_at INTEGER NOT NULL,
PRIMARY KEY (platform, id)
);

CREATE TABLE IF NOT EXISTS song_cache (
cache_key TEXT PRIMARY KEY,
source TEXT NOT NULL,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
mime TEXT,
cached_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_song_cache_last_used ON song_cache(last_used_at);
CREATE INDEX IF NOT EXISTS idx_song_cache_source ON song_cache(source);
`);
migrate(db);
libraryLog.info(`数据库已初始化: ${dbPath}`);
Expand Down
133 changes: 133 additions & 0 deletions electron/main/database/songCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { TrackSource } from "@shared/types/player";
import { getDb } from "./index";

/** 单条记录 */
export interface SongCacheRow {
cacheKey: string;
source: TrackSource;
filename: string;
size: number;
mime: string | null;
cachedAt: number;
lastUsedAt: number;
}

/** sqlite 原始行 */
interface RawRow {
cache_key: string;
source: TrackSource;
filename: string;
size: number;
mime: string | null;
cached_at: number;
last_used_at: number;
}

/** sqlite snake_case 行 → 业务 camelCase */
const toRow = (raw: RawRow): SongCacheRow => ({
cacheKey: raw.cache_key,
source: raw.source,
filename: raw.filename,
size: raw.size,
mime: raw.mime,
cachedAt: raw.cached_at,
lastUsedAt: raw.last_used_at,
});

/**
* 按 key 查记录
* @param cacheKey - 缓存键
* @returns 记录
*/
export const findByKey = (cacheKey: string): SongCacheRow | null => {
const raw = getDb().prepare("SELECT * FROM song_cache WHERE cache_key = ?").get(cacheKey) as
| RawRow
| undefined;
return raw ? toRow(raw) : null;
};

/**
* 按 filename 反查
* @param filename - 文件名
* @returns 记录
*/
export const findByFilename = (filename: string): SongCacheRow | null => {
const raw = getDb().prepare("SELECT * FROM song_cache WHERE filename = ?").get(filename) as
| RawRow
| undefined;
return raw ? toRow(raw) : null;
};

/**
* 更新最近访问时间(命中 LRU)
* @param cacheKey - 缓存键
* @param now - 当前时间
*/
export const touchLastUsed = (cacheKey: string, now: number): void => {
getDb().prepare("UPDATE song_cache SET last_used_at = ? WHERE cache_key = ?").run(now, cacheKey);
};

/**
* 写入或覆盖一条记录
* @param row - 记录
*/
export const upsert = (row: SongCacheRow): void => {
getDb()
.prepare(
`INSERT INTO song_cache (cache_key, source, filename, size, mime, cached_at, last_used_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(cache_key) DO UPDATE SET
source = excluded.source,
filename = excluded.filename,
size = excluded.size,
mime = excluded.mime,
cached_at = excluded.cached_at,
last_used_at = excluded.last_used_at`,
)
.run(row.cacheKey, row.source, row.filename, row.size, row.mime, row.cachedAt, row.lastUsedAt);
};

/**
* 删除一条
* @param cacheKey - 缓存键
*/
export const deleteByKey = (cacheKey: string): void => {
getDb().prepare("DELETE FROM song_cache WHERE cache_key = ?").run(cacheKey);
};

/**
* 当前总占用字节数
* @returns 总占用字节数
*/
export const totalSize = (): number => {
const row = getDb().prepare("SELECT SUM(size) AS total FROM song_cache").get() as
| { total: number | null }
| undefined;
return row?.total ?? 0;
};

/**
* 列出全部 filename
* @returns 文件名列表
*/
export const listAllFilenames = (): string[] => {
const rows = getDb().prepare("SELECT filename FROM song_cache").all() as { filename: string }[];
return rows.map((entry) => entry.filename);
};

/**
* LRU 淘汰候选:按 last_used_at 升序取前 limit 条
* @param limit - 限制数量
* @returns 记录列表
*/
export const listLruVictims = (limit: number): SongCacheRow[] => {
const rows = getDb()
.prepare("SELECT * FROM song_cache ORDER BY last_used_at ASC LIMIT ?")
.all(limit) as RawRow[];
return rows.map(toRow);
};

/** 清空整张表 */
export const clearAll = (): void => {
getDb().prepare("DELETE FROM song_cache").run();
};
Loading
Loading