play页面缓存引入主动清理机制
This commit is contained in:
+2
-2
@@ -8,7 +8,7 @@ import './globals.css';
|
|||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { listEnabledSourceScripts } from '@/lib/source-script';
|
import { listEnabledSourceScripts } from '@/lib/source-script';
|
||||||
|
|
||||||
import { DanmakuCacheCleanup } from '../components/DanmakuCacheCleanup';
|
import { StartupCacheCleanup } from '../components/DanmakuCacheCleanup';
|
||||||
import { DownloadBubble } from '../components/DownloadBubble';
|
import { DownloadBubble } from '../components/DownloadBubble';
|
||||||
import { DownloadPanel } from '../components/DownloadPanel';
|
import { DownloadPanel } from '../components/DownloadPanel';
|
||||||
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
||||||
@@ -259,7 +259,7 @@ export default async function RootLayout({
|
|||||||
<SiteProvider siteName={siteName} announcement={announcement} tmdbApiKey={tmdbApiKey}>
|
<SiteProvider siteName={siteName} announcement={announcement} tmdbApiKey={tmdbApiKey}>
|
||||||
<WatchRoomProvider>
|
<WatchRoomProvider>
|
||||||
<DownloadProvider>
|
<DownloadProvider>
|
||||||
<DanmakuCacheCleanup />
|
<StartupCacheCleanup />
|
||||||
{children}
|
{children}
|
||||||
<GlobalErrorIndicator />
|
<GlobalErrorIndicator />
|
||||||
<ChatFloatingWindow />
|
<ChatFloatingWindow />
|
||||||
|
|||||||
+25
-58
@@ -47,6 +47,11 @@ import {
|
|||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
import { getDoubanDetail } from '@/lib/douban.client';
|
import { getDoubanDetail } from '@/lib/douban.client';
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||||
|
import {
|
||||||
|
getRecommendationCache,
|
||||||
|
recommendationCacheKeys,
|
||||||
|
setRecommendationCache,
|
||||||
|
} from '@/lib/recommendations/cache';
|
||||||
import { DanmakuFilterConfig, EpisodeFilterConfig, SearchResult } from '@/lib/types';
|
import { DanmakuFilterConfig, EpisodeFilterConfig, SearchResult } from '@/lib/types';
|
||||||
import { base58Decode, getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
import { base58Decode, getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||||
import { useEnableAIComments } from '@/hooks/useEnableAIComments';
|
import { useEnableAIComments } from '@/hooks/useEnableAIComments';
|
||||||
@@ -1175,52 +1180,27 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查title到tmdbId的映射缓存(1个月)
|
const mappingCacheKey = recommendationCacheKeys.tmdbTitleMapping(videoTitle);
|
||||||
const mappingCacheKey = `tmdb_title_mapping_${videoTitle}`;
|
const cachedId = getRecommendationCache<string>(mappingCacheKey);
|
||||||
const mappingCache = localStorage.getItem(mappingCacheKey);
|
|
||||||
let cachedId: string | null = null;
|
|
||||||
|
|
||||||
if (mappingCache) {
|
if (cachedId) {
|
||||||
try {
|
console.log('使用缓存的TMDB ID映射');
|
||||||
const { tmdbId, timestamp } = JSON.parse(mappingCache);
|
|
||||||
const cacheAge = Date.now() - timestamp;
|
|
||||||
const cacheMaxAge = 30 * 24 * 60 * 60 * 1000; // 1个月
|
|
||||||
|
|
||||||
if (cacheAge < cacheMaxAge && tmdbId) {
|
const detailsCacheKey = recommendationCacheKeys.tmdbDetails(cachedId);
|
||||||
console.log('使用缓存的TMDB ID映射');
|
const detailsCache = getRecommendationCache<any>(detailsCacheKey);
|
||||||
cachedId = tmdbId;
|
|
||||||
|
|
||||||
// 检查TMDB详情缓存(1天)
|
if (detailsCache) {
|
||||||
const detailsCacheKey = `tmdb_details_${tmdbId}`;
|
if (detailsCache.backdrop) {
|
||||||
const detailsCache = localStorage.getItem(detailsCacheKey);
|
setTmdbBackdrop(processImageUrl(detailsCache.backdrop));
|
||||||
|
} else {
|
||||||
if (detailsCache) {
|
setTmdbBackdrop(null);
|
||||||
try {
|
|
||||||
const { data, timestamp: detTimestamp } = JSON.parse(detailsCache);
|
|
||||||
const detCacheAge = Date.now() - detTimestamp;
|
|
||||||
const detCacheMaxAge = 24 * 60 * 60 * 1000; // 1天
|
|
||||||
|
|
||||||
if (detCacheAge < detCacheMaxAge && data) {
|
|
||||||
if (data.backdrop) {
|
|
||||||
setTmdbBackdrop(processImageUrl(data.backdrop));
|
|
||||||
} else {
|
|
||||||
setTmdbBackdrop(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有豆瓣ID,使用TMDb数据补充
|
|
||||||
if (!videoDoubanId || videoDoubanId === 0) {
|
|
||||||
populateDoubanFieldsFromTMDB(data);
|
|
||||||
}
|
|
||||||
populatePlayMetadataFromTMDB(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析详情缓存失败:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('解析映射缓存失败:', e);
|
if (!videoDoubanId || videoDoubanId === 0) {
|
||||||
|
populateDoubanFieldsFromTMDB(detailsCache);
|
||||||
|
}
|
||||||
|
populatePlayMetadataFromTMDB(detailsCache);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1253,23 +1233,10 @@ function PlayPageClient() {
|
|||||||
// 保存title到tmdbId的映射到localStorage(1个月)
|
// 保存title到tmdbId的映射到localStorage(1个月)
|
||||||
if (result.tmdbId) {
|
if (result.tmdbId) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
setRecommendationCache(mappingCacheKey, String(result.tmdbId));
|
||||||
mappingCacheKey,
|
|
||||||
JSON.stringify({
|
|
||||||
tmdbId: result.tmdbId,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// 保存TMDB详情数据到localStorage(1天)
|
const detailsCacheKey = recommendationCacheKeys.tmdbDetails(result.tmdbId);
|
||||||
const detailsCacheKey = `tmdb_details_${result.tmdbId}`;
|
setRecommendationCache(detailsCacheKey, result);
|
||||||
localStorage.setItem(
|
|
||||||
detailsCacheKey,
|
|
||||||
JSON.stringify({
|
|
||||||
data: result,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存缓存失败:', e);
|
console.error('保存缓存失败:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,16 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { initDanmakuModule } from '@/lib/danmaku/api';
|
import { initStartupCacheCleanup } from '@/lib/startup/cacheCleanup';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 弹幕缓存清理组件
|
* 启动缓存清理组件
|
||||||
* 在应用启动时执行一次过期缓存清理
|
* 在应用启动时异步执行一次缓存清理
|
||||||
*/
|
*/
|
||||||
export function DanmakuCacheCleanup() {
|
export function StartupCacheCleanup() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只在客户端执行一次
|
initStartupCacheCleanup();
|
||||||
initDanmakuModule();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 这个组件不渲染任何内容
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import { useEnableComments } from '@/hooks/useEnableComments';
|
|||||||
import ScrollableRow from '@/components/ScrollableRow';
|
import ScrollableRow from '@/components/ScrollableRow';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRecommendationCache,
|
||||||
|
recommendationCacheKeys,
|
||||||
|
setRecommendationCache,
|
||||||
|
} from '@/lib/recommendations/cache';
|
||||||
|
|
||||||
interface DoubanRecommendation {
|
interface DoubanRecommendation {
|
||||||
doubanId: string;
|
doubanId: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -31,25 +37,14 @@ export default function DoubanRecommendations({ doubanId }: DoubanRecommendation
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// 检查localStorage缓存
|
const cacheKey = recommendationCacheKeys.doubanRecommendations(doubanId);
|
||||||
const cacheKey = `douban_recommendations_${doubanId}`;
|
const cached = getRecommendationCache<DoubanRecommendation[]>(cacheKey);
|
||||||
const cached = localStorage.getItem(cacheKey);
|
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
try {
|
console.log('使用缓存的推荐数据');
|
||||||
const { data, timestamp } = JSON.parse(cached);
|
setRecommendations(cached);
|
||||||
const cacheAge = Date.now() - timestamp;
|
setLoading(false);
|
||||||
const cacheMaxAge = 7 * 24 * 60 * 60 * 1000; // 7天
|
return;
|
||||||
|
|
||||||
if (cacheAge < cacheMaxAge) {
|
|
||||||
console.log('使用缓存的推荐数据');
|
|
||||||
setRecommendations(data);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析缓存失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -66,15 +61,7 @@ export default function DoubanRecommendations({ doubanId }: DoubanRecommendation
|
|||||||
const recommendationsData = result.recommendations || [];
|
const recommendationsData = result.recommendations || [];
|
||||||
setRecommendations(recommendationsData);
|
setRecommendations(recommendationsData);
|
||||||
|
|
||||||
// 保存到localStorage
|
setRecommendationCache(cacheKey, recommendationsData);
|
||||||
try {
|
|
||||||
localStorage.setItem(cacheKey, JSON.stringify({
|
|
||||||
data: recommendationsData,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('保存缓存失败:', e);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取推荐失败:', err);
|
console.error('获取推荐失败:', err);
|
||||||
setError(err instanceof Error ? err.message : '获取推荐失败');
|
setError(err instanceof Error ? err.message : '获取推荐失败');
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import { useRecommendationDataSource } from '@/hooks/useRecommendationDataSource
|
|||||||
import ScrollableRow from '@/components/ScrollableRow';
|
import ScrollableRow from '@/components/ScrollableRow';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRecommendationCache,
|
||||||
|
recommendationCacheKeys,
|
||||||
|
setRecommendationCache,
|
||||||
|
} from '@/lib/recommendations/cache';
|
||||||
|
|
||||||
interface Recommendation {
|
interface Recommendation {
|
||||||
doubanId?: string;
|
doubanId?: string;
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
@@ -63,25 +69,14 @@ export default function SmartRecommendations({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// 检查localStorage缓存
|
const cacheKey = recommendationCacheKeys.doubanRecommendations(doubanId);
|
||||||
const cacheKey = `douban_recommendations_${doubanId}`;
|
const cached = getRecommendationCache<Recommendation[]>(cacheKey);
|
||||||
const cached = localStorage.getItem(cacheKey);
|
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
try {
|
console.log('使用缓存的豆瓣推荐数据');
|
||||||
const { data, timestamp } = JSON.parse(cached);
|
setRecommendations(cached);
|
||||||
const cacheAge = Date.now() - timestamp;
|
setLoading(false);
|
||||||
const cacheMaxAge = 7 * 24 * 60 * 60 * 1000; // 7天
|
return;
|
||||||
|
|
||||||
if (cacheAge < cacheMaxAge) {
|
|
||||||
console.log('使用缓存的豆瓣推荐数据');
|
|
||||||
setRecommendations(data);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析缓存失败:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/douban-recommendations?id=${doubanId}`);
|
const response = await fetch(`/api/douban-recommendations?id=${doubanId}`);
|
||||||
@@ -94,18 +89,7 @@ export default function SmartRecommendations({
|
|||||||
const recommendationsData = result.recommendations || [];
|
const recommendationsData = result.recommendations || [];
|
||||||
setRecommendations(recommendationsData);
|
setRecommendations(recommendationsData);
|
||||||
|
|
||||||
// 保存到localStorage
|
setRecommendationCache(cacheKey, recommendationsData);
|
||||||
try {
|
|
||||||
localStorage.setItem(
|
|
||||||
cacheKey,
|
|
||||||
JSON.stringify({
|
|
||||||
data: recommendationsData,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('保存缓存失败:', e);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取豆瓣推荐失败:', err);
|
console.error('获取豆瓣推荐失败:', err);
|
||||||
setError(err instanceof Error ? err.message : '获取推荐失败');
|
setError(err instanceof Error ? err.message : '获取推荐失败');
|
||||||
@@ -122,44 +106,20 @@ export default function SmartRecommendations({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// 检查title到tmdbId的映射缓存(1个月)
|
const mappingCacheKey = recommendationCacheKeys.tmdbTitleMapping(videoTitle);
|
||||||
const mappingCacheKey = `tmdb_title_mapping_${videoTitle}`;
|
const cachedId = getRecommendationCache<string>(mappingCacheKey);
|
||||||
const mappingCache = localStorage.getItem(mappingCacheKey);
|
|
||||||
let cachedId: string | null = null;
|
|
||||||
|
|
||||||
if (mappingCache) {
|
if (cachedId) {
|
||||||
try {
|
console.log('使用缓存的TMDB ID映射');
|
||||||
const { tmdbId, timestamp } = JSON.parse(mappingCache);
|
|
||||||
const cacheAge = Date.now() - timestamp;
|
|
||||||
const cacheMaxAge = 30 * 24 * 60 * 60 * 1000; // 1个月
|
|
||||||
|
|
||||||
if (cacheAge < cacheMaxAge && tmdbId) {
|
const recommendationsCacheKey = recommendationCacheKeys.tmdbRecommendations(cachedId);
|
||||||
console.log('使用缓存的TMDB ID映射');
|
const recommendationsCache = getRecommendationCache<Recommendation[]>(recommendationsCacheKey);
|
||||||
cachedId = tmdbId;
|
|
||||||
|
|
||||||
// 检查TMDB推荐数据缓存(1天)
|
if (recommendationsCache) {
|
||||||
const recommendationsCacheKey = `tmdb_recommendations_${tmdbId}`;
|
console.log('使用缓存的TMDB推荐数据');
|
||||||
const recommendationsCache = localStorage.getItem(recommendationsCacheKey);
|
setRecommendations(recommendationsCache);
|
||||||
|
setLoading(false);
|
||||||
if (recommendationsCache) {
|
return;
|
||||||
try {
|
|
||||||
const { data, timestamp: recTimestamp } = JSON.parse(recommendationsCache);
|
|
||||||
const recCacheAge = Date.now() - recTimestamp;
|
|
||||||
const recCacheMaxAge = 24 * 60 * 60 * 1000; // 1天
|
|
||||||
|
|
||||||
if (recCacheAge < recCacheMaxAge && data) {
|
|
||||||
console.log('使用缓存的TMDB推荐数据');
|
|
||||||
setRecommendations(data);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析推荐缓存失败:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析映射缓存失败:', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,23 +141,10 @@ export default function SmartRecommendations({
|
|||||||
// 保存title到tmdbId的映射到localStorage(1个月)
|
// 保存title到tmdbId的映射到localStorage(1个月)
|
||||||
if (result.tmdbId) {
|
if (result.tmdbId) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
setRecommendationCache(mappingCacheKey, String(result.tmdbId));
|
||||||
mappingCacheKey,
|
|
||||||
JSON.stringify({
|
|
||||||
tmdbId: result.tmdbId,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// 保存TMDB推荐数据到localStorage(1天)
|
const recommendationsCacheKey = recommendationCacheKeys.tmdbRecommendations(result.tmdbId);
|
||||||
const recommendationsCacheKey = `tmdb_recommendations_${result.tmdbId}`;
|
setRecommendationCache(recommendationsCacheKey, recommendationsData);
|
||||||
localStorage.setItem(
|
|
||||||
recommendationsCacheKey,
|
|
||||||
JSON.stringify({
|
|
||||||
data: recommendationsData,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存缓存失败:', e);
|
console.error('保存缓存失败:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const RECOMMENDATION_CACHE_CONFIG = {
|
||||||
|
doubanRecommendations: {
|
||||||
|
prefix: 'douban_recommendations_',
|
||||||
|
ttlMs: 7 * DAY_IN_MS,
|
||||||
|
},
|
||||||
|
tmdbTitleMapping: {
|
||||||
|
prefix: 'tmdb_title_mapping_',
|
||||||
|
ttlMs: 30 * DAY_IN_MS,
|
||||||
|
},
|
||||||
|
tmdbRecommendations: {
|
||||||
|
prefix: 'tmdb_recommendations_',
|
||||||
|
ttlMs: DAY_IN_MS,
|
||||||
|
},
|
||||||
|
tmdbDetails: {
|
||||||
|
prefix: 'tmdb_details_',
|
||||||
|
ttlMs: DAY_IN_MS,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type RecommendationCacheType = keyof typeof RECOMMENDATION_CACHE_CONFIG;
|
||||||
|
|
||||||
|
interface RecommendationCacheEntry<T> {
|
||||||
|
value: T;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let recommendationCacheCleanupInitialized = false;
|
||||||
|
|
||||||
|
export const recommendationCacheKeys = {
|
||||||
|
doubanRecommendations: (doubanId: string | number) =>
|
||||||
|
`${RECOMMENDATION_CACHE_CONFIG.doubanRecommendations.prefix}${doubanId}`,
|
||||||
|
tmdbTitleMapping: (title: string) =>
|
||||||
|
`${RECOMMENDATION_CACHE_CONFIG.tmdbTitleMapping.prefix}${title}`,
|
||||||
|
tmdbRecommendations: (tmdbId: string | number) =>
|
||||||
|
`${RECOMMENDATION_CACHE_CONFIG.tmdbRecommendations.prefix}${tmdbId}`,
|
||||||
|
tmdbDetails: (tmdbId: string | number) =>
|
||||||
|
`${RECOMMENDATION_CACHE_CONFIG.tmdbDetails.prefix}${tmdbId}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
function scheduleCleanup(task: () => void): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(() => task());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(task, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCacheTypeForKey(key: string): RecommendationCacheType | null {
|
||||||
|
for (const [type, config] of Object.entries(RECOMMENDATION_CACHE_CONFIG) as Array<
|
||||||
|
[RecommendationCacheType, (typeof RECOMMENDATION_CACHE_CONFIG)[RecommendationCacheType]]
|
||||||
|
>) {
|
||||||
|
if (key.startsWith(config.prefix)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCacheEntry<T>(rawValue: string | null): RecommendationCacheEntry<T> | null {
|
||||||
|
if (!rawValue) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(rawValue) as {
|
||||||
|
value?: T;
|
||||||
|
data?: T;
|
||||||
|
tmdbId?: T;
|
||||||
|
timestamp?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof parsed.timestamp !== 'number' || Number.isNaN(parsed.timestamp)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('value' in parsed) {
|
||||||
|
return {
|
||||||
|
value: parsed.value as T,
|
||||||
|
timestamp: parsed.timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('data' in parsed) {
|
||||||
|
return {
|
||||||
|
value: parsed.data as T,
|
||||||
|
timestamp: parsed.timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('tmdbId' in parsed) {
|
||||||
|
return {
|
||||||
|
value: parsed.tmdbId as T,
|
||||||
|
timestamp: parsed.timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpired(type: RecommendationCacheType, timestamp: number): boolean {
|
||||||
|
return Date.now() - timestamp >= RECOMMENDATION_CACHE_CONFIG[type].ttlMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecommendationCache<T>(
|
||||||
|
key: string
|
||||||
|
): T | null {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
|
const cacheType = getCacheTypeForKey(key);
|
||||||
|
if (!cacheType) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry = parseCacheEntry<T>(localStorage.getItem(key));
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired(cacheType, entry.timestamp)) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取推荐缓存失败:', error);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRecommendationCache<T>(key: string, value: T): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry: RecommendationCacheEntry<T> = {
|
||||||
|
value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
localStorage.setItem(key, JSON.stringify(entry));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存推荐缓存失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRecommendationCache(key: string): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearExpiredRecommendationCaches(): Promise<number> {
|
||||||
|
if (typeof window === 'undefined') return 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (!key) continue;
|
||||||
|
|
||||||
|
const cacheType = getCacheTypeForKey(key);
|
||||||
|
if (!cacheType) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry = parseCacheEntry<unknown>(localStorage.getItem(key));
|
||||||
|
if (!entry || isExpired(cacheType, entry.timestamp)) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||||
|
return keysToRemove.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理推荐缓存失败:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initRecommendationCacheModule(): void {
|
||||||
|
if (typeof window === 'undefined' || recommendationCacheCleanupInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendationCacheCleanupInitialized = true;
|
||||||
|
|
||||||
|
scheduleCleanup(() => {
|
||||||
|
void clearExpiredRecommendationCaches()
|
||||||
|
.then((count) => {
|
||||||
|
if (count > 0) {
|
||||||
|
console.log(`[推荐缓存] 启动清理: 已删除 ${count} 个过期缓存`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[推荐缓存] 清理失败:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { initDanmakuModule } from '@/lib/danmaku/api';
|
||||||
|
import { initRecommendationCacheModule } from '@/lib/recommendations/cache';
|
||||||
|
|
||||||
|
let startupCacheCleanupInitialized = false;
|
||||||
|
|
||||||
|
export function initStartupCacheCleanup(): void {
|
||||||
|
if (typeof window === 'undefined' || startupCacheCleanupInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startupCacheCleanupInitialized = true;
|
||||||
|
|
||||||
|
initDanmakuModule();
|
||||||
|
initRecommendationCacheModule();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user