song cache
This commit is contained in:
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { MusicPlayRecord } from '@/lib/db.client';
|
import { MusicPlayRecord } from '@/lib/db.client';
|
||||||
|
import { getCachedSongs, setCachedSong } from '@/lib/music-song-cache';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -29,7 +30,28 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const records = await db.getAllMusicPlayRecords(authInfo.username);
|
const records = await db.getAllMusicPlayRecords(authInfo.username);
|
||||||
return NextResponse.json(records, { status: 200 });
|
|
||||||
|
// 从缓存中获取歌曲信息并填充到记录中
|
||||||
|
const keys = Object.keys(records).map(key => {
|
||||||
|
const [platform, id] = key.split('+');
|
||||||
|
return { platform, id };
|
||||||
|
});
|
||||||
|
const cachedSongs = getCachedSongs(keys);
|
||||||
|
|
||||||
|
// 将缓存的歌曲信息合并到记录中
|
||||||
|
const enrichedRecords: Record<string, MusicPlayRecord> = {};
|
||||||
|
for (const [key, record] of Object.entries(records)) {
|
||||||
|
const cachedSong = cachedSongs.get(key);
|
||||||
|
enrichedRecords[key] = {
|
||||||
|
...record,
|
||||||
|
name: cachedSong?.name || record.name,
|
||||||
|
artist: cachedSong?.artist || record.artist,
|
||||||
|
album: cachedSong?.album || record.album,
|
||||||
|
pic: cachedSong?.pic || record.pic,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(enrichedRecords, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取音乐播放记录失败', err);
|
console.error('获取音乐播放记录失败', err);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -86,6 +108,16 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.saveMusicPlayRecord(authInfo.username, platform, id, record);
|
await db.saveMusicPlayRecord(authInfo.username, platform, id, record);
|
||||||
|
|
||||||
|
// 缓存歌曲信息到服务器内存
|
||||||
|
setCachedSong(platform, id, {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
artist: record.artist,
|
||||||
|
album: record.album,
|
||||||
|
pic: record.pic,
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 });
|
return NextResponse.json({ success: true }, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('保存音乐播放记录失败', err);
|
console.error('保存音乐播放记录失败', err);
|
||||||
|
|||||||
+83
-2
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
getAllMusicPlayRecords,
|
||||||
|
saveMusicPlayRecord,
|
||||||
|
MusicPlayRecord,
|
||||||
|
} from '@/lib/db.client';
|
||||||
|
|
||||||
interface Song {
|
interface Song {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -145,9 +150,49 @@ export default function MusicPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面加载时恢复播放状态
|
// 页面加载时恢复播放状态和数据库记录
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
restorePlayState();
|
const initializePlayState = async () => {
|
||||||
|
// 先恢复 localStorage 中的播放状态
|
||||||
|
restorePlayState();
|
||||||
|
|
||||||
|
// 从数据库加载播放记录
|
||||||
|
try {
|
||||||
|
const dbRecords = await getAllMusicPlayRecords();
|
||||||
|
|
||||||
|
// 将数据库记录转换为前端格式
|
||||||
|
const records: PlayRecord[] = [];
|
||||||
|
const songs: Song[] = [];
|
||||||
|
|
||||||
|
Object.entries(dbRecords).forEach(([key, record]) => {
|
||||||
|
records.push({
|
||||||
|
platform: record.platform,
|
||||||
|
id: record.id,
|
||||||
|
playTime: record.play_time,
|
||||||
|
duration: record.duration,
|
||||||
|
timestamp: record.save_time,
|
||||||
|
});
|
||||||
|
|
||||||
|
songs.push({
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
artist: record.artist,
|
||||||
|
album: record.album,
|
||||||
|
pic: record.pic,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
if (records.length > 0) {
|
||||||
|
setPlayRecords(records);
|
||||||
|
setPlaylist(songs);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载播放记录失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializePlayState();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听播放状态变化,自动保存
|
// 监听播放状态变化,自动保存
|
||||||
@@ -578,6 +623,24 @@ export default function MusicPage() {
|
|||||||
...updated[playlistIndex],
|
...updated[playlistIndex],
|
||||||
playTime: audio.currentTime,
|
playTime: audio.currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
const record = updated[playlistIndex];
|
||||||
|
const dbRecord: MusicPlayRecord = {
|
||||||
|
platform: record.platform,
|
||||||
|
id: record.id,
|
||||||
|
name: currentSong.name,
|
||||||
|
artist: currentSong.artist,
|
||||||
|
album: currentSong.album,
|
||||||
|
pic: currentSong.pic,
|
||||||
|
play_time: audio.currentTime,
|
||||||
|
duration: audio.duration || 0,
|
||||||
|
save_time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
saveMusicPlayRecord(record.platform, record.id, dbRecord).catch(err => {
|
||||||
|
console.error('保存播放记录到数据库失败:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
@@ -607,6 +670,24 @@ export default function MusicPage() {
|
|||||||
...updated[playlistIndex],
|
...updated[playlistIndex],
|
||||||
duration: audio.duration,
|
duration: audio.duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 保存到数据库(包含时长信息)
|
||||||
|
const record = updated[playlistIndex];
|
||||||
|
const dbRecord: MusicPlayRecord = {
|
||||||
|
platform: record.platform,
|
||||||
|
id: record.id,
|
||||||
|
name: currentSong.name,
|
||||||
|
artist: currentSong.artist,
|
||||||
|
album: currentSong.album,
|
||||||
|
pic: currentSong.pic,
|
||||||
|
play_time: record.playTime,
|
||||||
|
duration: audio.duration,
|
||||||
|
save_time: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
saveMusicPlayRecord(record.platform, record.id, dbRecord).catch(err => {
|
||||||
|
console.error('保存播放记录到数据库失败:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
// 音乐歌曲信息缓存模块 - 基于 platform+id 的全局缓存
|
||||||
|
|
||||||
|
// 歌曲信息接口
|
||||||
|
export interface SongInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
artist: string;
|
||||||
|
album?: string;
|
||||||
|
pic?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存条目接口
|
||||||
|
export interface SongCacheEntry {
|
||||||
|
expiresAt: number;
|
||||||
|
data: SongInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
const SONG_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24小时
|
||||||
|
const CACHE_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1小时清理一次
|
||||||
|
const MAX_CACHE_SIZE = 5000; // 最大缓存条目数量
|
||||||
|
const SONG_CACHE: Map<string, SongCacheEntry> = new Map();
|
||||||
|
|
||||||
|
// 惰性清理时间戳
|
||||||
|
let lastCleanupTime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成歌曲缓存键:platform+id
|
||||||
|
*/
|
||||||
|
function makeSongCacheKey(platform: string, id: string): string {
|
||||||
|
return `${platform}+${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的歌曲信息
|
||||||
|
*/
|
||||||
|
export function getCachedSong(platform: string, id: string): SongInfo | null {
|
||||||
|
const key = makeSongCacheKey(platform, id);
|
||||||
|
const entry = SONG_CACHE.get(key);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (entry.expiresAt <= Date.now()) {
|
||||||
|
SONG_CACHE.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存的歌曲信息
|
||||||
|
*/
|
||||||
|
export function setCachedSong(platform: string, id: string, songInfo: SongInfo): void {
|
||||||
|
// 惰性清理:每次写入时检查是否需要清理
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCleanupTime > CACHE_CLEANUP_INTERVAL_MS) {
|
||||||
|
performCacheCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = makeSongCacheKey(platform, id);
|
||||||
|
SONG_CACHE.set(key, {
|
||||||
|
expiresAt: now + SONG_CACHE_TTL_MS,
|
||||||
|
data: songInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取缓存的歌曲信息
|
||||||
|
*/
|
||||||
|
export function getCachedSongs(keys: Array<{ platform: string; id: string }>): Map<string, SongInfo> {
|
||||||
|
const result = new Map<string, SongInfo>();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const { platform, id } of keys) {
|
||||||
|
const key = makeSongCacheKey(platform, id);
|
||||||
|
const entry = SONG_CACHE.get(key);
|
||||||
|
|
||||||
|
if (entry && entry.expiresAt > now) {
|
||||||
|
result.set(key, entry.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置缓存的歌曲信息
|
||||||
|
*/
|
||||||
|
export function setCachedSongs(songs: Array<{ platform: string; id: string; songInfo: SongInfo }>): void {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 惰性清理
|
||||||
|
if (now - lastCleanupTime > CACHE_CLEANUP_INTERVAL_MS) {
|
||||||
|
performCacheCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { platform, id, songInfo } of songs) {
|
||||||
|
const key = makeSongCacheKey(platform, id);
|
||||||
|
SONG_CACHE.set(key, {
|
||||||
|
expiresAt: now + SONG_CACHE_TTL_MS,
|
||||||
|
data: songInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能清理过期的缓存条目
|
||||||
|
*/
|
||||||
|
function performCacheCleanup(): { expired: number; total: number; sizeLimited: number } {
|
||||||
|
const now = Date.now();
|
||||||
|
const keysToDelete: string[] = [];
|
||||||
|
let sizeLimitedDeleted = 0;
|
||||||
|
|
||||||
|
// 1. 清理过期条目
|
||||||
|
SONG_CACHE.forEach((entry, key) => {
|
||||||
|
if (entry.expiresAt <= now) {
|
||||||
|
keysToDelete.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiredCount = keysToDelete.length;
|
||||||
|
keysToDelete.forEach(key => SONG_CACHE.delete(key));
|
||||||
|
|
||||||
|
// 2. 如果缓存大小超限,清理最老的条目(LRU策略)
|
||||||
|
if (SONG_CACHE.size > MAX_CACHE_SIZE) {
|
||||||
|
const entries = Array.from(SONG_CACHE.entries());
|
||||||
|
// 按照过期时间排序,最早过期的在前面
|
||||||
|
entries.sort((a, b) => a[1].expiresAt - b[1].expiresAt);
|
||||||
|
|
||||||
|
const toRemove = SONG_CACHE.size - MAX_CACHE_SIZE;
|
||||||
|
for (let i = 0; i < toRemove; i++) {
|
||||||
|
SONG_CACHE.delete(entries[i][0]);
|
||||||
|
sizeLimitedDeleted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCleanupTime = now;
|
||||||
|
|
||||||
|
return {
|
||||||
|
expired: expiredCount,
|
||||||
|
total: SONG_CACHE.size,
|
||||||
|
sizeLimited: sizeLimitedDeleted
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有歌曲缓存
|
||||||
|
*/
|
||||||
|
export function clearSongCache(): { cleared: number } {
|
||||||
|
const size = SONG_CACHE.size;
|
||||||
|
SONG_CACHE.clear();
|
||||||
|
return { cleared: size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存统计信息
|
||||||
|
*/
|
||||||
|
export function getSongCacheStats(): {
|
||||||
|
size: number;
|
||||||
|
maxSize: number;
|
||||||
|
ttlMs: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
size: SONG_CACHE.size,
|
||||||
|
maxSize: MAX_CACHE_SIZE,
|
||||||
|
ttlMs: SONG_CACHE_TTL_MS,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user