搜索增加部分缓存快速启动机制

This commit is contained in:
mtvpls
2026-05-16 09:56:40 +08:00
parent 09fc63c116
commit 4f0a0db0fd
3 changed files with 137 additions and 88 deletions
+88 -82
View File
@@ -109,6 +109,13 @@ interface PlayFallbackRecommendation {
doubanId?: number; doubanId?: number;
} }
interface SearchCachePayload {
status: 'complete' | 'partial';
results: SearchResult[];
query: string;
updatedAt: number;
}
function PlayPageClient() { function PlayPageClient() {
const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080'; const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080';
const router = useRouter(); const router = useRouter();
@@ -3764,53 +3771,88 @@ function PlayPageClient() {
.slice(0, 12); .slice(0, 12);
}; };
const readSearchCache = (query: string): SearchCachePayload | null => {
if (typeof window === 'undefined' || !query.trim()) {
return null;
}
try {
const cacheKey = `search_cache_${query.trim()}`;
const cached = sessionStorage.getItem(cacheKey);
if (!cached) return null;
const parsed = JSON.parse(cached) as SearchCachePayload;
if (
(parsed?.status === 'complete' || parsed?.status === 'partial') &&
Array.isArray(parsed.results)
) {
return parsed;
}
} catch (error) {
console.error('[Play] 读取缓存失败:', error);
}
return null;
};
const writeCompleteSearchCache = (query: string, results: SearchResult[]) => {
if (typeof window === 'undefined' || !query.trim()) return;
try {
const cacheKey = `search_cache_${query.trim()}`;
const payload: SearchCachePayload = {
status: 'complete',
results,
query: query.trim(),
updatedAt: Date.now(),
};
sessionStorage.setItem(cacheKey, JSON.stringify(payload));
} catch (error) {
console.error('[Play] 写入缓存失败:', error);
}
};
const filterSourcesForCurrentVideo = (items: SearchResult[]): SearchResult[] => {
return items.filter(
(result: SearchResult) =>
normalizeTitle(result.title).toLowerCase() ===
normalizeTitle(videoTitleRef.current).toLowerCase() &&
(videoYearRef.current
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
!result.year ||
result.year.trim() === '' ||
result.year === 'unknown' ||
!/^\d{4}$/.test(result.year)
: true) &&
(searchType
? getType(result) === searchType
: true)
);
};
const fetchSourcesData = async (query: string): Promise<SearchResult[]> => { const fetchSourcesData = async (query: string): Promise<SearchResult[]> => {
// 根据搜索词获取全部源信息 // 根据搜索词获取全部源信息
setHasCompletedSearchRequest(false); setHasCompletedSearchRequest(false);
setFallbackRecommendations([]); setFallbackRecommendations([]);
let fallbackCachedResults: SearchResult[] = [];
try { try {
// 先检查 sessionStorage 中是否有缓存 const cachedPayload = readSearchCache(query);
const cacheKey = `search_cache_${query.trim()}`; if (cachedPayload) {
let results: SearchResult[] = []; console.log(`[Play] 使用 sessionStorage ${cachedPayload.status === 'partial' ? '临时' : '完整'}缓存的搜索结果`);
setFallbackRecommendations(buildFallbackRecommendations(cachedPayload.results, query));
if (typeof window !== 'undefined') { const cachedResults = filterSourcesForCurrentVideo(cachedPayload.results);
try { fallbackCachedResults = cachedResults;
const cached = sessionStorage.getItem(cacheKey); setAvailableSources(applyCorrectionsToSources(cachedResults));
if (cached) {
console.log('[Play] 使用 sessionStorage 缓存的搜索结果');
const cachedData = JSON.parse(cached) as SearchResult[];
setHasCompletedSearchRequest(true); if (cachedPayload.status === 'complete') {
setFallbackRecommendations(buildFallbackRecommendations(cachedData, query)); setHasCompletedSearchRequest(true);
return cachedResults;
// 处理缓存的搜索结果,根据规则过滤
results = cachedData.filter(
(result: SearchResult) =>
normalizeTitle(result.title).toLowerCase() ===
normalizeTitle(videoTitleRef.current).toLowerCase() &&
(videoYearRef.current
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
!result.year ||
result.year.trim() === '' ||
result.year === 'unknown' ||
!/^\d{4}$/.test(result.year)
: true) &&
(searchType
? getType(result) === searchType
: true)
);
setAvailableSources(applyCorrectionsToSources(results));
return results;
}
} catch (error) {
console.error('[Play] 读取缓存失败:', error);
// 继续执行 API 调用
} }
} }
// 如果没有缓存,调用 API // 没有缓存或只有 partial 缓存时,重新请求完整搜索结果
const response = await fetch( const response = await fetch(
`/api/search?q=${encodeURIComponent(query.trim())}` `/api/search?q=${encodeURIComponent(query.trim())}`
); );
@@ -3820,29 +3862,18 @@ function PlayPageClient() {
const data = await response.json(); const data = await response.json();
const allResults = (data.results || []) as SearchResult[]; const allResults = (data.results || []) as SearchResult[];
writeCompleteSearchCache(query, allResults);
setHasCompletedSearchRequest(true); setHasCompletedSearchRequest(true);
setFallbackRecommendations(buildFallbackRecommendations(allResults, query)); setFallbackRecommendations(buildFallbackRecommendations(allResults, query));
// 处理搜索结果,根据规则过滤 const results = filterSourcesForCurrentVideo(allResults);
results = allResults.filter(
(result: SearchResult) =>
normalizeTitle(result.title).toLowerCase() ===
normalizeTitle(videoTitleRef.current).toLowerCase() &&
(videoYearRef.current
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
!result.year ||
result.year.trim() === '' ||
result.year === 'unknown' ||
!/^\d{4}$/.test(result.year)
: true) &&
(searchType
? getType(result) === searchType
: true)
);
setAvailableSources(applyCorrectionsToSources(results)); setAvailableSources(applyCorrectionsToSources(results));
return results; return results;
} catch (err) { } catch (err) {
setSourceSearchError(err instanceof Error ? err.message : '搜索失败'); setSourceSearchError(err instanceof Error ? err.message : '搜索失败');
if (fallbackCachedResults.length > 0) {
return fallbackCachedResults;
}
setAvailableSources([]); setAvailableSources([]);
return []; return [];
} finally { } finally {
@@ -3851,39 +3882,14 @@ function PlayPageClient() {
}; };
const getCachedSourcesData = (query: string): SearchResult[] => { const getCachedSourcesData = (query: string): SearchResult[] => {
if (typeof window === 'undefined' || !query.trim()) { const cachedPayload = readSearchCache(query);
if (!cachedPayload) {
return []; return [];
} }
try { return applyCorrectionsToSources(
const cacheKey = `search_cache_${query.trim()}`; filterSourcesForCurrentVideo(cachedPayload.results)
const cached = sessionStorage.getItem(cacheKey); );
if (!cached) {
return [];
}
const cachedData = JSON.parse(cached);
const results = cachedData.filter(
(result: SearchResult) =>
normalizeTitle(result.title).toLowerCase() ===
normalizeTitle(videoTitleRef.current).toLowerCase() &&
(videoYearRef.current
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
!result.year ||
result.year.trim() === '' ||
result.year === 'unknown' ||
!/^\d{4}$/.test(result.year)
: true) &&
(searchType
? getType(result) === searchType
: true)
);
return applyCorrectionsToSources(results);
} catch (error) {
console.error('[Play] 读取缓存失败:', error);
return [];
}
}; };
const initAll = async () => { const initAll = async () => {
+41 -6
View File
@@ -46,6 +46,13 @@ import SearchSuggestions from '@/components/SearchSuggestions';
import VideoCard, { VideoCardHandle } from '@/components/VideoCard'; import VideoCard, { VideoCardHandle } from '@/components/VideoCard';
import VirtualScrollableGrid from '@/components/VirtualScrollableGrid'; import VirtualScrollableGrid from '@/components/VirtualScrollableGrid';
type SearchCachePayload = {
status: 'complete' | 'partial';
results: SearchResult[];
query: string;
updatedAt: number;
};
function SearchPageClient() { function SearchPageClient() {
// 搜索历史 // 搜索历史
const [searchHistory, setSearchHistory] = useState<string[]>([]); const [searchHistory, setSearchHistory] = useState<string[]>([]);
@@ -107,14 +114,17 @@ function SearchPageClient() {
return `search_cache_${query.trim()}`; return `search_cache_${query.trim()}`;
}; };
// 从 sessionStorage 获取缓存的搜索结果 // 从 sessionStorage 获取完整缓存的搜索结果(partial 只给播放页快速启动使用)
const getCachedResults = (query: string): SearchResult[] | null => { const getCachedResults = (query: string): SearchResult[] | null => {
if (typeof window === 'undefined') return null; if (typeof window === 'undefined') return null;
try { try {
const cacheKey = getCacheKey(query); const cacheKey = getCacheKey(query);
const cached = sessionStorage.getItem(cacheKey); const cached = sessionStorage.getItem(cacheKey);
if (cached) { if (!cached) return null;
return JSON.parse(cached);
const parsed = JSON.parse(cached) as SearchCachePayload;
if (parsed?.status === 'complete' && Array.isArray(parsed.results)) {
return parsed.results;
} }
} catch (error) { } catch (error) {
console.error('Failed to get cached results:', error); console.error('Failed to get cached results:', error);
@@ -123,16 +133,36 @@ function SearchPageClient() {
}; };
// 保存搜索结果到 sessionStorage // 保存搜索结果到 sessionStorage
const setCachedResults = (query: string, results: SearchResult[]) => { const setCachedResults = (
query: string,
results: SearchResult[],
status: SearchCachePayload['status'] = 'complete'
) => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
try { try {
const cacheKey = getCacheKey(query); const cacheKey = getCacheKey(query);
sessionStorage.setItem(cacheKey, JSON.stringify(results)); const payload: SearchCachePayload = {
status,
results,
query: query.trim(),
updatedAt: Date.now(),
};
sessionStorage.setItem(cacheKey, JSON.stringify(payload));
} catch (error) { } catch (error) {
console.error('Failed to cache results:', error); console.error('Failed to cache results:', error);
} }
}; };
const savePartialCacheForPlayback = () => {
const query = currentQueryRef.current.trim();
if (!query || !eventSourceRef.current || !isLoading) return;
const snapshot = searchResults.concat(pendingResultsRef.current);
if (snapshot.length > 20) {
setCachedResults(query, snapshot, 'partial');
}
};
// 清除指定查询的缓存 // 清除指定查询的缓存
const clearCachedResults = (query: string) => { const clearCachedResults = (query: string) => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -847,7 +877,10 @@ function SearchPageClient() {
<button <button
key={item.key} key={item.key}
type='button' type='button'
onClick={() => router.push(itemUrl)} onClick={() => {
savePartialCacheForPlayback();
router.push(itemUrl);
}}
className='group w-full rounded-2xl border border-gray-200/80 bg-white/90 p-3 text-left shadow-sm transition-all hover:border-green-300 hover:shadow-md dark:border-gray-700 dark:bg-gray-900/70 dark:hover:border-green-700' className='group w-full rounded-2xl border border-gray-200/80 bg-white/90 p-3 text-left shadow-sm transition-all hover:border-green-300 hover:shadow-md dark:border-gray-700 dark:bg-gray-900/70 dark:hover:border-green-700'
> >
<div className='flex items-start gap-4'> <div className='flex items-start gap-4'>
@@ -1818,6 +1851,7 @@ function SearchPageClient() {
<VideoCard <VideoCard
ref={getGroupRef(mapKey)} ref={getGroupRef(mapKey)}
from='search' from='search'
onBeforeNavigate={savePartialCacheForPlayback}
isAggregate={true} isAggregate={true}
title={title} title={title}
poster={poster} poster={poster}
@@ -1867,6 +1901,7 @@ function SearchPageClient() {
> >
<VideoCard <VideoCard
id={item.id} id={item.id}
onBeforeNavigate={savePartialCacheForPlayback}
title={item.title} title={item.title}
poster={item.poster} poster={item.poster}
episodes={item.episodes.length} episodes={item.episodes.length}
+8
View File
@@ -69,6 +69,7 @@ export interface VideoCardProps {
episodes?: string[]; episodes?: string[];
episodes_titles?: string[]; episodes_titles?: string[];
}; };
onBeforeNavigate?: () => void;
} }
export type VideoCardHandle = { export type VideoCardHandle = {
@@ -107,6 +108,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
playTime, playTime,
totalTime, totalTime,
cmsData, cmsData,
onBeforeNavigate,
}: VideoCardProps, }: VideoCardProps,
ref ref
) { ) {
@@ -326,6 +328,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
return; return;
} }
onBeforeNavigate?.();
if (origin === 'live' && actualSource && actualId) { if (origin === 'live' && actualSource && actualId) {
// 直播内容跳转到直播页面 // 直播内容跳转到直播页面
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`; const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
@@ -376,6 +380,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
isAggregate, isAggregate,
actualQuery, actualQuery,
actualSearchType, actualSearchType,
onBeforeNavigate,
]); ]);
// 新标签页播放处理函数 // 新标签页播放处理函数
@@ -385,6 +390,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
return; return;
} }
onBeforeNavigate?.();
if (origin === 'live' && actualSource && actualId) { if (origin === 'live' && actualSource && actualId) {
// 直播内容跳转到直播页面 // 直播内容跳转到直播页面
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`; const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
@@ -411,6 +418,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
isAggregate, isAggregate,
actualQuery, actualQuery,
actualSearchType, actualSearchType,
onBeforeNavigate,
]); ]);
// 检查搜索结果的收藏状态 // 检查搜索结果的收藏状态