Skip to content

Commit a662654

Browse files
committed
✨ feat: 下一首歌曲封面预载 & 可单独关闭歌曲缓存
1 parent 5ab998b commit a662654

6 files changed

Lines changed: 102 additions & 26 deletions

File tree

electron/main/services/CacheService.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ export class CacheService {
207207
const { target } = this.resolveSafePath(type, key);
208208
const buffer = this.toBuffer(data);
209209

210+
// 检查并清理超限缓存
211+
await this.checkAndCleanCache();
212+
210213
// 检查旧文件大小
211214
let oldSize = 0;
212215
try {
@@ -243,6 +246,51 @@ export class CacheService {
243246
this.sizes[type] = this.sizes[type] - oldSize + dataToWrite.length;
244247
}
245248

249+
/**
250+
* 检查并清理超限缓存
251+
* 优先清理 music 类型的缓存
252+
*/
253+
public async checkAndCleanCache(): Promise<void> {
254+
const store = useStore();
255+
const limitSizeGB = store.get("cacheLimit") ?? 10;
256+
257+
// 如果设置为 0,则不限制
258+
if (limitSizeGB <= 0) return;
259+
260+
const limitSizeBytes = limitSizeGB * 1024 * 1024 * 1024;
261+
const currentSize = await this.getSize();
262+
263+
if (currentSize <= limitSizeBytes) return;
264+
265+
// 需要腾出的空间(至少 100MB)
266+
const targetFreeSize = currentSize - limitSizeBytes + 100 * 1024 * 1024;
267+
let freedSize = 0;
268+
269+
// 清理顺序:优先清理 music,然后是其他类型
270+
const cleanOrder: CacheResourceType[] = ["music", "lyrics", "list-data", "local-data"];
271+
272+
for (const cacheType of cleanOrder) {
273+
if (freedSize >= targetFreeSize) break;
274+
275+
const items = await this.list(cacheType);
276+
// 按 atime 升序排序 (最久未访问的在前)
277+
items.sort((a, b) => a.atime - b.atime);
278+
279+
for (const item of items) {
280+
if (freedSize >= targetFreeSize) break;
281+
try {
282+
await this.remove(cacheType, item.key);
283+
freedSize += item.size;
284+
cacheLog.info(`Cleaned cache: ${cacheType}/${item.key}, size: ${item.size}`);
285+
} catch (e) {
286+
cacheLog.warn(`Failed to remove cache file: ${item.key}`, e);
287+
}
288+
}
289+
}
290+
291+
cacheLog.info(`Cache cleanup completed. Freed: ${freedSize} bytes`);
292+
}
293+
246294
/**
247295
* 读取缓存
248296
*/

electron/main/services/MusicCacheService.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { existsSync, createWriteStream } from "fs";
22
import { unlink, rename, stat } from "fs/promises";
33
import { pipeline } from "stream/promises";
44
import { CacheService } from "./CacheService";
5-
import { useStore } from "../store";
65
import { cacheLog } from "../logger";
76
import got from "got";
87

@@ -54,9 +53,7 @@ export class MusicCacheService {
5453
const items = await this.cacheService.list("music");
5554
// 查找以 id_ 开头且以 .sc 结尾的文件(排除 .tmp 文件)
5655
const prefix = `${id}_`;
57-
const match = items.find(
58-
(item) => item.key.startsWith(prefix) && item.key.endsWith(".sc"),
59-
);
56+
const match = items.find((item) => item.key.startsWith(prefix) && item.key.endsWith(".sc"));
6057
if (match) {
6158
return this.cacheService.getFilePath("music", match.key);
6259
}
@@ -74,30 +71,16 @@ export class MusicCacheService {
7471
* @returns 缓存后的本地文件路径
7572
*/
7673
public async cacheMusic(id: number | string, url: string, quality: string): Promise<string> {
77-
const store = useStore();
78-
const limitSizeGB = store.get("cacheLimit") ?? 10;
79-
const limitSizeBytes = limitSizeGB * 1024 * 1024 * 1024;
80-
81-
// 如果设置为 0,则不限制
82-
if (limitSizeGB > 0) {
83-
const currentSize = await this.cacheService.getSize();
84-
85-
if (currentSize > limitSizeBytes) {
86-
// 腾出至少 100MB 空间
87-
await this.cacheService.cleanOldCache(
88-
"music",
89-
currentSize - limitSizeBytes + 100 * 1024 * 1024,
90-
);
91-
}
92-
}
93-
9474
const key = this.getCacheKey(id, quality);
9575
const filePath = this.cacheService.getFilePath("music", key);
9676
const tempPath = `${filePath}.tmp`;
9777

9878
// 确保目录存在
9979
await this.cacheService.init();
10080

81+
// 检查并清理超限缓存
82+
await this.cacheService.checkAndCleanCache();
83+
10184
// 下载并写入
10285
try {
10386
const downloadStream = got.stream(url);

src/components/Setting/LocalSetting.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@
108108
<n-switch class="set" v-model:value="settingStore.cacheEnabled" :round="false" />
109109
</n-card>
110110
<n-collapse-transition :show="settingStore.cacheEnabled">
111+
<n-card class="set-item">
112+
<div class="label">
113+
<n-text class="name">缓存歌曲</n-text>
114+
<n-text class="tip" :depth="3">是否缓存歌曲音频,关闭后可节省缓存空间</n-text>
115+
</div>
116+
<n-switch class="set" v-model:value="settingStore.songCacheEnabled" :round="false" />
117+
</n-card>
111118
<n-card class="set-item">
112119
<div class="label">
113120
<n-text class="name">缓存大小上限</n-text>

src/components/Setting/PlaySetting.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@
333333

334334
<script setup lang="ts">
335335
import type { SelectOption } from "naive-ui";
336-
import { useSettingStore, useStatusStore } from "@/stores";
336+
import { useSettingStore } from "@/stores";
337337
import { isLogin } from "@/utils/auth";
338338
import { renderOption } from "@/utils/helper";
339339
import { isElectron } from "@/utils/env";
@@ -342,7 +342,6 @@ import { usePlayerController } from "@/core/player/PlayerController";
342342
import { openSongUnlockManager } from "@/utils/modal";
343343
344344
const player = usePlayerController();
345-
const statusStore = useStatusStore();
346345
const settingStore = useSettingStore();
347346
// 输出设备数据
348347
const outputDevices = ref<SelectOption[]>([]);

src/core/player/SongManager.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,47 @@ class SongManager {
3636
/** 预载下一首歌曲播放信息 */
3737
private nextPrefetch: AudioSource | undefined;
3838

39+
/**
40+
* 预加载封面图片
41+
* @param song 歌曲信息
42+
*/
43+
private prefetchCover(song: SongType): void {
44+
if (!song || song.path) return; // 本地歌曲跳过
45+
46+
const coverUrls: string[] = [];
47+
48+
// 收集需要预加载的封面 URL
49+
if (song.coverSize) {
50+
// 优先预加载大尺寸封面
51+
if (song.coverSize.xl) coverUrls.push(song.coverSize.xl);
52+
if (song.coverSize.l) coverUrls.push(song.coverSize.l);
53+
}
54+
if (song.cover && !coverUrls.includes(song.cover)) {
55+
coverUrls.push(song.cover);
56+
}
57+
// 预加载图片
58+
coverUrls.forEach((url) => {
59+
if (!url || !url.startsWith("http")) return;
60+
const img = new Image();
61+
// 清理
62+
const cleanup = () => {
63+
img.onload = null;
64+
img.onerror = null;
65+
};
66+
img.onload = cleanup;
67+
img.onerror = cleanup;
68+
img.src = url;
69+
});
70+
}
71+
3972
/**
4073
* 检查本地缓存
4174
* @param id 歌曲id
4275
* @param quality 音质
4376
*/
4477
private checkLocalCache = async (id: number, quality?: QualityType): Promise<string | null> => {
4578
const settingStore = useSettingStore();
46-
if (isElectron && settingStore.cacheEnabled) {
79+
if (isElectron && settingStore.cacheEnabled && settingStore.songCacheEnabled) {
4780
try {
4881
const cachePath = await window.electron.ipcRenderer.invoke(
4982
"music-cache-check",
@@ -69,7 +102,7 @@ class SongManager {
69102
*/
70103
private triggerCacheDownload = (id: number, url: string, quality?: QualityType | string) => {
71104
const settingStore = useSettingStore();
72-
if (isElectron && settingStore.cacheEnabled && url) {
105+
if (isElectron && settingStore.cacheEnabled && settingStore.songCacheEnabled && url) {
73106
window.electron.ipcRenderer.invoke("music-cache-download", id, url, quality || "standard");
74107
}
75108
};
@@ -189,6 +222,9 @@ class SongManager {
189222
const nextSong = playList[nextIndex];
190223
if (!nextSong) return;
191224

225+
// 预加载封面图片
226+
this.prefetchCover(nextSong);
227+
192228
// 本地歌曲跳过
193229
if (nextSong.path) return;
194230

src/stores/setting.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export interface SettingState {
7575
downloadPath: string;
7676
/** 是否启用缓存 */
7777
cacheEnabled: boolean;
78+
/** 是否缓存歌曲(音频文件) */
79+
songCacheEnabled: boolean;
7880
/** 音乐命名格式 */
7981
fileNameFormat: "title" | "artist-title" | "title-artist";
8082
/** 文件智能分类 */
@@ -140,7 +142,7 @@ export interface SettingState {
140142
/** 背景动画流动速度 */
141143
playerBackgroundFlowSpeed: number;
142144
/** 背景动画是否在歌曲暂停时暂停 */
143-
playerBackgroundPause: boolean
145+
playerBackgroundPause: boolean;
144146
/** 播放器元素自动隐藏 */
145147
autoHidePlayerMeta: boolean;
146148
/** 记忆最后进度 */
@@ -388,6 +390,7 @@ export const useSettingStore = defineStore("setting", {
388390
showLocalCover: true,
389391
downloadPath: "",
390392
cacheEnabled: true,
393+
songCacheEnabled: true,
391394
fileNameFormat: "title-artist",
392395
folderStrategy: "none",
393396
downloadMeta: true,

0 commit comments

Comments
 (0)