diff --git a/src/app/page.tsx b/src/app/page.tsx index b6a4cea..70b7cbf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -49,6 +49,7 @@ function HomeClient() { episodes: number; source_name: string; currentEpisode?: number; + search_title?: string; }; const [favoriteItems, setFavoriteItems] = useState([]); @@ -112,6 +113,7 @@ function HomeClient() { episodes: fav.total_episodes, source_name: fav.source_name, currentEpisode, + search_title: fav?.search_title, } as FavoriteItem; }); setFavoriteItems(sorted); @@ -161,7 +163,11 @@ function HomeClient() {
{favoriteItems.map((item) => (
- +
))} {favoriteItems.length === 0 && ( diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index f17d204..ec0b1f1 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -74,6 +74,10 @@ function PlayPageClient() { ); const [currentId, setCurrentId] = useState(searchParams.get('id') || ''); + // 搜索所需信息 + const [searchTitle] = useState(searchParams.get('stitle') || ''); + const [searchType] = useState(searchParams.get('stype') || ''); + // 是否需要优选 const [needPrefer, setNeedPrefer] = useState( searchParams.get('prefer') === 'true' @@ -159,7 +163,6 @@ function PlayPageClient() { const allResults: Array<{ source: SearchResult; testResult: { quality: string; loadSpeed: string; pingTime: number }; - score: number; } | null> = []; for (let start = 0; start < sources.length; start += batchSize) { @@ -181,7 +184,6 @@ function PlayPageClient() { return { source, testResult, - score: calculateSourceScore(testResult), }; } catch (error) { return null; @@ -216,7 +218,6 @@ function PlayPageClient() { const successfulResults = allResults.filter(Boolean) as Array<{ source: SearchResult; testResult: { quality: string; loadSpeed: string; pingTime: number }; - score: number; }>; setPrecomputedVideoInfo(newVideoInfoMap); @@ -226,11 +227,47 @@ function PlayPageClient() { return sources[0]; } + // 找出所有有效速度的最大值,用于线性映射 + const validSpeeds = successfulResults + .map((result) => { + const speedStr = result.testResult.loadSpeed; + if (speedStr === '未知' || speedStr === '测量中...') return 0; + + const match = speedStr.match(/^([\d.]+)\s*(KB\/s|MB\/s)$/); + if (!match) return 0; + + const value = parseFloat(match[1]); + const unit = match[2]; + return unit === 'MB/s' ? value * 1024 : value; // 统一转换为 KB/s + }) + .filter((speed) => speed > 0); + + const maxSpeed = validSpeeds.length > 0 ? Math.max(...validSpeeds) : 1024; // 默认1MB/s作为基准 + + // 找出所有有效延迟的最小值和最大值,用于线性映射 + const validPings = successfulResults + .map((result) => result.testResult.pingTime) + .filter((ping) => ping > 0); + + const minPing = validPings.length > 0 ? Math.min(...validPings) : 50; + const maxPing = validPings.length > 0 ? Math.max(...validPings) : 1000; + + // 计算每个结果的评分 + const resultsWithScore = successfulResults.map((result) => ({ + ...result, + score: calculateSourceScore( + result.testResult, + maxSpeed, + minPing, + maxPing + ), + })); + // 按综合评分排序,选择最佳播放源 - successfulResults.sort((a, b) => b.score - a.score); + resultsWithScore.sort((a, b) => b.score - a.score); console.log('播放源评分排序结果:'); - successfulResults.forEach((result, index) => { + resultsWithScore.forEach((result, index) => { console.log( `${index + 1}. ${ result.source.source_name @@ -240,15 +277,20 @@ function PlayPageClient() { ); }); - return successfulResults[0].source; + return resultsWithScore[0].source; }; // 计算播放源综合评分 - const calculateSourceScore = (testResult: { - quality: string; - loadSpeed: string; - pingTime: number; - }): number => { + const calculateSourceScore = ( + testResult: { + quality: string; + loadSpeed: string; + pingTime: number; + }, + maxSpeed: number, + minPing: number, + maxPing: number + ): number => { let score = 0; // 分辨率评分 (40% 权重) @@ -272,7 +314,7 @@ function PlayPageClient() { })(); score += qualityScore * 0.4; - // 下载速度评分 (40% 权重) + // 下载速度评分 (40% 权重) - 基于最大速度线性映射 const speedScore = (() => { const speedStr = testResult.loadSpeed; if (speedStr === '未知' || speedStr === '测量中...') return 30; @@ -285,25 +327,23 @@ function PlayPageClient() { const unit = match[2]; const speedKBps = unit === 'MB/s' ? value * 1024 : value; - // 根据速度给分 - if (speedKBps >= 5120) return 100; // ≥5MB/s - if (speedKBps >= 2048) return 85; // ≥2MB/s - if (speedKBps >= 1024) return 70; // ≥1MB/s - if (speedKBps >= 512) return 55; // ≥512KB/s - if (speedKBps >= 256) return 40; // ≥256KB/s - return 25; // <256KB/s + // 基于最大速度线性映射,最高100分 + const speedRatio = speedKBps / maxSpeed; + return Math.min(100, Math.max(0, speedRatio * 100)); })(); score += speedScore * 0.4; - // 网络延迟评分 (20% 权重) + // 网络延迟评分 (20% 权重) - 基于延迟范围线性映射 const pingScore = (() => { const ping = testResult.pingTime; - if (ping <= 50) return 100; // ≤50ms - if (ping <= 100) return 85; // ≤100ms - if (ping <= 200) return 70; // ≤200ms - if (ping <= 500) return 50; // ≤500ms - if (ping <= 1000) return 30; // ≤1s - return 15; // >1s + if (ping <= 0) return 0; // 无效延迟给默认分 + + // 如果所有延迟都相同,给满分 + if (maxPing === minPing) return 100; + + // 线性映射:最低延迟=100分,最高延迟=0分 + const pingRatio = (maxPing - ping) / (maxPing - minPing); + return Math.min(100, Math.max(0, pingRatio * 100)); })(); score += pingScore * 0.2; @@ -407,8 +447,14 @@ function PlayPageClient() { // 获取视频详情 useEffect(() => { const fetchDetailAsync = async () => { - console.log('fetchDetailAsync', currentSource, currentId, videoTitle); - if (!currentSource && !currentId && !videoTitle) { + console.log( + 'fetchDetailAsync', + currentSource, + currentId, + videoTitle, + searchTitle + ); + if (!currentSource && !currentId && !videoTitle && !searchTitle) { setError('缺少必要参数'); setLoading(false); return; @@ -420,8 +466,15 @@ function PlayPageClient() { setLoadingStage('searching'); setLoadingMessage('🔍 正在搜索播放源...'); - const searchResults = await handleSearchSources(videoTitle); + const searchResults = await handleSearchSources( + searchTitle || videoTitle + ); if (searchResults.length == 0) { + if (currentSource && currentId) { + // 跳过优选 + setNeedPrefer(false); + return; + } setError('未找到匹配结果'); setLoading(false); return; @@ -453,8 +506,9 @@ function PlayPageClient() { const detailData = await fetchVideoDetail({ source: currentSource, id: currentId, - fallbackTitle: videoTitleRef.current.trim(), - fallbackYear: videoYearRef.current, + fallbackTitle: searchTitle || videoTitleRef.current.trim(), + fallbackYear: + videoYearRef.current === 'unknown' ? '' : videoYearRef.current, }); // 更新状态保存详情 @@ -493,7 +547,7 @@ function PlayPageClient() { }; fetchDetailAsync(); - }, [currentSource, currentId]); + }, [currentSource, currentId, needPrefer]); // 播放记录处理 useEffect(() => { @@ -601,13 +655,20 @@ function PlayPageClient() { result.title.toLowerCase() === videoTitleRef.current.toLowerCase() && (videoYearRef.current - ? result.year.toLowerCase() === videoYearRef.current.toLowerCase() + ? videoYearRef.current === 'unknown' + ? result.year === '' + : result.year.toLowerCase() === + videoYearRef.current.toLowerCase() : true) && (detailRef.current ? (detailRef.current.episodes.length === 1 && result.episodes.length === 1) || (detailRef.current.episodes.length > 1 && result.episodes.length > 1) + : true) && + (searchType + ? (searchType === 'tv' && result.episodes.length > 1) || + (searchType === 'movie' && result.episodes.length === 1) : true) ); if (exactMatchs.length > 0) { @@ -855,13 +916,14 @@ function PlayPageClient() { await savePlayRecord(currentSourceRef.current, currentIdRef.current, { title: videoTitleRef.current, source_name: detailRef.current?.source_name || '', - year: detailRef.current?.year || '', + year: detailRef.current?.year || 'unknown', cover: detailRef.current?.poster || '', index: currentEpisodeIndexRef.current + 1, // 转换为1基索引 total_episodes: detailRef.current?.episodes.length || 1, play_time: Math.floor(currentTime), total_time: Math.floor(duration), save_time: Date.now(), + search_title: searchTitle, }); lastSaveTimeRef.current = Date.now(); @@ -941,10 +1003,11 @@ function PlayPageClient() { { title: videoTitleRef.current, source_name: detailRef.current?.source_name || '', - year: detailRef.current?.year || '', + year: detailRef.current?.year || 'unknown', cover: detailRef.current?.poster || '', total_episodes: detailRef.current?.episodes.length || 1, save_time: Date.now(), + search_title: searchTitle, } ); setFavorited(newState); @@ -1503,7 +1566,7 @@ function PlayPageClient() { onSourceChange={handleSourceChange} currentSource={currentSource} currentId={currentId} - videoTitle={videoTitle} + videoTitle={searchTitle || videoTitle} availableSources={availableSources} onSearchSources={handleSearchSources} sourceSearchLoading={sourceSearchLoading} diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 90e2f71..1daaf91 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -201,7 +201,15 @@ function SearchPageClient() { ? aggregatedResults.map(([mapKey, group]) => { return (
- +
); }) @@ -218,6 +226,11 @@ function SearchPageClient() { source={item.source} source_name={item.source_name} douban_id={item.douban_id?.toString()} + query={ + searchQuery.trim() !== item.title + ? searchQuery.trim() + : '' + } from='search' />
diff --git a/src/components/ContinueWatching.tsx b/src/components/ContinueWatching.tsx index e367d89..448f1a8 100644 --- a/src/components/ContinueWatching.tsx +++ b/src/components/ContinueWatching.tsx @@ -120,6 +120,7 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) { progress={getProgress(record)} episodes={record.total_episodes} currentEpisode={record.index} + query={record.search_title} from='playrecord' onDelete={() => setPlayRecords((prev) => diff --git a/src/components/VideoCard.tsx b/src/components/VideoCard.tsx index ab0e35b..3621874 100644 --- a/src/components/VideoCard.tsx +++ b/src/components/VideoCard.tsx @@ -12,6 +12,7 @@ interface VideoCardProps { id?: string; source?: string; title?: string; + query?: string; poster?: string; episodes?: number; source_name?: string; @@ -28,6 +29,7 @@ interface VideoCardProps { export default function VideoCard({ id, title = '', + query = '', poster = '', episodes, source, @@ -54,7 +56,6 @@ export default function VideoCard({ const countMap = new Map(); const episodeCountMap = new Map(); - const yearCountMap = new Map(); items.forEach((item) => { if (item.douban_id && item.douban_id !== 0) { @@ -64,10 +65,6 @@ export default function VideoCard({ if (len > 0) { episodeCountMap.set(len, (episodeCountMap.get(len) || 0) + 1); } - if (item.year?.trim()) { - const yearStr = item.year.trim(); - yearCountMap.set(yearStr, (yearCountMap.get(yearStr) || 0) + 1); - } }); const getMostFrequent = ( @@ -88,7 +85,6 @@ export default function VideoCard({ first: items[0], mostFrequentDoubanId: getMostFrequent(countMap), mostFrequentEpisodes: getMostFrequent(episodeCountMap) || 0, - mostFrequentYear: getMostFrequent(yearCountMap), }; }, [isAggregate, items]); @@ -100,7 +96,14 @@ export default function VideoCard({ aggregateData?.mostFrequentDoubanId ?? douban_id ); const actualEpisodes = aggregateData?.mostFrequentEpisodes ?? episodes; - const actualYear = aggregateData?.mostFrequentYear ?? year; + const actualYear = + (isAggregate ? aggregateData?.first.year : year) || 'unknown'; + const actualQuery = query || ''; + const actualSearchType = isAggregate + ? aggregateData?.first.episodes.length === 1 + ? 'movie' + : 'tv' + : ''; // 获取收藏状态 useEffect(() => { @@ -176,7 +179,9 @@ export default function VideoCard({ actualTitle )}${actualYear ? `&year=${actualYear}` : ''}${ isAggregate ? '&prefer=true' : '' - }` + }${ + actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : '' + }${actualSearchType ? `&stype=${actualSearchType}` : ''}` ); } }, [ @@ -187,6 +192,8 @@ export default function VideoCard({ actualTitle, actualYear, isAggregate, + actualQuery, + actualSearchType, ]); const config = useMemo(() => { diff --git a/src/lib/db.client.ts b/src/lib/db.client.ts index 63d82bf..1a08563 100644 --- a/src/lib/db.client.ts +++ b/src/lib/db.client.ts @@ -24,6 +24,7 @@ export interface PlayRecord { play_time: number; // 播放进度(秒) total_time: number; // 总进度(秒) save_time: number; // 记录保存时间(时间戳) + search_title?: string; // 搜索时使用的标题 } // ---- 常量 ---- @@ -308,6 +309,7 @@ export interface Favorite { cover: string; total_episodes: number; save_time: number; + search_title?: string; // 搜索时使用的标题 } // 收藏在 localStorage 中使用的 key diff --git a/src/lib/types.ts b/src/lib/types.ts index aa4e726..c547c35 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,6 +10,7 @@ export interface PlayRecord { play_time: number; // 播放进度(秒) total_time: number; // 总进度(秒) save_time: number; // 记录保存时间(时间戳) + search_title?: string; // 搜索时使用的标题 } // 收藏数据结构 @@ -19,6 +20,7 @@ export interface Favorite { title: string; cover: string; save_time: number; // 记录保存时间(时间戳) + search_title?: string; // 搜索时使用的标题 } // 存储接口