diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index b57d3d3..608f209 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -109,6 +109,13 @@ interface PlayFallbackRecommendation { doubanId?: number; } +interface SearchCachePayload { + status: 'complete' | 'partial'; + results: SearchResult[]; + query: string; + updatedAt: number; +} + function PlayPageClient() { const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080'; const router = useRouter(); @@ -3764,53 +3771,88 @@ function PlayPageClient() { .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 => { // 根据搜索词获取全部源信息 setHasCompletedSearchRequest(false); setFallbackRecommendations([]); + let fallbackCachedResults: SearchResult[] = []; + try { - // 先检查 sessionStorage 中是否有缓存 - const cacheKey = `search_cache_${query.trim()}`; - let results: SearchResult[] = []; + const cachedPayload = readSearchCache(query); + if (cachedPayload) { + console.log(`[Play] 使用 sessionStorage ${cachedPayload.status === 'partial' ? '临时' : '完整'}缓存的搜索结果`); + setFallbackRecommendations(buildFallbackRecommendations(cachedPayload.results, query)); - if (typeof window !== 'undefined') { - try { - const cached = sessionStorage.getItem(cacheKey); - if (cached) { - console.log('[Play] 使用 sessionStorage 缓存的搜索结果'); - const cachedData = JSON.parse(cached) as SearchResult[]; + const cachedResults = filterSourcesForCurrentVideo(cachedPayload.results); + fallbackCachedResults = cachedResults; + setAvailableSources(applyCorrectionsToSources(cachedResults)); - setHasCompletedSearchRequest(true); - setFallbackRecommendations(buildFallbackRecommendations(cachedData, query)); - - // 处理缓存的搜索结果,根据规则过滤 - 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 调用 + if (cachedPayload.status === 'complete') { + setHasCompletedSearchRequest(true); + return cachedResults; } } - // 如果没有缓存,调用 API + // 没有缓存或只有 partial 缓存时,重新请求完整搜索结果 const response = await fetch( `/api/search?q=${encodeURIComponent(query.trim())}` ); @@ -3820,29 +3862,18 @@ function PlayPageClient() { const data = await response.json(); const allResults = (data.results || []) as SearchResult[]; + writeCompleteSearchCache(query, allResults); setHasCompletedSearchRequest(true); setFallbackRecommendations(buildFallbackRecommendations(allResults, query)); - // 处理搜索结果,根据规则过滤 - 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) - ); + const results = filterSourcesForCurrentVideo(allResults); setAvailableSources(applyCorrectionsToSources(results)); return results; } catch (err) { setSourceSearchError(err instanceof Error ? err.message : '搜索失败'); + if (fallbackCachedResults.length > 0) { + return fallbackCachedResults; + } setAvailableSources([]); return []; } finally { @@ -3851,39 +3882,14 @@ function PlayPageClient() { }; const getCachedSourcesData = (query: string): SearchResult[] => { - if (typeof window === 'undefined' || !query.trim()) { + const cachedPayload = readSearchCache(query); + if (!cachedPayload) { return []; } - try { - const cacheKey = `search_cache_${query.trim()}`; - 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 []; - } + return applyCorrectionsToSources( + filterSourcesForCurrentVideo(cachedPayload.results) + ); }; const initAll = async () => { diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 82f24ba..0d0254b 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -46,6 +46,13 @@ import SearchSuggestions from '@/components/SearchSuggestions'; import VideoCard, { VideoCardHandle } from '@/components/VideoCard'; import VirtualScrollableGrid from '@/components/VirtualScrollableGrid'; +type SearchCachePayload = { + status: 'complete' | 'partial'; + results: SearchResult[]; + query: string; + updatedAt: number; +}; + function SearchPageClient() { // 搜索历史 const [searchHistory, setSearchHistory] = useState([]); @@ -107,14 +114,17 @@ function SearchPageClient() { return `search_cache_${query.trim()}`; }; - // 从 sessionStorage 获取缓存的搜索结果 + // 从 sessionStorage 获取完整缓存的搜索结果(partial 只给播放页快速启动使用) const getCachedResults = (query: string): SearchResult[] | null => { if (typeof window === 'undefined') return null; try { const cacheKey = getCacheKey(query); const cached = sessionStorage.getItem(cacheKey); - if (cached) { - return JSON.parse(cached); + if (!cached) return null; + + const parsed = JSON.parse(cached) as SearchCachePayload; + if (parsed?.status === 'complete' && Array.isArray(parsed.results)) { + return parsed.results; } } catch (error) { console.error('Failed to get cached results:', error); @@ -123,16 +133,36 @@ function SearchPageClient() { }; // 保存搜索结果到 sessionStorage - const setCachedResults = (query: string, results: SearchResult[]) => { + const setCachedResults = ( + query: string, + results: SearchResult[], + status: SearchCachePayload['status'] = 'complete' + ) => { if (typeof window === 'undefined') return; try { 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) { 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) => { if (typeof window === 'undefined') return; @@ -847,7 +877,10 @@ function SearchPageClient() {