feat: video loading stage

This commit is contained in:
shinya
2025-07-13 00:04:06 +08:00
parent 323c1ba953
commit db217eb1e8
5 changed files with 280 additions and 329 deletions

View File

@@ -16,10 +16,7 @@ import {
savePlayRecord,
toggleFavorite,
} from '@/lib/db.client';
import {
type VideoDetail,
fetchVideoDetail,
} from '@/lib/fetchVideoDetail.client';
import { type VideoDetail } from '@/lib/fetchVideoDetail.client';
import { SearchResult } from '@/lib/types';
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
@@ -87,8 +84,7 @@ function PlayPageClient() {
needPreferRef.current = needPrefer;
}, [needPrefer]);
// 集数相关
const initialIndex = parseInt(searchParams.get('index') || '1') - 1; // 转换为0基数组索引
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(initialIndex);
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(0);
const currentSourceRef = useRef(currentSource);
const currentIdRef = useRef(currentId);
@@ -141,6 +137,12 @@ function PlayPageClient() {
const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] =
useState(false);
// 换源加载状态
const [isVideoLoading, setIsVideoLoading] = useState(true);
const [videoLoadingStage, setVideoLoadingStage] = useState<
'initing' | 'sourceChanging'
>('initing');
// 播放进度保存相关
const saveIntervalRef = useRef<NodeJS.Timeout | null>(null);
const lastSaveTimeRef = useRef<number>(0);
@@ -445,114 +447,137 @@ function PlayPageClient() {
updateVideoUrl(detail, currentEpisodeIndex);
}, [detail, currentEpisodeIndex]);
// 获取视频详情
// 进入页面时直接获取全部源信息
useEffect(() => {
const fetchDetailAsync = async () => {
console.log(
'fetchDetailAsync',
currentSource,
currentId,
videoTitle,
searchTitle
);
const fetchSourcesData = async (query: string): Promise<SearchResult[]> => {
// 根据搜索词获取全部源信息
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query.trim())}`
);
if (!response.ok) {
throw new Error('搜索失败');
}
const data = await response.json();
// 处理搜索结果,根据规则过滤
const results = data.results.filter(
(result: SearchResult) =>
result.title.replaceAll(' ', '').toLowerCase() ===
videoTitleRef.current.replaceAll(' ', '').toLowerCase() &&
(videoYearRef.current
? result.year.toLowerCase() === videoYearRef.current.toLowerCase()
: true) &&
(searchType
? (searchType === 'tv' && result.episodes.length > 1) ||
(searchType === 'movie' && result.episodes.length === 1)
: true)
);
if (results.length !== 0) {
setAvailableSources(results);
return results;
}
// 未获取到任何内容fallback 使用 source + id
if (!currentSource || !currentId) {
return [];
}
const detailResponse = await fetch(
`/api/detail?source=${currentSource}&id=${currentId}`
);
if (!detailResponse.ok) {
throw new Error('获取视频详情失败');
}
const detailData = (await detailResponse.json()) as SearchResult;
results.push(detailData);
setAvailableSources(results);
return results;
} catch (err) {
setSourceSearchError(err instanceof Error ? err.message : '搜索失败');
setAvailableSources([]);
return [];
} finally {
setSourceSearchLoading(false);
}
};
const initAll = async () => {
if (!currentSource && !currentId && !videoTitle && !searchTitle) {
setError('缺少必要参数');
setLoading(false);
return;
}
setLoading(true);
setLoadingStage(currentSource && currentId ? 'fetching' : 'searching');
setLoadingMessage(
currentSource && currentId
? '🎬 正在获取视频详情...'
: '🔍 正在搜索播放源...'
);
const sourcesInfo = await fetchSourcesData(searchTitle || videoTitle);
if (sourcesInfo.length === 0) {
setError('未找到匹配结果');
setLoading(false);
return;
}
let needLoadSource = currentSource;
let needLoadId = currentId;
if ((!currentSource && !currentId) || needPreferRef.current) {
// 只包含视频标题,搜索视频
setLoading(true);
setLoadingStage('searching');
setLoadingMessage('🔍 正在搜索播放源...');
const searchResults = await handleSearchSources(
searchTitle || videoTitle
);
if (searchResults.length == 0) {
if (currentSource && currentId) {
// 跳过优选
setNeedPrefer(false);
return;
}
setError('未找到匹配结果');
setLoading(false);
return;
}
// 对播放源做优选
setLoadingStage('preferring');
setLoadingMessage('⚡ 正在优选最佳播放源...');
const preferredSource = await preferBestSource(searchResults);
const preferredSource = await preferBestSource(sourcesInfo);
setNeedPrefer(false);
setCurrentSource(preferredSource.source);
setCurrentId(preferredSource.id);
setVideoYear(preferredSource.year);
// 替换URL参数
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('source', preferredSource.source);
newUrl.searchParams.set('id', preferredSource.id);
newUrl.searchParams.set('year', preferredSource.year);
newUrl.searchParams.delete('prefer');
window.history.replaceState({}, '', newUrl.toString());
return;
needLoadSource = preferredSource.source;
needLoadId = preferredSource.id;
}
const fetchDetail = async () => {
try {
setLoadingStage('fetching');
setLoadingMessage('🎬 正在获取视频详情...');
console.log(sourcesInfo);
console.log(needLoadSource, needLoadId);
const detailData = sourcesInfo.find(
(source) =>
source.source === needLoadSource &&
source.id.toString() === needLoadId.toString()
);
if (!detailData) {
setError('未找到匹配结果');
setLoading(false);
return;
}
setVideoTitle(detailData.title || videoTitleRef.current);
setVideoYear(detailData.year);
setVideoCover(detailData.poster);
setDetail(detailData);
if (currentEpisodeIndex >= detailData.episodes.length) {
setCurrentEpisodeIndex(0);
}
const detailData = await fetchVideoDetail({
source: currentSource,
id: currentId,
fallbackTitle: searchTitle || videoTitleRef.current.trim(),
});
// 规范URL参数
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('source', needLoadSource);
newUrl.searchParams.set('id', needLoadId);
newUrl.searchParams.set('year', detailData.year);
newUrl.searchParams.set('title', detailData.title);
newUrl.searchParams.delete('prefer');
window.history.replaceState({}, '', newUrl.toString());
// 更新状态保存详情
setVideoTitle(detailData.title || videoTitleRef.current);
setVideoYear(detailData.year);
setVideoCover(detailData.poster);
setDetail(detailData);
setLoadingStage('ready');
setLoadingMessage('✨ 准备就绪,即将开始播放...');
// 确保集数索引在有效范围内
if (currentEpisodeIndex >= detailData.episodes.length) {
setCurrentEpisodeIndex(0);
}
// 清理URL参数移除index参数
if (searchParams.has('index')) {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('year', detailData.year);
newUrl.searchParams.set(
'title',
detailData.title || videoTitleRef.current
);
newUrl.searchParams.delete('index');
newUrl.searchParams.delete('position');
window.history.replaceState({}, '', newUrl.toString());
}
setLoadingStage('ready');
setLoadingMessage('✨ 准备就绪,即将开始播放...');
// 短暂延迟让用户看到完成状态
setTimeout(() => {
setLoading(false);
}, 1000);
} catch (err) {
console.error('获取视频详情失败:', err);
setError(err instanceof Error ? err.message : '获取视频详情失败');
setLoading(false);
}
};
fetchDetail();
// 短暂延迟让用户看到完成状态
setTimeout(() => {
setLoading(false);
}, 1000);
};
fetchDetailAsync();
}, [currentSource, currentId, needPrefer]);
initAll();
}, []);
// 播放记录处理
useEffect(() => {
@@ -565,33 +590,7 @@ function PlayPageClient() {
const key = generateStorageKey(currentSource, currentId);
const record = allRecords[key];
// URL 参数
const urlIndexParam = searchParams.get('index');
const urlPositionParam = searchParams.get('position');
// 当index参数存在时的处理逻辑
if (urlIndexParam) {
const urlIndex = parseInt(urlIndexParam, 10) - 1;
let targetTime = 0; // 默认从0开始
// 只有index参数和position参数都存在时才生效position
if (urlPositionParam) {
targetTime = parseInt(urlPositionParam, 10);
} else if (record && urlIndex === record.index - 1) {
// 如果有同集播放记录则跳转到播放记录处
targetTime = record.play_time;
}
// 否则从0开始targetTime已经是0
// 更新当前选集索引
if (urlIndex !== currentEpisodeIndex) {
setCurrentEpisodeIndex(urlIndex);
}
// 保存待恢复的播放进度,待播放器就绪后跳转
resumeTimeRef.current = targetTime;
} else if (record) {
// 没有index参数但有播放记录时使用原有逻辑
if (record) {
const targetIndex = record.index - 1;
const targetTime = record.play_time;
@@ -611,84 +610,6 @@ function PlayPageClient() {
initFromHistory();
}, []);
// ---------------------------------------------------------------------------
// 换源搜索与切换
// ---------------------------------------------------------------------------
// 处理换源搜索
const handleSearchSources = async (
query: string
): Promise<SearchResult[]> => {
if (!query.trim()) {
setAvailableSources([]);
return [];
}
setSourceSearchLoading(true);
setSourceSearchError(null);
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query.trim())}`
);
if (!response.ok) {
throw new Error('搜索失败');
}
const data = await response.json();
// 处理搜索结果每个数据源只展示一个优先展示与title同名的结果
const processedResults: SearchResult[] = [];
const sourceMap = new Map<string, SearchResult[]>();
// 按数据源分组
data.results?.forEach((result: SearchResult) => {
if (!sourceMap.has(result.source)) {
sourceMap.set(result.source, []);
}
const list = sourceMap.get(result.source);
if (list) {
list.push(result);
}
});
// 为每个数据源选择最佳结果
sourceMap.forEach((results) => {
if (results.length === 0) return;
// 只选择和当前视频标题完全匹配的结果,如果有年份,还需要年份完全匹配
const exactMatchs = results.filter(
(result) =>
result.title.toLowerCase() ===
videoTitleRef.current.toLowerCase() &&
(videoYearRef.current
? 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) {
processedResults.push(...exactMatchs);
}
});
setAvailableSources(processedResults);
return processedResults;
} catch (err) {
setSourceSearchError(err instanceof Error ? err.message : '搜索失败');
setAvailableSources([]);
return [];
} finally {
setSourceSearchLoading(false);
}
};
// 处理换源
const handleSourceChange = async (
newSource: string,
@@ -696,13 +617,14 @@ function PlayPageClient() {
newTitle: string
) => {
try {
// 显示换源加载状态
setVideoLoadingStage('sourceChanging');
setIsVideoLoading(true);
// 记录当前播放进度(仅在同一集数切换时恢复)
const currentPlayTime = artPlayerRef.current?.currentTime || 0;
console.log('换源前当前播放时间:', currentPlayTime);
// 显示加载状态
setError(null);
// 清除前一个历史记录
if (currentSourceRef.current && currentIdRef.current) {
try {
@@ -716,12 +638,13 @@ function PlayPageClient() {
}
}
// 获取新源的详情
const newDetail = await fetchVideoDetail({
source: newSource,
id: newId,
fallbackTitle: searchTitle || newTitle.trim(),
});
const newDetail = availableSources.find(
(source) => source.source === newSource && source.id === newId
);
if (!newDetail) {
setError('未找到匹配结果');
return;
}
// 尝试跳转到当前正在播放的集数
let targetIndex = currentEpisodeIndex;
@@ -732,11 +655,13 @@ function PlayPageClient() {
}
// 如果仍然是同一集数且播放进度有效,则在播放器就绪后恢复到原始进度
if (targetIndex === currentEpisodeIndex && currentPlayTime > 1) {
resumeTimeRef.current = currentPlayTime;
} else {
// 否则从头开始播放,防止影响后续选集逻辑
if (targetIndex !== currentEpisodeIndex) {
resumeTimeRef.current = 0;
} else if (
(!resumeTimeRef.current || resumeTimeRef.current === 0) &&
currentPlayTime > 1
) {
resumeTimeRef.current = currentPlayTime;
}
// 更新URL参数不刷新页面
@@ -754,6 +679,8 @@ function PlayPageClient() {
setDetail(newDetail);
setCurrentEpisodeIndex(targetIndex);
} catch (err) {
// 隐藏换源加载状态
setIsVideoLoading(false);
setError(err instanceof Error ? err.message : '换源失败');
}
};
@@ -1243,8 +1170,8 @@ function PlayPageClient() {
} catch (err) {
console.warn('恢复播放进度失败:', err);
}
resumeTimeRef.current = null;
}
resumeTimeRef.current = null;
setTimeout(() => {
if (
@@ -1254,6 +1181,9 @@ function PlayPageClient() {
}
artPlayerRef.current.notice.show = '';
}, 0);
// 隐藏换源加载状态
setIsVideoLoading(false);
});
artPlayerRef.current.on('error', (err: any) => {
@@ -1344,10 +1274,9 @@ function PlayPageClient() {
<div className='flex justify-center space-x-2 mb-4'>
<div
className={`w-3 h-3 rounded-full transition-all duration-500 ${
loadingStage === 'searching'
loadingStage === 'searching' || loadingStage === 'fetching'
? 'bg-green-500 scale-125'
: loadingStage === 'preferring' ||
loadingStage === 'fetching' ||
loadingStage === 'ready'
? 'bg-green-500'
: 'bg-gray-300'
@@ -1356,15 +1285,6 @@ function PlayPageClient() {
<div
className={`w-3 h-3 rounded-full transition-all duration-500 ${
loadingStage === 'preferring'
? 'bg-green-500 scale-125'
: loadingStage === 'fetching' || loadingStage === 'ready'
? 'bg-green-500'
: 'bg-gray-300'
}`}
></div>
<div
className={`w-3 h-3 rounded-full transition-all duration-500 ${
loadingStage === 'fetching'
? 'bg-green-500 scale-125'
: loadingStage === 'ready'
? 'bg-green-500'
@@ -1386,12 +1306,11 @@ function PlayPageClient() {
className='h-full bg-gradient-to-r from-green-500 to-emerald-600 rounded-full transition-all duration-1000 ease-out'
style={{
width:
loadingStage === 'searching'
? '25%'
loadingStage === 'searching' ||
loadingStage === 'fetching'
? '33%'
: loadingStage === 'preferring'
? '50%'
: loadingStage === 'fetching'
? '75%'
? '66%'
: '100%',
}}
></div>
@@ -1544,14 +1463,54 @@ function PlayPageClient() {
>
{/* 播放器 */}
<div
className={`h-full transition-all duration-300 ease-in-out ${
className={`h-full transition-all duration-300 ease-in-out rounded-xl border border-white/0 dark:border-white/30 ${
isEpisodeSelectorCollapsed ? 'col-span-1' : 'md:col-span-3'
}`}
>
<div
ref={artRef}
className='bg-black w-full h-[300px] lg:h-full rounded-xl overflow-hidden border border-white/0 dark:border-white/30 shadow-lg'
></div>
<div className='relative w-full h-[300px] lg:h-full'>
<div
ref={artRef}
className='bg-black w-full h-full rounded-xl overflow-hidden shadow-lg'
></div>
{/* 换源加载蒙层 */}
{isVideoLoading && (
<div className='absolute inset-0 bg-black/85 backdrop-blur-sm rounded-xl flex items-center justify-center z-[9999] transition-all duration-300'>
<div className='text-center max-w-md mx-auto px-6'>
{/* 动画影院图标 */}
<div className='relative mb-8'>
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
<div className='text-white text-4xl'>🎬</div>
{/* 旋转光环 */}
<div className='absolute -inset-2 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl opacity-20 animate-spin'></div>
</div>
{/* 浮动粒子效果 */}
<div className='absolute top-0 left-0 w-full h-full pointer-events-none'>
<div className='absolute top-2 left-2 w-2 h-2 bg-green-400 rounded-full animate-bounce'></div>
<div
className='absolute top-4 right-4 w-1.5 h-1.5 bg-emerald-400 rounded-full animate-bounce'
style={{ animationDelay: '0.5s' }}
></div>
<div
className='absolute bottom-3 left-6 w-1 h-1 bg-lime-400 rounded-full animate-bounce'
style={{ animationDelay: '1s' }}
></div>
</div>
</div>
{/* 换源消息 */}
<div className='space-y-2'>
<p className='text-xl font-semibold text-white animate-pulse'>
{videoLoadingStage === 'sourceChanging'
? '🔄 切换播放源...'
: '🔄 视频加载中...'}
</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
@@ -1571,7 +1530,6 @@ function PlayPageClient() {
currentId={currentId}
videoTitle={searchTitle || videoTitle}
availableSources={availableSources}
onSearchSources={handleSearchSources}
sourceSearchLoading={sourceSearchLoading}
sourceSearchError={sourceSearchError}
precomputedVideoInfo={precomputedVideoInfo}

View File

@@ -41,27 +41,44 @@ function SearchPageClient() {
const map = new Map<string, SearchResult[]>();
searchResults.forEach((item) => {
// 使用 title + year + type 作为键year 必然存在,但依然兜底 'unknown'
const key = `${item.title}-${item.year || 'unknown'}-${
item.episodes.length === 1 ? 'movie' : 'tv'
}`;
const key = `${item.title.replaceAll(' ', '')}-${
item.year || 'unknown'
}-${item.episodes.length === 1 ? 'movie' : 'tv'}`;
const arr = map.get(key) || [];
arr.push(item);
map.set(key, arr);
});
return Array.from(map.entries()).sort((a, b) => {
// 优先排序:标题与搜索词完全一致的排在前面
const aExactMatch = a[1][0].title === searchQuery.trim();
const bExactMatch = b[1][0].title === searchQuery.trim();
const aExactMatch = a[1][0].title
.replaceAll(' ', '')
.includes(searchQuery.trim().replaceAll(' ', ''));
const bExactMatch = b[1][0].title
.replaceAll(' ', '')
.includes(searchQuery.trim().replaceAll(' ', ''));
if (aExactMatch && !bExactMatch) return -1;
if (!aExactMatch && bExactMatch) return 1;
// 如果都匹配或都不匹配,则按原来的逻辑排序
return a[1][0].year === b[1][0].year
? a[0].localeCompare(b[0])
: a[1][0].year > b[1][0].year
? -1
: 1;
// 年份排序
if (a[1][0].year === b[1][0].year) {
return a[0].localeCompare(b[0]);
} else {
// 处理 unknown 的情况
const aYear = a[1][0].year;
const bYear = b[1][0].year;
if (aYear === 'unknown' && bYear === 'unknown') {
return 0;
} else if (aYear === 'unknown') {
return 1; // a 排在后面
} else if (bYear === 'unknown') {
return -1; // b 排在后面
} else {
// 都是数字年份,按数字大小排序(大的在前面)
return aYear > bYear ? -1 : 1;
}
}
});
}, [searchResults]);
@@ -105,11 +122,21 @@ function SearchPageClient() {
if (!aExactMatch && bExactMatch) return 1;
// 如果都匹配或都不匹配,则按原来的逻辑排序
return a.year === b.year
? a.title.localeCompare(b.title)
: a.year > b.year
? -1
: 1;
if (a.year === b.year) {
return a.title.localeCompare(b.title);
} else {
// 处理 unknown 的情况
if (a.year === 'unknown' && b.year === 'unknown') {
return 0;
} else if (a.year === 'unknown') {
return 1; // a 排在后面
} else if (b.year === 'unknown') {
return -1; // b 排在后面
} else {
// 都是数字年份,按数字大小排序(大的在前面)
return parseInt(a.year) > parseInt(b.year) ? -1 : 1;
}
}
})
);
setShowResults(true);

View File

@@ -36,7 +36,6 @@ interface EpisodeSelectorProps {
videoTitle?: string;
videoYear?: string;
availableSources?: SearchResult[];
onSearchSources?: (query: string) => void;
sourceSearchLoading?: boolean;
sourceSearchError?: string | null;
/** 预计算的测速结果,避免重复测速 */
@@ -56,7 +55,6 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
currentId,
videoTitle,
availableSources = [],
onSearchSources,
sourceSearchLoading = false,
sourceSearchError = null,
precomputedVideoInfo,
@@ -207,11 +205,25 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
// 当分页切换时,将激活的分页标签滚动到视口中间
useEffect(() => {
const btn = buttonRefs.current[currentPage];
if (btn) {
btn.scrollIntoView({
const container = categoryContainerRef.current;
if (btn && container) {
// 手动计算滚动位置,只滚动分页标签容器
const containerRect = container.getBoundingClientRect();
const btnRect = btn.getBoundingClientRect();
const scrollLeft = container.scrollLeft;
// 计算按钮相对于容器的位置
const btnLeft = btnRect.left - containerRect.left + scrollLeft;
const btnWidth = btnRect.width;
const containerWidth = containerRect.width;
// 计算目标滚动位置,使按钮居中
const targetScrollLeft = btnLeft - (containerWidth - btnWidth) / 2;
// 平滑滚动到目标位置
container.scrollTo({
left: targetScrollLeft,
behavior: 'smooth',
inline: 'center',
block: 'nearest',
});
}
}, [currentPage, pageCount]);
@@ -219,10 +231,6 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
// 处理换源tab点击只在点击时才搜索
const handleSourceTabClick = () => {
setActiveTab('sources');
// 只在点击时搜索,且只搜索一次
if (availableSources.length === 0 && videoTitle && onSearchSources) {
onSearchSources(videoTitle);
}
};
const handleCategoryClick = useCallback((index: number) => {
@@ -243,20 +251,6 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
[onSourceChange]
);
// 如果组件初始即显示 "换源",自动触发搜索一次
useEffect(() => {
if (
activeTab === 'sources' &&
availableSources.length === 0 &&
videoTitle &&
onSearchSources
) {
onSearchSources(videoTitle);
}
// 只在依赖变化时尝试availableSources 长度变化可阻止重复搜索
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, availableSources.length, videoTitle]);
const currentStart = currentPage * episodesPerPage + 1;
const currentEnd = Math.min(
currentStart + episodesPerPage - 1,

View File

@@ -1,5 +1,5 @@
import { API_CONFIG, ApiSite, getConfig } from '@/lib/config';
import { SearchResult, VideoDetail } from '@/lib/types';
import { SearchResult } from '@/lib/types';
import { cleanHtmlTags } from '@/lib/utils';
const config = getConfig();
@@ -77,7 +77,7 @@ export async function searchFromApi(
});
return {
id: item.vod_id,
id: item.vod_id.toString(),
title: item.vod_name.trim().replace(/\s+/g, ' '),
poster: item.vod_pic,
episodes,
@@ -147,7 +147,7 @@ export async function searchFromApi(
});
return {
id: item.vod_id,
id: item.vod_id.toString(),
title: item.vod_name.trim().replace(/\s+/g, ' '),
poster: item.vod_pic,
episodes,
@@ -193,7 +193,7 @@ const M3U8_PATTERN = /(https?:\/\/[^"'\s]+?\.m3u8)/g;
export async function getDetailFromApi(
apiSite: ApiSite,
id: string
): Promise<VideoDetail> {
): Promise<SearchResult> {
if (apiSite.detail) {
return handleSpecialSourceDetail(id, apiSite);
}
@@ -253,32 +253,26 @@ export async function getDetailFromApi(
}
return {
code: 200,
id: id.toString(),
title: videoDetail.vod_name,
poster: videoDetail.vod_pic,
episodes,
detailUrl,
videoInfo: {
title: videoDetail.vod_name,
cover: videoDetail.vod_pic,
desc: cleanHtmlTags(videoDetail.vod_content),
type: videoDetail.type_name,
year: videoDetail.vod_year
? videoDetail.vod_year.match(/\d{4}/)?.[0] || ''
: 'unknown',
area: videoDetail.vod_area,
director: videoDetail.vod_director,
actor: videoDetail.vod_actor,
remarks: videoDetail.vod_remarks,
source_name: apiSite.name,
source: apiSite.key,
id,
},
source: apiSite.key,
source_name: apiSite.name,
class: videoDetail.vod_class,
year: videoDetail.vod_year
? videoDetail.vod_year.match(/\d{4}/)?.[0] || ''
: 'unknown',
desc: cleanHtmlTags(videoDetail.vod_content),
type_name: videoDetail.type_name,
douban_id: videoDetail.vod_douban_id,
};
}
async function handleSpecialSourceDetail(
id: string,
apiSite: ApiSite
): Promise<VideoDetail> {
): Promise<SearchResult> {
const detailUrl = `${apiSite.detail}/index.php/vod/detail/id/${id}.html`;
const controller = new AbortController();
@@ -335,17 +329,16 @@ async function handleSpecialSourceDetail(
const yearText = yearMatch ? yearMatch[1] : 'unknown';
return {
code: 200,
id,
title: titleText,
poster: coverUrl,
episodes: matches,
detailUrl,
videoInfo: {
title: titleText,
cover: coverUrl,
desc: descText,
source_name: apiSite.name,
source: apiSite.key,
year: yearText,
id,
},
source: apiSite.key,
source_name: apiSite.name,
class: '',
year: yearText,
desc: descText,
type_name: '',
douban_id: 0,
};
}

View File

@@ -62,27 +62,6 @@ export interface IStorage {
setAdminConfig(config: AdminConfig): Promise<void>;
}
// 视频详情数据结构
export interface VideoDetail {
code: number;
episodes: string[];
detailUrl: string;
videoInfo: {
title: string;
cover?: string;
desc?: string;
type?: string;
year?: string;
area?: string;
director?: string;
actor?: string;
remarks?: string;
source_name: string;
source: string;
id: string;
};
}
// 搜索结果数据结构
export interface SearchResult {
id: string;