mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-22 02:24:44 +08:00
feat: add stitle and stype, optimize prefer choose
This commit is contained in:
@@ -49,6 +49,7 @@ function HomeClient() {
|
||||
episodes: number;
|
||||
source_name: string;
|
||||
currentEpisode?: number;
|
||||
search_title?: string;
|
||||
};
|
||||
|
||||
const [favoriteItems, setFavoriteItems] = useState<FavoriteItem[]>([]);
|
||||
@@ -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() {
|
||||
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-14 sm:gap-y-20 px-2 sm:grid-cols-[repeat(auto-fill,_minmax(11rem,_1fr))] sm:gap-x-8 sm:px-4'>
|
||||
{favoriteItems.map((item) => (
|
||||
<div key={item.id + item.source} className='w-full'>
|
||||
<VideoCard {...item} from='favorite' />
|
||||
<VideoCard
|
||||
query={item.search_title}
|
||||
{...item}
|
||||
from='favorite'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{favoriteItems.length === 0 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -201,7 +201,15 @@ function SearchPageClient() {
|
||||
? aggregatedResults.map(([mapKey, group]) => {
|
||||
return (
|
||||
<div key={`agg-${mapKey}`} className='w-full'>
|
||||
<VideoCard from='search' items={group} />
|
||||
<VideoCard
|
||||
from='search'
|
||||
items={group}
|
||||
query={
|
||||
searchQuery.trim() !== group[0].title
|
||||
? searchQuery.trim()
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -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'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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<string | number, number>();
|
||||
const episodeCountMap = new Map<number, number>();
|
||||
const yearCountMap = new Map<string, number>();
|
||||
|
||||
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 = <T extends string | number>(
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; // 搜索时使用的标题
|
||||
}
|
||||
|
||||
// 存储接口
|
||||
|
||||
Reference in New Issue
Block a user