视频右键增加详情

This commit is contained in:
mtvpls
2025-12-31 14:32:35 +08:00
parent 3e6d1feb84
commit 99124565d5
4 changed files with 751 additions and 6 deletions

View File

@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { getNextApiKey } from '@/lib/tmdb.client';
import { HttpsProxyAgent } from 'https-proxy-agent';
import nodeFetch from 'node-fetch';
export const runtime = 'nodejs';
/**
* GET /api/tmdb/detail?id=xxx&type=movie|tv
* 获取TMDB详情
*/
export async function GET(request: NextRequest) {
try {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const type = searchParams.get('type') || 'movie';
if (!id) {
return NextResponse.json({ error: '缺少ID参数' }, { status: 400 });
}
const config = await getConfig();
const tmdbApiKey = config.SiteConfig.TMDBApiKey;
const tmdbProxy = config.SiteConfig.TMDBProxy;
const actualKey = getNextApiKey(tmdbApiKey || '');
if (!actualKey) {
return NextResponse.json(
{ error: 'TMDB API Key 未配置' },
{ status: 400 }
);
}
// 根据类型选择API端点
const endpoint = type === 'movie' ? 'movie' : 'tv';
const url = `https://api.themoviedb.org/3/${endpoint}/${id}?api_key=${actualKey}&language=zh-CN&append_to_response=credits`;
const fetchOptions: any = tmdbProxy
? {
agent: new HttpsProxyAgent(tmdbProxy, {
timeout: 30000,
keepAlive: false,
}),
signal: AbortSignal.timeout(30000),
}
: {
signal: AbortSignal.timeout(15000),
};
const response = await nodeFetch(url, fetchOptions);
if (!response.ok) {
console.error('TMDB 详情获取失败:', response.status, response.statusText);
return NextResponse.json(
{ error: 'TMDB 详情获取失败', code: response.status },
{ status: response.status }
);
}
const data: any = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('TMDB详情获取失败:', error);
return NextResponse.json(
{ error: '获取详情失败', details: (error as Error).message },
{ status: 500 }
);
}
}

View File

@@ -240,11 +240,21 @@ function HomeClient() {
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
>
<VideoCard
id={duanju.id}
source={duanju.source}
poster={duanju.poster}
title={duanju.title}
year={duanju.year}
type='tv'
from='douban'
from='search'
source_name={duanju.source_name}
episodes={duanju.episodes?.length}
douban_id={duanju.douban_id}
cmsData={{
desc: duanju.desc,
episodes: duanju.episodes,
episodes_titles: duanju.episodes_titles,
}}
/>
</div>
))}

View File

@@ -0,0 +1,613 @@
'use client';
import { X, Calendar, Star, Clock, Tag, Users, Globe, Film } from 'lucide-react';
import Image from 'next/image';
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
interface DetailPanelProps {
isOpen: boolean;
onClose: () => void;
title: string;
poster?: string;
doubanId?: number;
bangumiId?: number;
isBangumi?: boolean;
tmdbId?: number;
type?: 'movie' | 'tv';
seasonNumber?: number;
cmsData?: {
desc?: string;
episodes?: string[];
episodes_titles?: string[];
};
// 用于调用 source-detail API
sourceId?: string;
source?: string;
}
interface DetailData {
title: string;
originalTitle?: string;
year?: string;
poster?: string;
rating?: {
value: number;
count: number;
};
intro?: string;
genres?: string[];
directors?: Array<{ name: string }>;
actors?: Array<{ name: string }>;
countries?: string[];
languages?: string[];
duration?: string;
episodesCount?: number;
releaseDate?: string;
status?: string;
tagline?: string;
seasons?: number;
overview?: string;
}
const DetailPanel: React.FC<DetailPanelProps> = ({
isOpen,
onClose,
title,
poster,
doubanId,
bangumiId,
isBangumi,
tmdbId,
type = 'movie',
seasonNumber,
cmsData,
sourceId,
source,
}) => {
const [isVisible, setIsVisible] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [mounted, setMounted] = useState(false);
const [detailData, setDetailData] = useState<DetailData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 确保组件在客户端挂载后才渲染 Portal
useEffect(() => {
setMounted(true);
}, []);
// 控制动画状态
useEffect(() => {
let animationId: number;
let timer: NodeJS.Timeout;
if (isOpen) {
setIsVisible(true);
animationId = requestAnimationFrame(() => {
animationId = requestAnimationFrame(() => {
setIsAnimating(true);
});
});
} else {
setIsAnimating(false);
timer = setTimeout(() => {
setIsVisible(false);
}, 200);
}
return () => {
if (animationId) {
cancelAnimationFrame(animationId);
}
if (timer) {
clearTimeout(timer);
}
};
}, [isOpen]);
// 阻止背景滚动
useEffect(() => {
if (isVisible) {
// 保存当前滚动位置
const scrollY = window.scrollY;
const scrollX = window.scrollX;
const body = document.body;
const html = document.documentElement;
// 获取滚动条宽度
const scrollBarWidth = window.innerWidth - html.clientWidth;
// 保存原始样式
const originalBodyStyle = {
position: body.style.position,
top: body.style.top,
left: body.style.left,
right: body.style.right,
width: body.style.width,
paddingRight: body.style.paddingRight,
overflow: body.style.overflow,
};
// 设置body样式来阻止滚动但保持原位置
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.left = `-${scrollX}px`;
body.style.right = '0';
body.style.width = '100%';
body.style.overflow = 'hidden';
body.style.paddingRight = `${scrollBarWidth}px`;
return () => {
// 恢复所有原始样式
body.style.position = originalBodyStyle.position;
body.style.top = originalBodyStyle.top;
body.style.left = originalBodyStyle.left;
body.style.right = originalBodyStyle.right;
body.style.width = originalBodyStyle.width;
body.style.paddingRight = originalBodyStyle.paddingRight;
body.style.overflow = originalBodyStyle.overflow;
// 使用 requestAnimationFrame 确保样式恢复后再滚动
requestAnimationFrame(() => {
window.scrollTo(scrollX, scrollY);
});
};
}
}, [isVisible]);
// ESC键关闭
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
if (isVisible) {
document.addEventListener('keydown', handleEsc);
return () => document.removeEventListener('keydown', handleEsc);
}
}, [isVisible, onClose]);
// 获取详情数据
useEffect(() => {
if (!isOpen) {
return;
}
const fetchDetail = async () => {
setLoading(true);
setError(null);
try {
// 优先使用苹果CMS数据短剧等
// 如果 cmsData 存在但 desc 为空,尝试通过 source-detail API 获取
if (cmsData) {
if (cmsData.desc) {
// 有 desc直接使用
setDetailData({
title: title,
intro: cmsData.desc,
episodesCount: cmsData.episodes?.length,
poster: poster,
});
setLoading(false);
return;
}
// cmsData 存在但 desc 为空,尝试通过 API 获取详情
if (sourceId && source) {
try {
const response = await fetch(
`/api/source-detail?id=${encodeURIComponent(sourceId)}&source=${encodeURIComponent(source)}&title=${encodeURIComponent(title)}`
);
if (response.ok) {
const data = await response.json();
setDetailData({
title: data.title || title,
intro: data.desc || '',
episodesCount: data.episodes?.length || cmsData.episodes?.length,
poster: data.poster || poster,
year: data.year,
});
setLoading(false);
return;
}
} catch (err) {
console.error('获取source-detail失败:', err);
// 继续执行后续逻辑
}
}
}
// 优先使用 Bangumi ID因为 isBangumi 为 true 时doubanId 实际上是 bangumiId
if (bangumiId || (isBangumi && doubanId)) {
const actualBangumiId = bangumiId || doubanId;
const response = await fetch(`https://api.bgm.tv/v0/subjects/${actualBangumiId}`);
if (!response.ok) {
throw new Error('获取Bangumi详情失败');
}
const data = await response.json();
setDetailData({
title: data.name_cn || data.name,
originalTitle: data.name,
year: data.date ? data.date.substring(0, 4) : undefined,
poster: data.images?.large || poster,
rating: data.rating
? {
value: data.rating.score,
count: data.rating.total,
}
: undefined,
intro: data.summary,
genres: data.tags?.map((tag: any) => tag.name).slice(0, 5),
episodesCount: data.eps,
releaseDate: data.date,
});
return;
}
// 使用豆瓣ID
if (doubanId && !isBangumi) {
const response = await fetch(`/api/douban/detail?id=${doubanId}`);
if (!response.ok) {
throw new Error('获取豆瓣详情失败');
}
const data = await response.json();
setDetailData({
title: data.title,
originalTitle: data.original_title,
year: data.year,
poster: data.pic?.large || data.pic?.normal || poster,
rating: data.rating
? {
value: data.rating.value,
count: data.rating.count,
}
: undefined,
intro: data.intro,
genres: data.genres,
directors: data.directors,
actors: data.actors,
countries: data.countries,
languages: data.languages,
duration: data.durations?.[0],
episodesCount: data.episodes_count,
});
return;
}
// 使用 TMDB 搜索
if (title) {
// 移除季度信息进行搜索
let searchTitle = title;
let extractedSeasonNumber = seasonNumber;
// 匹配各种季度格式: 第一季、第1季、第一部、Season 1、S1等
const seasonPatterns = [
/第([一二三四五六七八九十\d]+)[季部]/,
/Season\s*(\d+)/i,
/S(\d+)/i,
];
for (const pattern of seasonPatterns) {
const match = title.match(pattern);
if (match) {
searchTitle = title.replace(pattern, '').trim();
// 如果没有传入seasonNumber,尝试从标题中提取
if (!extractedSeasonNumber) {
const seasonStr = match[1];
// 中文数字转数字
const chineseNumbers: Record<string, number> = {
'一': 1, '二': 2, '三': 3, '四': 4, '五': 5,
'六': 6, '七': 7, '八': 8, '九': 9, '十': 10,
};
extractedSeasonNumber = chineseNumbers[seasonStr] || parseInt(seasonStr) || undefined;
}
break;
}
}
const searchResponse = await fetch(
`/api/tmdb/search?query=${encodeURIComponent(searchTitle)}`
);
if (!searchResponse.ok) {
throw new Error('搜索失败');
}
const searchData = await searchResponse.json();
if (searchData.results && searchData.results.length > 0) {
const result = searchData.results[0];
const detailId = result.id;
const mediaType = result.media_type || type;
// 获取详情
const detailResponse = await fetch(`/api/tmdb/detail?id=${detailId}&type=${mediaType}`);
if (!detailResponse.ok) {
throw new Error('获取TMDB详情失败');
}
const detailResult = await detailResponse.json();
// 如果有季度信息,尝试获取季度详情
let seasonData = null;
if (extractedSeasonNumber && mediaType === 'tv') {
try {
const seasonResponse = await fetch(
`/api/tmdb/seasons?id=${detailId}&season=${extractedSeasonNumber}`
);
if (seasonResponse.ok) {
seasonData = await seasonResponse.json();
}
} catch (err) {
console.error('获取季度信息失败', err);
}
}
setDetailData({
title: mediaType === 'movie' ? detailResult.title : detailResult.name,
originalTitle:
mediaType === 'movie' ? detailResult.original_title : detailResult.original_name,
year:
mediaType === 'movie'
? detailResult.release_date?.substring(0, 4)
: detailResult.first_air_date?.substring(0, 4),
poster: detailResult.poster_path
? `https://image.tmdb.org/t/p/w500${detailResult.poster_path}`
: poster,
rating: detailResult.vote_average
? {
value: detailResult.vote_average,
count: detailResult.vote_count,
}
: undefined,
intro: seasonData?.overview || detailResult.overview,
genres: detailResult.genres?.map((g: any) => g.name),
countries: detailResult.production_countries?.map((c: any) => c.name),
languages: detailResult.spoken_languages?.map((l: any) => l.name),
duration: detailResult.runtime ? `${detailResult.runtime}分钟` : undefined,
episodesCount: seasonData?.episodes?.length || detailResult.number_of_episodes,
releaseDate:
mediaType === 'movie' ? detailResult.release_date : detailResult.first_air_date,
status: detailResult.status,
tagline: detailResult.tagline,
seasons: detailResult.number_of_seasons,
overview: detailResult.overview,
});
return;
}
throw new Error('未找到相关内容');
}
throw new Error('缺少必要的查询参数');
} catch (err) {
console.error('获取详情失败:', err);
setError(err instanceof Error ? err.message : '获取详情失败');
} finally {
setLoading(false);
}
};
fetchDetail();
}, [isOpen, doubanId, bangumiId, isBangumi, tmdbId, title, type, seasonNumber, poster, cmsData, sourceId, source]);
if (!isVisible || !mounted) return null;
const content = (
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
{/* 背景遮罩 */}
<div
className={`absolute inset-0 bg-black/50 transition-opacity duration-200 ease-out ${
isAnimating ? 'opacity-100' : 'opacity-0'
}`}
onClick={onClose}
style={{
backdropFilter: 'blur(4px)',
willChange: 'opacity',
}}
/>
{/* 详情面板 */}
<div
className="relative w-full max-w-2xl max-h-[90vh] bg-white dark:bg-gray-900 rounded-2xl shadow-2xl overflow-hidden transition-all duration-200 ease-out"
style={{
willChange: 'transform, opacity',
backfaceVisibility: 'hidden',
transform: isAnimating ? 'scale(1) translateZ(0)' : 'scale(0.95) translateZ(0)',
opacity: isAnimating ? 1 : 0,
}}
>
{/* 头部 */}
<div className="flex items-center justify-between p-4 border-b border-gray-100 dark:border-gray-800 sticky top-0 bg-white dark:bg-gray-900 z-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100"></h2>
<button
onClick={onClose}
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
>
<X size={20} className="text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* 内容区域 */}
<div className="overflow-y-auto max-h-[calc(90vh-4rem)]">
{loading && (
<div className="flex items-center justify-center py-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500"></div>
</div>
)}
{error && (
<div className="p-6 text-center">
<p className="text-red-500 dark:text-red-400">{error}</p>
</div>
)}
{!loading && !error && detailData && (
<div className="p-6">
{/* 海报和基本信息 */}
<div className="flex gap-6 mb-6">
{detailData.poster && (
<div className="relative w-32 h-48 rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-800 flex-shrink-0">
<Image src={detailData.poster} alt={detailData.title} fill className="object-cover" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">
{detailData.title}
</h3>
{detailData.originalTitle && detailData.originalTitle !== detailData.title && (
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3">
{detailData.originalTitle}
</p>
)}
{/* 评分 */}
{detailData.rating && (
<div className="flex items-center gap-2 mb-3">
<Star
size={20}
className="text-yellow-500 fill-yellow-500"
/>
<span className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{detailData.rating.value.toFixed(1)}
</span>
{detailData.rating.count > 0 && (
<span className="text-sm text-gray-500 dark:text-gray-400">
({detailData.rating.count} )
</span>
)}
</div>
)}
{/* 类型标签 */}
{detailData.genres && detailData.genres.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{detailData.genres.map((genre, index) => (
<span
key={index}
className="px-2 py-1 text-xs rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300"
>
{genre}
</span>
))}
</div>
)}
{/* 年份和时长 */}
<div className="flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-400">
{detailData.year && (
<div className="flex items-center gap-1">
<Calendar size={16} />
<span>{detailData.year}</span>
</div>
)}
{detailData.duration && (
<div className="flex items-center gap-1">
<Clock size={16} />
<span>{detailData.duration}</span>
</div>
)}
{detailData.episodesCount && (
<div className="flex items-center gap-1">
<Film size={16} />
<span>{detailData.episodesCount} </span>
</div>
)}
</div>
</div>
</div>
{/* 简介 */}
{(detailData.intro || detailData.overview) && (
<div className="mb-6">
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h4>
<p className="text-gray-700 dark:text-gray-300 leading-relaxed whitespace-pre-wrap">
{detailData.intro || detailData.overview}
</p>
</div>
)}
{/* 导演和演员 */}
{detailData.directors && detailData.directors.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2 flex items-center gap-2">
<Users size={16} />
</h4>
<p className="text-gray-700 dark:text-gray-300">
{detailData.directors.map((d) => d.name).join(', ')}
</p>
</div>
)}
{detailData.actors && detailData.actors.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2 flex items-center gap-2">
<Users size={16} />
</h4>
<p className="text-gray-700 dark:text-gray-300">
{detailData.actors.slice(0, 10).map((a) => a.name).join(', ')}
</p>
</div>
)}
{/* 制作信息 */}
<div className="grid grid-cols-2 gap-4 text-sm">
{detailData.countries && detailData.countries.length > 0 && (
<div>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-1 flex items-center gap-1">
<Globe size={14} />
/
</h4>
<p className="text-gray-700 dark:text-gray-300">
{detailData.countries.join(', ')}
</p>
</div>
)}
{detailData.languages && detailData.languages.length > 0 && (
<div>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-1 flex items-center gap-1">
<Tag size={14} />
</h4>
<p className="text-gray-700 dark:text-gray-300">
{detailData.languages.join(', ')}
</p>
</div>
)}
{detailData.releaseDate && (
<div>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-1 flex items-center gap-1">
<Calendar size={14} />
</h4>
<p className="text-gray-700 dark:text-gray-300">{detailData.releaseDate}</p>
</div>
)}
{detailData.status && (
<div>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-1"></h4>
<p className="text-gray-700 dark:text-gray-300">{detailData.status}</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
return createPortal(content, document.body);
};
export default DetailPanel;

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */
import { ExternalLink, Heart, Link, PlayCircleIcon, Radio, Sparkles, Trash2 } from 'lucide-react';
import { ExternalLink, Heart, Info, Link, PlayCircleIcon, Radio, Sparkles, Trash2 } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import React, {
@@ -27,6 +27,7 @@ import { useLongPress } from '@/hooks/useLongPress';
import { ImagePlaceholder } from '@/components/ImagePlaceholder';
import MobileActionSheet from '@/components/MobileActionSheet';
import AIChatPanel from '@/components/AIChatPanel';
import DetailPanel from '@/components/DetailPanel';
export interface VideoCardProps {
id?: string;
@@ -56,6 +57,11 @@ export interface VideoCardProps {
orientation?: 'vertical' | 'horizontal'; // 卡片方向
playTime?: number; // 当前播放时间(秒)
totalTime?: number; // 总时长(秒)
cmsData?: {
desc?: string;
episodes?: string[];
episodes_titles?: string[];
};
}
export type VideoCardHandle = {
@@ -93,6 +99,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
orientation = 'vertical',
playTime,
totalTime,
cmsData,
}: VideoCardProps,
ref
) {
@@ -103,6 +110,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
const [searchFavorited, setSearchFavorited] = useState<boolean | null>(null); // 搜索结果的收藏状态
const [showAIChat, setShowAIChat] = useState(false);
const [aiEnabled, setAiEnabled] = useState(false);
const [showDetailPanel, setShowDetailPanel] = useState(false);
// 检查AI功能是否启用
useEffect(() => {
@@ -553,6 +561,21 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
});
}
// 详情页面按钮
actions.push({
id: 'detail',
label: '详情',
icon: <Info size={20} />,
onClick: () => {
setShowMobileActions(false);
// 延迟打开 DetailPanel确保 MobileActionSheet 完全清理完成
setTimeout(() => {
setShowDetailPanel(true);
}, 250);
},
color: 'default' as const,
});
// AI问片功能
if (aiEnabled && actualTitle) {
actions.push({
@@ -1167,7 +1190,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
)}
{/* 来源 - 右侧 */}
{config.showSourceName && source_name && (
{config.showSourceName && source_name && !cmsData && (
<span
className={`inline-block border rounded px-1 py-0.5 text-[8px] text-white/90 bg-black/30 backdrop-blur-sm ${
actualSource === 'openlist' ? 'border-yellow-500' : 'border-white/60'
@@ -1216,7 +1239,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
)}
{/* 直播时只显示来源 */}
{origin === 'live' && config.showSourceName && source_name && (
{origin === 'live' && config.showSourceName && source_name && !cmsData && (
<div className='flex items-center justify-end'>
<span
className={`inline-block border rounded px-1 py-0.5 text-[8px] text-white/90 bg-black/30 backdrop-blur-sm ${
@@ -1334,7 +1357,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
></div>
</div>
</div>
{config.showSourceName && source_name && (
{config.showSourceName && source_name && !cmsData && (
<span
className='block text-xs text-gray-500 dark:text-gray-400 mt-1'
style={{
@@ -1380,7 +1403,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
actions={mobileActions}
sources={isAggregate && dynamicSourceNames ? Array.from(new Set(dynamicSourceNames)) : undefined}
isAggregate={isAggregate}
sourceName={source_name}
sourceName={cmsData ? undefined : source_name}
currentEpisode={currentEpisode}
totalEpisodes={actualEpisodes}
origin={origin}
@@ -1402,6 +1425,25 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
welcomeMessage={`想了解《${actualTitle}》的更多信息吗?我可以帮你查询剧情、演员、评价等。`}
/>
)}
{/* 详情面板 */}
{showDetailPanel && (
<DetailPanel
isOpen={showDetailPanel}
onClose={() => setShowDetailPanel(false)}
title={actualTitle}
poster={processImageUrl(actualPoster)}
doubanId={actualDoubanId}
bangumiId={isBangumi ? actualDoubanId : undefined}
isBangumi={isBangumi}
tmdbId={tmdb_id}
type={actualSearchType as 'movie' | 'tv'}
seasonNumber={seasonNumber}
cmsData={cmsData}
sourceId={id}
source={source}
/>
)}
</>
);
}