搜索增加部分缓存快速启动机制
This commit is contained in:
+88
-82
@@ -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<SearchResult[]> => {
|
||||
// 根据搜索词获取全部源信息
|
||||
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 () => {
|
||||
|
||||
+41
-6
@@ -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<string[]>([]);
|
||||
@@ -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() {
|
||||
<button
|
||||
key={item.key}
|
||||
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'
|
||||
>
|
||||
<div className='flex items-start gap-4'>
|
||||
@@ -1818,6 +1851,7 @@ function SearchPageClient() {
|
||||
<VideoCard
|
||||
ref={getGroupRef(mapKey)}
|
||||
from='search'
|
||||
onBeforeNavigate={savePartialCacheForPlayback}
|
||||
isAggregate={true}
|
||||
title={title}
|
||||
poster={poster}
|
||||
@@ -1867,6 +1901,7 @@ function SearchPageClient() {
|
||||
>
|
||||
<VideoCard
|
||||
id={item.id}
|
||||
onBeforeNavigate={savePartialCacheForPlayback}
|
||||
title={item.title}
|
||||
poster={item.poster}
|
||||
episodes={item.episodes.length}
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface VideoCardProps {
|
||||
episodes?: string[];
|
||||
episodes_titles?: string[];
|
||||
};
|
||||
onBeforeNavigate?: () => void;
|
||||
}
|
||||
|
||||
export type VideoCardHandle = {
|
||||
@@ -107,6 +108,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
playTime,
|
||||
totalTime,
|
||||
cmsData,
|
||||
onBeforeNavigate,
|
||||
}: VideoCardProps,
|
||||
ref
|
||||
) {
|
||||
@@ -326,6 +328,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
return;
|
||||
}
|
||||
|
||||
onBeforeNavigate?.();
|
||||
|
||||
if (origin === 'live' && actualSource && actualId) {
|
||||
// 直播内容跳转到直播页面
|
||||
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
|
||||
@@ -376,6 +380,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
isAggregate,
|
||||
actualQuery,
|
||||
actualSearchType,
|
||||
onBeforeNavigate,
|
||||
]);
|
||||
|
||||
// 新标签页播放处理函数
|
||||
@@ -385,6 +390,8 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
return;
|
||||
}
|
||||
|
||||
onBeforeNavigate?.();
|
||||
|
||||
if (origin === 'live' && actualSource && actualId) {
|
||||
// 直播内容跳转到直播页面
|
||||
const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`;
|
||||
@@ -411,6 +418,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
isAggregate,
|
||||
actualQuery,
|
||||
actualSearchType,
|
||||
onBeforeNavigate,
|
||||
]);
|
||||
|
||||
// 检查搜索结果的收藏状态
|
||||
|
||||
Reference in New Issue
Block a user