diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 28cbb2e..7bf446a 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -73,6 +73,7 @@ import ProxyImage from '@/components/ProxyImage'; import { useSite } from '@/components/SiteProvider'; import SmartRecommendations from '@/components/SmartRecommendations'; import Toast, { ToastProps } from '@/components/Toast'; +import VideoCard from '@/components/VideoCard'; import { useDownload } from '@/contexts/DownloadContext'; @@ -91,6 +92,14 @@ interface WakeLockSentinel { removeEventListener(type: 'release', listener: () => void): void; } +interface PlayFallbackRecommendation { + key: string; + item: SearchResult; + episodes?: number; + sourceNames: string[]; + doubanId?: number; +} + function PlayPageClient() { const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080'; const router = useRouter(); @@ -1518,6 +1527,8 @@ function PlayPageClient() { const [sourceSearchError, setSourceSearchError] = useState( null ); + const [fallbackRecommendations, setFallbackRecommendations] = useState([]); + const [hasCompletedSearchRequest, setHasCompletedSearchRequest] = useState(false); const [backgroundSourcesLoading, setBackgroundSourcesLoading] = useState(false); // 优选和测速开关 @@ -3414,8 +3425,109 @@ function PlayPageClient() { }; + const buildFallbackRecommendations = (items: SearchResult[], query: string): PlayFallbackRecommendation[] => { + const typedItems = searchType ? items.filter((item) => getType(item) === searchType) : items; + const preliminaryMap = new Map(); + + typedItems.forEach((item) => { + const preliminaryKey = `${normalizeTitle(item.title).toLowerCase()}-${getType(item)}`; + const group = preliminaryMap.get(preliminaryKey) || []; + group.push(item); + preliminaryMap.set(preliminaryKey, group); + }); + + const finalRecommendations: PlayFallbackRecommendation[] = []; + + preliminaryMap.forEach((group, preliminaryKey) => { + const withYear = new Map(); + const withoutYear: SearchResult[] = []; + + group.forEach((item) => { + if (item.year && item.year.trim() !== '' && item.year !== 'unknown' && /^\d{4}$/.test(item.year)) { + const yearGroup = withYear.get(item.year) || []; + yearGroup.push(item); + withYear.set(item.year, yearGroup); + } else { + withoutYear.push(item); + } + }); + + const emitGroup = (groupKey: string, mergedGroup: SearchResult[]) => { + const sourceNames = Array.from(new Set(mergedGroup.map((item) => item.source_name).filter(Boolean))); + const episodeCountMap = new Map(); + const doubanCountMap = new Map(); + + mergedGroup.forEach((item) => { + const episodeCount = item.episodes?.length || 0; + if (episodeCount > 0) { + episodeCountMap.set(episodeCount, (episodeCountMap.get(episodeCount) || 0) + 1); + } + if (item.douban_id && item.douban_id > 0) { + doubanCountMap.set(item.douban_id, (doubanCountMap.get(item.douban_id) || 0) + 1); + } + }); + + let episodes = 0; + let episodeVotes = 0; + episodeCountMap.forEach((votes, count) => { + if (votes > episodeVotes) { + episodeVotes = votes; + episodes = count; + } + }); + + let doubanId: number | undefined; + let doubanVotes = 0; + doubanCountMap.forEach((votes, id) => { + if (votes > doubanVotes) { + doubanVotes = votes; + doubanId = id; + } + }); + + const representative = mergedGroup.slice().sort((a, b) => { + const aPoster = a.poster ? 1 : 0; + const bPoster = b.poster ? 1 : 0; + if (bPoster !== aPoster) return bPoster - aPoster; + return (b.weight ?? 0) - (a.weight ?? 0); + })[0]; + + finalRecommendations.push({ + key: groupKey, + item: representative, + episodes: episodes || undefined, + sourceNames, + doubanId, + }); + }; + + if (withYear.size > 0) { + withYear.forEach((yearGroup, year) => { + emitGroup(`${preliminaryKey}-${year}`, [...yearGroup, ...withoutYear]); + }); + } else if (withoutYear.length > 0) { + emitGroup(`${preliminaryKey}-unknown`, withoutYear); + } + }); + + const normalizedQuery = normalizeTitle(query).toLowerCase(); + + return finalRecommendations + .sort((a, b) => { + const aContains = normalizeTitle(a.item.title).toLowerCase().includes(normalizedQuery) ? 1 : 0; + const bContains = normalizeTitle(b.item.title).toLowerCase().includes(normalizedQuery) ? 1 : 0; + if (bContains !== aContains) return bContains - aContains; + if (b.sourceNames.length !== a.sourceNames.length) return b.sourceNames.length - a.sourceNames.length; + return (b.item.weight ?? 0) - (a.item.weight ?? 0); + }) + .slice(0, 12); + }; + const fetchSourcesData = async (query: string): Promise => { // 根据搜索词获取全部源信息 + setHasCompletedSearchRequest(false); + setFallbackRecommendations([]); + try { // 先检查 sessionStorage 中是否有缓存 const cacheKey = `search_cache_${query.trim()}`; @@ -3426,7 +3538,10 @@ function PlayPageClient() { const cached = sessionStorage.getItem(cacheKey); if (cached) { console.log('[Play] 使用 sessionStorage 缓存的搜索结果'); - const cachedData = JSON.parse(cached); + const cachedData = JSON.parse(cached) as SearchResult[]; + + setHasCompletedSearchRequest(true); + setFallbackRecommendations(buildFallbackRecommendations(cachedData, query)); // 处理缓存的搜索结果,根据规则过滤 results = cachedData.filter( @@ -3462,9 +3577,13 @@ function PlayPageClient() { throw new Error('搜索失败'); } const data = await response.json(); + const allResults = (data.results || []) as SearchResult[]; + + setHasCompletedSearchRequest(true); + setFallbackRecommendations(buildFallbackRecommendations(allResults, query)); // 处理搜索结果,根据规则过滤 - results = data.results.filter( + results = allResults.filter( (result: SearchResult) => normalizeTitle(result.title).toLowerCase() === normalizeTitle(videoTitleRef.current).toLowerCase() && @@ -7918,71 +8037,107 @@ function PlayPageClient() { if (error) { return ( -
-
- {/* 错误图标 */} -
-
-
😵
- {/* 脉冲效果 */} -
+
+
+
+ {/* 错误图标 */} +
+
+
😵
+ {/* 脉冲效果 */} +
+
+ + {/* 浮动错误粒子 */} +
+
+
+
+
- {/* 浮动错误粒子 */} -
-
-
-
-
-
- - {/* 错误信息 */} -
-

- 哎呀,出现了一些问题 -

-
-

- {error} + {/* 错误信息 */} +

+

+ 哎呀,出现了一些问题 +

+
+

+ {error} +

+
+

+ 请检查网络连接或尝试刷新页面

-

- 请检查网络连接或尝试刷新页面 -

+ + {/* 操作按钮 */} +
+ + + +
- {/* 操作按钮 */} -
- - - -
+ {hasCompletedSearchRequest && fallbackRecommendations.length > 0 && ( +
+
+ +

+ 也许你想看 +

+
+
+
+ {fallbackRecommendations.map((recommendation) => ( +
+ +
+ ))} +
+
+
+ )}
); } + return ( {/* TMDB背景图 */}