音乐播放记录多设备同步。

This commit is contained in:
mtvpls
2026-02-01 17:07:50 +08:00
parent 2ef04bc434
commit 4cb5bc27c6
8 changed files with 647 additions and 2 deletions
+271
View File
@@ -57,6 +57,19 @@ export interface Favorite {
vod_remarks?: string; // 视频备注信息
}
// ---- 音乐播放记录类型 ----
export interface MusicPlayRecord {
platform: 'netease' | 'qq' | 'kuwo'; // 音乐平台
id: string; // 歌曲ID
name: string; // 歌曲名
artist: string; // 艺术家
album?: string; // 专辑
pic?: string; // 封面图
play_time: number; // 播放进度(秒)
duration: number; // 总时长(秒)
save_time: number; // 记录保存时间(时间戳)
}
// ---- 缓存数据结构 ----
interface CacheData<T> {
data: T;
@@ -70,12 +83,14 @@ interface UserCacheStore {
searchHistory?: CacheData<string[]>;
skipConfigs?: CacheData<Record<string, SkipConfig>>;
danmakuFilterConfig?: CacheData<DanmakuFilterConfig>;
musicPlayRecords?: CacheData<Record<string, MusicPlayRecord>>; // 音乐播放记录
}
// ---- 常量 ----
const PLAY_RECORDS_KEY = 'moontv_play_records';
const FAVORITES_KEY = 'moontv_favorites';
const SEARCH_HISTORY_KEY = 'moontv_search_history';
const MUSIC_PLAY_RECORDS_KEY = 'moontv_music_play_records';
// 缓存相关常量
const CACHE_PREFIX = 'moontv_cache_';
@@ -398,6 +413,32 @@ class HybridCacheManager {
this.saveUserCache(username, userCache);
}
/**
* 音乐播放记录缓存方法
*/
getCachedMusicPlayRecords(): Record<string, MusicPlayRecord> | null {
const username = this.getCurrentUsername();
if (!username) return null;
const userCache = this.getUserCache(username);
const cached = userCache.musicPlayRecords;
if (cached && this.isCacheValid(cached)) {
return cached.data;
}
return null;
}
cacheMusicPlayRecords(data: Record<string, MusicPlayRecord>): void {
const username = this.getCurrentUsername();
if (!username) return;
const userCache = this.getUserCache(username);
userCache.musicPlayRecords = this.createCacheData(data);
this.saveUserCache(username, userCache);
}
/**
* 清除指定用户的所有缓存
*/
@@ -1888,6 +1929,236 @@ export async function saveDanmakuFilterConfig(
}
}
// ---------------- 音乐播放记录相关 API ----------------
/**
* 获取全部音乐播放记录。
* 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。
*/
export async function getAllMusicPlayRecords(): Promise<Record<string, MusicPlayRecord>> {
// 服务器端渲染阶段直接返回空
if (typeof window === 'undefined') {
return {};
}
// 数据库存储模式:使用混合缓存策略(包括 redis 和 upstash
if (STORAGE_TYPE !== 'localstorage') {
// 优先从缓存获取数据
const cachedData = cacheManager.getCachedMusicPlayRecords();
if (cachedData) {
// 返回缓存数据,同时后台异步更新
fetchFromApi<Record<string, MusicPlayRecord>>(`/api/music/playrecords`)
.then((freshData) => {
// 只有数据真正不同时才更新缓存
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
cacheManager.cacheMusicPlayRecords(freshData);
// 触发数据更新事件
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: freshData,
})
);
}
})
.catch((err) => {
console.warn('后台同步音乐播放记录失败:', err);
triggerGlobalError('后台同步音乐播放记录失败');
});
return cachedData;
} else {
// 缓存为空,直接从 API 获取并缓存
try {
const freshData = await fetchFromApi<Record<string, MusicPlayRecord>>(
`/api/music/playrecords`
);
cacheManager.cacheMusicPlayRecords(freshData);
return freshData;
} catch (err) {
console.error('获取音乐播放记录失败:', err);
triggerGlobalError('获取音乐播放记录失败');
return {};
}
}
}
// localstorage 模式
try {
const raw = localStorage.getItem(MUSIC_PLAY_RECORDS_KEY);
if (!raw) return {};
return JSON.parse(raw) as Record<string, MusicPlayRecord>;
} catch (err) {
console.error('读取音乐播放记录失败:', err);
triggerGlobalError('读取音乐播放记录失败');
return {};
}
}
/**
* 保存音乐播放记录。
* 数据库存储模式下使用乐观更新:先更新缓存(立即生效),再异步同步到数据库。
*/
export async function saveMusicPlayRecord(
platform: string,
id: string,
record: MusicPlayRecord
): Promise<void> {
const key = generateStorageKey(platform, id);
// 数据库存储模式:乐观更新策略(包括 redis 和 upstash
if (STORAGE_TYPE !== 'localstorage') {
// 立即更新缓存
const cachedRecords = cacheManager.getCachedMusicPlayRecords() || {};
cachedRecords[key] = record;
cacheManager.cacheMusicPlayRecords(cachedRecords);
// 触发立即更新事件
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: cachedRecords,
})
);
// 异步同步到数据库
try {
await fetchWithAuth('/api/music/playrecords', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key, record }),
});
} catch (err) {
console.error('保存音乐播放记录失败:', err);
triggerGlobalError('保存音乐播放记录失败');
throw err;
}
return;
}
// localstorage 模式
if (typeof window === 'undefined') {
console.warn('无法在服务端保存音乐播放记录到 localStorage');
return;
}
try {
const allRecords = await getAllMusicPlayRecords();
allRecords[key] = record;
localStorage.setItem(MUSIC_PLAY_RECORDS_KEY, JSON.stringify(allRecords));
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: allRecords,
})
);
} catch (err) {
console.error('保存音乐播放记录失败:', err);
triggerGlobalError('保存音乐播放记录失败');
throw err;
}
}
/**
* 删除音乐播放记录。
* 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。
*/
export async function deleteMusicPlayRecord(
platform: string,
id: string
): Promise<void> {
const key = generateStorageKey(platform, id);
// 数据库存储模式:乐观更新策略(包括 redis 和 upstash
if (STORAGE_TYPE !== 'localstorage') {
// 立即更新缓存
const cachedRecords = cacheManager.getCachedMusicPlayRecords() || {};
delete cachedRecords[key];
cacheManager.cacheMusicPlayRecords(cachedRecords);
// 触发立即更新事件
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: cachedRecords,
})
);
// 异步同步到数据库
try {
await fetchWithAuth(`/api/music/playrecords?key=${encodeURIComponent(key)}`, {
method: 'DELETE',
});
} catch (err) {
console.error('删除音乐播放记录失败:', err);
triggerGlobalError('删除音乐播放记录失败');
throw err;
}
return;
}
// localstorage 模式
if (typeof window === 'undefined') {
console.warn('无法在服务端删除音乐播放记录到 localStorage');
return;
}
try {
const allRecords = await getAllMusicPlayRecords();
delete allRecords[key];
localStorage.setItem(MUSIC_PLAY_RECORDS_KEY, JSON.stringify(allRecords));
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: allRecords,
})
);
} catch (err) {
console.error('删除音乐播放记录失败:', err);
triggerGlobalError('删除音乐播放记录失败');
throw err;
}
}
/**
* 清空全部音乐播放记录
* 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。
*/
export async function clearAllMusicPlayRecords(): Promise<void> {
// 数据库存储模式:乐观更新策略(包括 redis 和 upstash
if (STORAGE_TYPE !== 'localstorage') {
// 立即更新缓存
cacheManager.cacheMusicPlayRecords({});
// 触发立即更新事件
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: {},
})
);
// 异步同步到数据库
try {
await fetchWithAuth(`/api/music/playrecords`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
} catch (err) {
console.error('清空音乐播放记录失败:', err);
triggerGlobalError('清空音乐播放记录失败');
throw err;
}
return;
}
// localStorage 模式
if (typeof window === 'undefined') return;
localStorage.removeItem(MUSIC_PLAY_RECORDS_KEY);
window.dispatchEvent(
new CustomEvent('musicPlayRecordsUpdated', {
detail: {},
})
);
}
// ---------------- 集数过滤配置相关 API ----------------
/**