搜索增加部分缓存快速启动机制
This commit is contained in:
+88
-82
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 检查搜索结果的收藏状态
|
// 检查搜索结果的收藏状态
|
||||||
|
|||||||
Reference in New Issue
Block a user