搜索逻辑优化,聚合逻辑优化
This commit is contained in:
@@ -2288,6 +2288,41 @@ function PlayPageClient() {
|
||||
.replace(/[^\w\u4e00-\u9fa5]/g, ''); // 去除特殊符号,保留字母、数字、下划线和中文
|
||||
};
|
||||
|
||||
// 辅助函数:获取视频类型
|
||||
const getType = (item: SearchResult): 'movie' | 'tv' => {
|
||||
// 1. Emby 和 OpenList 源:使用 type_name(基于 TMDB,最可靠)
|
||||
if (item.source === 'emby' || item.source?.startsWith('emby_') || item.source === 'openlist') {
|
||||
return item.type_name === '电影' ? 'movie' : 'tv';
|
||||
}
|
||||
|
||||
// 2. API 采集源:综合判断
|
||||
const typeName = item.type_name?.toLowerCase() || '';
|
||||
|
||||
// 2.1 明确包含"电影"或"movie"或"片"的,判断为电影
|
||||
if (typeName.includes('电影') || typeName.includes('movie') ||
|
||||
typeName.endsWith('片') && !typeName.includes('动漫')) {
|
||||
return 'movie';
|
||||
}
|
||||
|
||||
// 2.2 包含"剧"、"动漫"、"综艺"等关键词的,判断为剧集
|
||||
if (typeName.includes('剧') || typeName.includes('动漫') ||
|
||||
typeName.includes('综艺') || typeName.includes('anime')) {
|
||||
return 'tv';
|
||||
}
|
||||
|
||||
// 2.3 检查 episodes_titles:如果包含"第X集",判断为剧集
|
||||
if (item.episodes_titles && item.episodes_titles.length > 0) {
|
||||
const firstTitle = item.episodes_titles[0] || '';
|
||||
if (/第\d+集|第\d+话|EP?\d+/i.test(firstTitle)) {
|
||||
return 'tv';
|
||||
}
|
||||
}
|
||||
|
||||
// 2.4 兜底:使用 episodes.length(最不可靠)
|
||||
return item.episodes.length === 1 ? 'movie' : 'tv';
|
||||
};
|
||||
|
||||
|
||||
const fetchSourcesData = async (query: string): Promise<SearchResult[]> => {
|
||||
// 根据搜索词获取全部源信息
|
||||
try {
|
||||
@@ -2303,19 +2338,19 @@ function PlayPageClient() {
|
||||
const cachedData = JSON.parse(cached);
|
||||
|
||||
// 处理缓存的搜索结果,根据规则过滤
|
||||
results = cachedData.filter(
|
||||
results = cachedData.filter(
|
||||
(result: SearchResult) =>
|
||||
normalizeTitle(result.title).toLowerCase() ===
|
||||
normalizeTitle(videoTitleRef.current).toLowerCase() &&
|
||||
(videoYearRef.current
|
||||
? result.year.toLowerCase() === videoYearRef.current.toLowerCase()
|
||||
: true) &&
|
||||
(searchType
|
||||
? // openlist 和 emby 源跳过 episodes 长度检查,因为搜索时不返回详细播放列表
|
||||
result.source === 'openlist' ||
|
||||
result.source === 'emby' ||
|
||||
(searchType === 'tv' && result.episodes.length > 1) ||
|
||||
(searchType === 'movie' && result.episodes.length === 1)
|
||||
? 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)
|
||||
);
|
||||
|
||||
@@ -2343,14 +2378,14 @@ function PlayPageClient() {
|
||||
normalizeTitle(result.title).toLowerCase() ===
|
||||
normalizeTitle(videoTitleRef.current).toLowerCase() &&
|
||||
(videoYearRef.current
|
||||
? result.year.toLowerCase() === videoYearRef.current.toLowerCase()
|
||||
: true) &&
|
||||
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
|
||||
!result.year ||
|
||||
result.year.trim() === '' ||
|
||||
result.year === 'unknown' ||
|
||||
!/^\d{4}$/.test(result.year)
|
||||
: true) &&
|
||||
(searchType
|
||||
? // openlist 和 emby 源跳过 episodes 长度检查,因为搜索时不返回详细播放列表
|
||||
result.source === 'openlist' ||
|
||||
result.source === 'emby' ||
|
||||
(searchType === 'tv' && result.episodes.length > 1) ||
|
||||
(searchType === 'movie' && result.episodes.length === 1)
|
||||
? getType(result) === searchType
|
||||
: true)
|
||||
);
|
||||
setAvailableSources(results);
|
||||
|
||||
@@ -220,29 +220,98 @@ function SearchPageClient() {
|
||||
.replace(/[^\w\u4e00-\u9fa5]/g, ''); // 去除特殊符号,保留字母、数字、下划线和中文
|
||||
};
|
||||
|
||||
// 辅助函数:获取视频类型
|
||||
const getType = (item: SearchResult): 'movie' | 'tv' => {
|
||||
// 1. Emby 和 OpenList 源:使用 type_name(基于 TMDB,最可靠)
|
||||
if (item.source === 'emby' || item.source?.startsWith('emby_') || item.source === 'openlist') {
|
||||
return item.type_name === '电影' ? 'movie' : 'tv';
|
||||
}
|
||||
|
||||
// 2. API 采集源:综合判断
|
||||
const typeName = item.type_name?.toLowerCase() || '';
|
||||
|
||||
// 2.1 明确包含"电影"或"movie"或"片"的,判断为电影
|
||||
if (typeName.includes('电影') || typeName.includes('movie') ||
|
||||
typeName.endsWith('片') && !typeName.includes('动漫')) {
|
||||
return 'movie';
|
||||
}
|
||||
|
||||
// 2.2 包含"剧"、"动漫"、"综艺"等关键词的,判断为剧集
|
||||
if (typeName.includes('剧') || typeName.includes('动漫') ||
|
||||
typeName.includes('综艺') || typeName.includes('anime')) {
|
||||
return 'tv';
|
||||
}
|
||||
|
||||
// 2.3 检查 episodes_titles:如果包含"第X集",判断为剧集
|
||||
if (item.episodes_titles && item.episodes_titles.length > 0) {
|
||||
const firstTitle = item.episodes_titles[0] || '';
|
||||
if (/第\d+集|第\d+话|EP?\d+/i.test(firstTitle)) {
|
||||
return 'tv';
|
||||
}
|
||||
}
|
||||
|
||||
// 2.4 兜底:使用 episodes.length(最不可靠)
|
||||
return item.episodes.length === 1 ? 'movie' : 'tv';
|
||||
};
|
||||
// 聚合后的结果(按标题和年份分组)
|
||||
const aggregatedResults = useMemo(() => {
|
||||
const map = new Map<string, SearchResult[]>();
|
||||
const keyOrder: string[] = []; // 记录键出现的顺序
|
||||
//===== 阶段1:按 normalizedTitle-type 初步分组 =====
|
||||
const preliminaryMap = new Map<string, SearchResult[]>();
|
||||
|
||||
searchResults.forEach((item) => {
|
||||
// 使用规范化后的 title + year + type 作为键,year 必然存在,但依然兜底 'unknown'
|
||||
const normalizedTitle = normalizeTitle(item.title);
|
||||
const key = `${normalizedTitle}-${item.year || 'unknown'
|
||||
}-${item.episodes.length === 1 ? 'movie' : 'tv'}`;
|
||||
const arr = map.get(key) || [];
|
||||
|
||||
// 如果是新的键,记录其顺序
|
||||
if (arr.length === 0) {
|
||||
keyOrder.push(key);
|
||||
}
|
||||
const type = getType(item);
|
||||
const preliminaryKey = `${normalizedTitle}-${type}`;
|
||||
|
||||
const arr = preliminaryMap.get(preliminaryKey) || [];
|
||||
arr.push(item);
|
||||
map.set(key, arr);
|
||||
preliminaryMap.set(preliminaryKey, arr);
|
||||
});
|
||||
|
||||
//===== 阶段2:智能年份推断和最终分组 =====
|
||||
const finalMap = new Map<string, SearchResult[]>();
|
||||
const keyOrder: string[] = [];
|
||||
|
||||
preliminaryMap.forEach((group, preliminaryKey) => {
|
||||
// 分离有年份和无年份的结果
|
||||
const withYear = new Map<string, SearchResult[]>();
|
||||
const withoutYear: SearchResult[] = [];
|
||||
|
||||
group.forEach((item) => {
|
||||
const year = item.year;
|
||||
|
||||
// 判断是否为有效年份:必须是4位数字,且不能是空字符串或'unknown'
|
||||
if (year && year.trim() !== '' && year !== 'unknown' && /^\d{4}$/.test(year)) {
|
||||
// 有有效年份
|
||||
const arr = withYear.get(year) || [];
|
||||
arr.push(item);
|
||||
withYear.set(year, arr);
|
||||
} else {
|
||||
// 无年份(包括空字符串、'unknown'、null、undefined等)
|
||||
withoutYear.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有有效年份组
|
||||
if (withYear.size > 0) {
|
||||
// 将无年份的结果复制到每个有年份的组中
|
||||
withYear.forEach((yearGroup, year) => {
|
||||
const finalKey = `${preliminaryKey}-${year}`;
|
||||
// 合并:有年份的 + 无年份的(复制)
|
||||
const mergedGroup = [...yearGroup, ...withoutYear];
|
||||
finalMap.set(finalKey, mergedGroup);
|
||||
keyOrder.push(finalKey);
|
||||
});
|
||||
} else if (withoutYear.length > 0) {
|
||||
// 如果完全没有年份信息,单独成组
|
||||
const finalKey = `${preliminaryKey}-unknown`;
|
||||
finalMap.set(finalKey, withoutYear);
|
||||
keyOrder.push(finalKey);
|
||||
}
|
||||
});
|
||||
|
||||
// 按出现顺序返回聚合结果
|
||||
return keyOrder.map(key => [key, map.get(key)!] as [string, SearchResult[]]);
|
||||
return keyOrder.map(key => [key, finalMap.get(key)!] as [string, SearchResult[]]);
|
||||
}, [searchResults]);
|
||||
|
||||
// 当聚合结果变化时,如果某个聚合已存在,则调用其卡片 ref 的 set 方法增量更新
|
||||
@@ -296,9 +365,9 @@ function SearchPageClient() {
|
||||
const aIsOpenList = a[0] === 'openlist';
|
||||
const bIsOpenList = b[0] === 'openlist';
|
||||
|
||||
// 判断是否为 emby 源(包括 emby 和 emby:xxx 格式)
|
||||
const aIsEmby = a[0] === 'emby' || a[0].startsWith('emby:');
|
||||
const bIsEmby = b[0] === 'emby' || b[0].startsWith('emby:');
|
||||
// 判断是否为 emby 源(包括 emby 和 emby_xxx 格式)
|
||||
const aIsEmby = a[0] === 'emby' || a[0].startsWith('emby_');
|
||||
const bIsEmby = b[0] === 'emby' || b[0].startsWith('emby_');
|
||||
|
||||
// 优先级:OpenList(100) > Emby(90) > 其他(0)
|
||||
const aPriority = aIsOpenList ? 100 : aIsEmby ? 90 : 0;
|
||||
@@ -1027,7 +1096,14 @@ function SearchPageClient() {
|
||||
const poster = group[0]?.poster || '';
|
||||
const year = group[0]?.year || 'unknown';
|
||||
const { episodes, source_names, douban_id } = computeGroupStats(group);
|
||||
const type = episodes === 1 ? 'movie' : 'tv';
|
||||
|
||||
// 从 mapKey 中提取类型(mapKey 格式:normalizedTitle-type-year)
|
||||
// 找到最后一个 '-' 之前的部分,再找倒数第二个 '-'
|
||||
const lastDashIndex = mapKey.lastIndexOf('-');
|
||||
const secondLastDashIndex = mapKey.lastIndexOf('-', lastDashIndex - 1);
|
||||
const type = secondLastDashIndex > 0
|
||||
? mapKey.substring(secondLastDashIndex + 1, lastDashIndex) as 'movie' | 'tv'
|
||||
: (episodes === 1 ? 'movie' : 'tv'); // 兜底
|
||||
|
||||
// 如果该聚合第一次出现,写入初始统计
|
||||
if (!groupStatsRef.current.has(mapKey)) {
|
||||
|
||||
@@ -779,8 +779,8 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
}
|
||||
className={`flex items-start gap-3 px-2 py-3 rounded-lg transition-all select-none duration-200 relative
|
||||
${isCurrentSource
|
||||
? 'bg-green-500/10 dark:bg-green-500/20 border-green-500/30 border'
|
||||
: 'hover:bg-gray-200/50 dark:hover:bg-white/10 hover:scale-[1.02] cursor-pointer'
|
||||
? 'bg-green-500/10 dark:bg-green-500/20 border-green-500/30 border'
|
||||
: 'hover:bg-gray-200/50 dark:hover:bg-white/10 hover:scale-[1.02] cursor-pointer'
|
||||
}`.trim()}
|
||||
>
|
||||
{/* 封面 */}
|
||||
@@ -855,7 +855,11 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
|
||||
{/* 源名称和集数信息 - 垂直居中 */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-xs px-2 py-1 border border-gray-500/60 rounded text-gray-700 dark:text-gray-300'>
|
||||
<span className={`text-xs px-2 py-1 border rounded text-gray-700 dark:text-gray-300 ${
|
||||
source.source === 'openlist' || source.source === 'emby' || source.source?.startsWith('emby_')
|
||||
? 'border-yellow-500'
|
||||
: 'border-gray-500/60'
|
||||
}`}>
|
||||
{source.source_name}
|
||||
</span>
|
||||
{source.episodes.length > 1 && (
|
||||
|
||||
@@ -159,9 +159,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
const actualEpisodes = dynamicEpisodes;
|
||||
const actualYear = year;
|
||||
const actualQuery = query || '';
|
||||
const actualSearchType = isAggregate
|
||||
? (actualEpisodes && actualEpisodes === 1 ? 'movie' : 'tv')
|
||||
: type;
|
||||
const actualSearchType = type;
|
||||
|
||||
// 获取收藏状态(搜索结果页面不检查)
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user