mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-19 03:57:28 +08:00
fix(local-music): 封面落盘 + URL 编码统一,修复持久化配额与编码边界
- 新增 src/shared/localUrl.ts 共用 local:// 编码:按路径段 encodeURIComponent, 避免整体编码把 / 转成 %2F 引发 Chromium 解析边界差异,同时正确处理 空格/中文/# 等特殊字符(封面落到含空格目录时 Image loader 会加载失败) - 封面从内嵌 base64 Data URL 改为 userData/AudioCovers/<sha256>.<ext> 落盘, MAX_COVER_BYTES 1MB→8MB;老条目(无 coverPath 字段)扫描时一次性自愈 - playlist minify 剥离 base64 picUrl 并仅持久化 local:// 永不过期的 playMusicUrl, 防止单张 base64 封面撑爆 localStorage 5MB 配额导致整个 playList 写入失败; localStorage 写入加 try/catch 兜底,避免配额超限时直接抛异常
This commit is contained in:
@@ -150,10 +150,15 @@ export const useLocalMusicStore = defineStore(
|
||||
}
|
||||
|
||||
// 2. 增量扫描:基于修改时间筛选需重新解析的文件
|
||||
// 老条目(无 coverPath 字段)也视为需要重新解析,让数据自愈到统一格式
|
||||
const parseTargets: string[] = [];
|
||||
for (const file of files) {
|
||||
const cached = cachedMap.get(file.path);
|
||||
if (!cached || cached.modifiedTime !== file.modifiedTime) {
|
||||
if (
|
||||
!cached ||
|
||||
cached.modifiedTime !== file.modifiedTime ||
|
||||
!('coverPath' in cached)
|
||||
) {
|
||||
parseTargets.push(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,16 +24,29 @@ const getMessage = () => {
|
||||
|
||||
/**
|
||||
* 精简 SongResult 对象,只保留持久化必要字段
|
||||
* 排除大体积字段:lyric, song, playMusicUrl, backgroundColor, primaryColor
|
||||
* 排除大体积字段:lyric, song, backgroundColor, primaryColor
|
||||
*
|
||||
* picUrl/al.picUrl 若为 base64 Data URL 一律剥离:localStorage 仅 5MB 配额,
|
||||
* 单张 base64 封面动辄几百 KB,几首就能撑爆导致整个 playList 写入失败。
|
||||
* 剥离后恢复时展示默认封面图,picUrl 仍是 http(s):// 或 local:// 短引用时原样保留。
|
||||
*
|
||||
* 仅 local:// 的 playMusicUrl(永不过期)会被持久化,让本地音乐恢复后免重新解析;
|
||||
* expiredAt 不持久化——本地音乐每次走 toSongResult 会重新生成,远程歌曲恢复后重新拉详情即可。
|
||||
*/
|
||||
const stripDataUrl = (url: string | undefined): string =>
|
||||
!url || url.startsWith('data:') ? '' : url;
|
||||
|
||||
const minifySong = (s: SongResult) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
picUrl: s.picUrl,
|
||||
picUrl: stripDataUrl(s.picUrl),
|
||||
ar: s.ar?.map((a) => ({ id: a.id, name: a.name })),
|
||||
al: s.al,
|
||||
al: s.al && { id: s.al.id, name: s.al.name, picUrl: stripDataUrl(s.al.picUrl) },
|
||||
source: s.source,
|
||||
dt: s.dt
|
||||
dt: s.dt,
|
||||
// 仅 local:// 永不过期,保留给本地音乐恢复后免重新解析;其他 URL 会过期,丢掉让恢复时重新拉
|
||||
// JSON.stringify 自动丢 undefined,无需条件 spread
|
||||
playMusicUrl: s.playMusicUrl?.startsWith('local://') ? s.playMusicUrl : undefined
|
||||
});
|
||||
|
||||
const minifySongList = (list: SongResult[] | undefined) => list?.map(minifySong) ?? [];
|
||||
@@ -44,15 +57,23 @@ const minifySongList = (list: SongResult[] | undefined) => list?.map(minifySong)
|
||||
*/
|
||||
const pendingWrites = new Map<string, string>();
|
||||
|
||||
const safeSetItem = (key: string, value: string) => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
console.error('[playlist] localStorage 写入失败(可能超出配额):', error);
|
||||
}
|
||||
};
|
||||
|
||||
const flushPendingWrites = () => {
|
||||
pendingWrites.forEach((value, key) => {
|
||||
localStorage.setItem(key, value);
|
||||
safeSetItem(key, value);
|
||||
});
|
||||
pendingWrites.clear();
|
||||
};
|
||||
|
||||
const debouncedSetItem = debounce((key: string, value: string) => {
|
||||
localStorage.setItem(key, value);
|
||||
safeSetItem(key, value);
|
||||
pendingWrites.delete(key);
|
||||
}, 2000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user