diff --git a/src/app/api/music/playrecords/route.ts b/src/app/api/music/playrecords/route.ts index 3e7b8a5..8ecc0bc 100644 --- a/src/app/api/music/playrecords/route.ts +++ b/src/app/api/music/playrecords/route.ts @@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; import { db } from '@/lib/db'; import { MusicPlayRecord } from '@/lib/db.client'; +import { getCachedSongs, setCachedSong } from '@/lib/music-song-cache'; export const runtime = 'nodejs'; @@ -29,7 +30,28 @@ export async function GET(request: NextRequest) { } 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 = {}; + 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) { console.error('获取音乐播放记录失败', err); return NextResponse.json( @@ -86,6 +108,16 @@ export async function POST(request: NextRequest) { } 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 }); } catch (err) { console.error('保存音乐播放记录失败', err); diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx index dfe9833..d22ae3e 100644 --- a/src/app/music/page.tsx +++ b/src/app/music/page.tsx @@ -3,6 +3,11 @@ import { useRouter } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; +import { + getAllMusicPlayRecords, + saveMusicPlayRecord, + MusicPlayRecord, +} from '@/lib/db.client'; interface Song { id: string; @@ -145,9 +150,49 @@ export default function MusicPage() { } }; - // 页面加载时恢复播放状态 + // 页面加载时恢复播放状态和数据库记录 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], 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; }); @@ -607,6 +670,24 @@ export default function MusicPage() { ...updated[playlistIndex], 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; }); diff --git a/src/lib/music-song-cache.ts b/src/lib/music-song-cache.ts new file mode 100644 index 0000000..b303db5 --- /dev/null +++ b/src/lib/music-song-cache.ts @@ -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 = 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 { + const result = new Map(); + 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, + }; +}