/* eslint-disable @typescript-eslint/no-explicit-any,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */ import { ExternalLink, Heart, Info, Link, PlayCircleIcon, Radio, Sparkles, Trash2 } from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from 'react'; import { deleteFavorite, deletePlayRecord, generateStorageKey, isFavorited, saveFavorite, subscribeToDataUpdates, } from '@/lib/db.client'; import { processImageUrl } from '@/lib/utils'; 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; source?: string; title?: string; query?: string; poster?: string; episodes?: number; source_name?: string; source_names?: string[]; progress?: number; year?: string; from: 'playrecord' | 'favorite' | 'search' | 'douban' | 'tmdb' | 'source-search'; currentEpisode?: number; douban_id?: number; tmdb_id?: number; onDelete?: () => void; rate?: string; type?: string; isBangumi?: boolean; isAggregate?: boolean; origin?: 'vod' | 'live'; releaseDate?: string; // 上映日期,格式:YYYY-MM-DD isUpcoming?: boolean; // 是否为即将上映 seasonNumber?: number; // 季度编号 seasonName?: string; // 季度名称 orientation?: 'vertical' | 'horizontal'; // 卡片方向 playTime?: number; // 当前播放时间(秒) totalTime?: number; // 总时长(秒) cmsData?: { desc?: string; episodes?: string[]; episodes_titles?: string[]; }; } export type VideoCardHandle = { setEpisodes: (episodes?: number) => void; setSourceNames: (names?: string[]) => void; setDoubanId: (id?: number) => void; }; const VideoCard = forwardRef(function VideoCard( { id, title = '', query = '', poster = '', episodes, source, source_name, source_names, progress = 0, year, from, currentEpisode, douban_id, tmdb_id, onDelete, rate, type = '', isBangumi = false, isAggregate = false, origin = 'vod', releaseDate, isUpcoming = false, seasonNumber, seasonName, orientation = 'vertical', playTime, totalTime, cmsData, }: VideoCardProps, ref ) { const router = useRouter(); const [favorited, setFavorited] = useState(false); const [isLoading, setIsLoading] = useState(false); const [showMobileActions, setShowMobileActions] = useState(false); const [searchFavorited, setSearchFavorited] = useState(null); // 搜索结果的收藏状态 const [showAIChat, setShowAIChat] = useState(false); const [aiEnabled, setAiEnabled] = useState(false); const [showDetailPanel, setShowDetailPanel] = useState(false); // 检查AI功能是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = (window as any).RUNTIME_CONFIG?.AI_ENABLED && (window as any).RUNTIME_CONFIG?.AI_ENABLE_VIDEOCARD_ENTRY; setAiEnabled(enabled); } }, []); // 可外部修改的可控字段 const [dynamicEpisodes, setDynamicEpisodes] = useState( episodes ); const [dynamicSourceNames, setDynamicSourceNames] = useState( source_names ); const [dynamicDoubanId, setDynamicDoubanId] = useState( douban_id ); useEffect(() => { setDynamicEpisodes(episodes); }, [episodes]); useEffect(() => { setDynamicSourceNames(source_names); }, [source_names]); useEffect(() => { setDynamicDoubanId(douban_id); }, [douban_id]); useImperativeHandle(ref, () => ({ setEpisodes: (eps?: number) => setDynamicEpisodes(eps), setSourceNames: (names?: string[]) => setDynamicSourceNames(names), setDoubanId: (id?: number) => setDynamicDoubanId(id), })); const actualTitle = title; const actualPoster = poster; const actualSource = source; const actualId = id; const actualDoubanId = dynamicDoubanId; const actualEpisodes = dynamicEpisodes; const actualYear = year; const actualQuery = query || ''; const actualSearchType = isAggregate ? (actualEpisodes && actualEpisodes === 1 ? 'movie' : 'tv') : type; // 获取收藏状态(搜索结果页面不检查) useEffect(() => { if (from === 'douban' || from === 'search' || !actualSource || !actualId) return; const fetchFavoriteStatus = async () => { try { const fav = await isFavorited(actualSource, actualId); setFavorited(fav); } catch (err) { throw new Error('检查收藏状态失败'); } }; fetchFavoriteStatus(); // 监听收藏状态更新事件 const storageKey = generateStorageKey(actualSource, actualId); const unsubscribe = subscribeToDataUpdates( 'favoritesUpdated', (newFavorites: Record) => { // 检查当前项目是否在新的收藏列表中 const isNowFavorited = !!newFavorites[storageKey]; setFavorited(isNowFavorited); } ); return unsubscribe; }, [from, actualSource, actualId]); const handleToggleFavorite = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (from === 'douban' || !actualSource || !actualId) return; try { // 确定当前收藏状态 const currentFavorited = from === 'search' ? searchFavorited : favorited; if (currentFavorited) { // 如果已收藏,删除收藏 await deleteFavorite(actualSource, actualId); if (from === 'search') { setSearchFavorited(false); } else { setFavorited(false); } } else { // 如果未收藏,添加收藏 await saveFavorite(actualSource, actualId, { title: actualTitle, source_name: source_name || '', year: actualYear || '', cover: actualPoster, total_episodes: actualEpisodes ?? 1, save_time: Date.now(), }); if (from === 'search') { setSearchFavorited(true); } else { setFavorited(true); } } } catch (err) { throw new Error('切换收藏状态失败'); } }, [ from, actualSource, actualId, actualTitle, source_name, actualYear, actualPoster, actualEpisodes, favorited, searchFavorited, ] ); const handleDeleteRecord = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (from !== 'playrecord' || !actualSource || !actualId) return; try { await deletePlayRecord(actualSource, actualId); onDelete?.(); } catch (err) { throw new Error('删除播放记录失败'); } }, [from, actualSource, actualId, onDelete] ); const handleClick = useCallback(() => { // 即将上映的电影不跳转 if (isUpcoming) { return; } if (origin === 'live' && actualSource && actualId) { // 直播内容跳转到直播页面 const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`; router.push(url); } else if (from === 'douban' || from === 'tmdb' || (isAggregate && !actualSource && !actualId)) { const url = `/play?title=${encodeURIComponent(actualTitle.trim())}${actualYear ? `&year=${actualYear}` : '' }${actualSearchType ? `&stype=${actualSearchType}` : ''}${isAggregate ? '&prefer=true' : ''}${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''}`; router.push(url); } else if (actualSource && actualId) { const url = `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent( actualTitle )}${actualYear ? `&year=${actualYear}` : ''}${isAggregate ? '&prefer=true' : '' }${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : '' }${actualSearchType ? `&stype=${actualSearchType}` : ''}`; router.push(url); } }, [ isUpcoming, origin, from, actualSource, actualId, router, actualTitle, actualYear, isAggregate, actualQuery, actualSearchType, ]); // 新标签页播放处理函数 const handlePlayInNewTab = useCallback(() => { // 即将上映的电影不跳转 if (isUpcoming) { return; } if (origin === 'live' && actualSource && actualId) { // 直播内容跳转到直播页面 const url = `/live?source=${actualSource.replace('live_', '')}&id=${actualId.replace('live_', '')}`; window.open(url, '_blank'); } else if (from === 'douban' || from === 'tmdb' || (isAggregate && !actualSource && !actualId)) { const url = `/play?title=${encodeURIComponent(actualTitle.trim())}${actualYear ? `&year=${actualYear}` : ''}${actualSearchType ? `&stype=${actualSearchType}` : ''}${isAggregate ? '&prefer=true' : ''}${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : ''}`; window.open(url, '_blank'); } else if (actualSource && actualId) { const url = `/play?source=${actualSource}&id=${actualId}&title=${encodeURIComponent( actualTitle )}${actualYear ? `&year=${actualYear}` : ''}${isAggregate ? '&prefer=true' : '' }${actualQuery ? `&stitle=${encodeURIComponent(actualQuery.trim())}` : '' }${actualSearchType ? `&stype=${actualSearchType}` : ''}`; window.open(url, '_blank'); } }, [ isUpcoming, origin, from, actualSource, actualId, actualTitle, actualYear, isAggregate, actualQuery, actualSearchType, ]); // 检查搜索结果的收藏状态 const checkSearchFavoriteStatus = useCallback(async () => { if (from === 'search' && !isAggregate && actualSource && actualId && searchFavorited === null) { try { const fav = await isFavorited(actualSource, actualId); setSearchFavorited(fav); } catch (err) { setSearchFavorited(false); } } }, [from, isAggregate, actualSource, actualId, searchFavorited]); // 长按操作 const handleLongPress = useCallback(() => { // 即将上映的电影不显示操作菜单 if (isUpcoming) { return; } if (!showMobileActions) { // 防止重复触发 // 立即显示菜单,避免等待数据加载导致动画卡顿 setShowMobileActions(true); // 异步检查收藏状态,不阻塞菜单显示 if (from === 'search' && !isAggregate && actualSource && actualId && searchFavorited === null) { checkSearchFavoriteStatus(); } } }, [isUpcoming, showMobileActions, from, isAggregate, actualSource, actualId, searchFavorited, checkSearchFavoriteStatus]); // 长按手势hook const longPressProps = useLongPress({ onLongPress: handleLongPress, onClick: handleClick, // 保持点击播放功能 longPressDelay: 500, }); // 计算距离上映的天数(使用本地时区) const daysUntilRelease = useMemo(() => { if (!isUpcoming || !releaseDate) return null; // 获取今天的本地日期(午夜) const today = new Date(); const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; // 将日期字符串解析为本地时区的日期对象 // 使用 'YYYY-MM-DD' 格式直接构造,避免 UTC 解析问题 const [releaseYear, releaseMonth, releaseDay] = releaseDate.split('-').map(Number); const release = new Date(releaseYear, releaseMonth - 1, releaseDay); const [todayYear, todayMonth, todayDay] = todayStr.split('-').map(Number); const todayDate = new Date(todayYear, todayMonth - 1, todayDay); const diffTime = release.getTime() - todayDate.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; }, [isUpcoming, releaseDate]); const config = useMemo(() => { const configs = { playrecord: { showSourceName: true, showProgress: true, showPlayButton: true, showHeart: true, showCheckCircle: true, showDoubanLink: false, showRating: false, showYear: false, }, favorite: { showSourceName: true, showProgress: false, showPlayButton: true, showHeart: true, showCheckCircle: false, showDoubanLink: false, showRating: false, showYear: false, }, search: { showSourceName: true, showProgress: false, showPlayButton: true, showHeart: true, // 移动端菜单中需要显示收藏选项 showCheckCircle: false, showDoubanLink: true, // 移动端菜单中显示豆瓣链接 showRating: !!rate, showYear: true, }, douban: { showSourceName: false, showProgress: false, showPlayButton: !isUpcoming, // 即将上映不显示播放按钮 showHeart: false, showCheckCircle: false, showDoubanLink: false, showRating: !!rate, showYear: false, }, tmdb: { showSourceName: false, showProgress: false, showPlayButton: !isUpcoming, // 即将上映不显示播放按钮 showHeart: false, showCheckCircle: false, showDoubanLink: false, showRating: !!rate, showYear: false, }, 'source-search': { showSourceName: false, showProgress: false, showPlayButton: true, showHeart: true, showCheckCircle: false, showDoubanLink: true, showRating: !!rate, showYear: true, }, }; return configs[from] || configs.search; }, [from, isAggregate, douban_id, rate, isUpcoming]); // 移动端操作菜单配置 const mobileActions = useMemo(() => { const actions = []; // 播放操作 if (config.showPlayButton) { actions.push({ id: 'play', label: origin === 'live' ? '观看直播' : '播放', icon: , onClick: handleClick, color: 'primary' as const, }); // 新标签页播放 actions.push({ id: 'play-new-tab', label: origin === 'live' ? '新标签页观看' : '新标签页播放', icon: , onClick: handlePlayInNewTab, color: 'default' as const, }); } // 聚合源信息 - 直接在菜单中展示,不需要单独的操作项 // 收藏/取消收藏操作 if (config.showHeart && from !== 'douban' && from !== 'tmdb' && actualSource && actualId) { const currentFavorited = from === 'search' ? searchFavorited : favorited; if (from === 'search') { // 搜索结果:根据加载状态显示不同的选项 if (searchFavorited !== null) { // 已加载完成,显示实际的收藏状态 actions.push({ id: 'favorite', label: currentFavorited ? '取消收藏' : '添加收藏', icon: currentFavorited ? ( ) : ( ), onClick: () => { const mockEvent = { preventDefault: () => { }, stopPropagation: () => { }, } as React.MouseEvent; handleToggleFavorite(mockEvent); }, color: currentFavorited ? ('danger' as const) : ('default' as const), }); } else { // 正在加载中,显示占位项 actions.push({ id: 'favorite-loading', label: '收藏加载中...', icon: , onClick: () => { }, // 加载中时不响应点击 disabled: true, }); } } else { // 非搜索结果:直接显示收藏选项 actions.push({ id: 'favorite', label: currentFavorited ? '取消收藏' : '添加收藏', icon: currentFavorited ? ( ) : ( ), onClick: () => { const mockEvent = { preventDefault: () => { }, stopPropagation: () => { }, } as React.MouseEvent; handleToggleFavorite(mockEvent); }, color: currentFavorited ? ('danger' as const) : ('default' as const), }); } } // 删除播放记录操作 if (config.showCheckCircle && from === 'playrecord' && actualSource && actualId) { actions.push({ id: 'delete', label: '删除记录', icon: , onClick: () => { const mockEvent = { preventDefault: () => { }, stopPropagation: () => { }, } as React.MouseEvent; handleDeleteRecord(mockEvent); }, color: 'danger' as const, }); } // 豆瓣链接操作 if (config.showDoubanLink && actualDoubanId && actualDoubanId !== 0) { actions.push({ id: 'douban', label: isBangumi ? 'Bangumi 详情' : '豆瓣详情', icon: , onClick: () => { const url = isBangumi ? `https://bgm.tv/subject/${actualDoubanId.toString()}` : `https://movie.douban.com/subject/${actualDoubanId.toString()}`; window.open(url, '_blank', 'noopener,noreferrer'); }, color: 'default' as const, }); } // 详情页面按钮 actions.push({ id: 'detail', label: '详情', icon: , onClick: () => { setShowMobileActions(false); // 延迟打开 DetailPanel,确保 MobileActionSheet 完全清理完成 setTimeout(() => { setShowDetailPanel(true); }, 250); }, color: 'default' as const, }); // AI问片功能 if (aiEnabled && actualTitle) { actions.push({ id: 'ai-chat', label: 'AI问片', icon: , onClick: () => { setShowMobileActions(false); // 延迟打开 AIChatPanel,确保 MobileActionSheet 完全清理完成 setTimeout(() => { setShowAIChat(true); }, 250); }, color: 'default' as const, }); } return actions; }, [ config, from, actualSource, actualId, favorited, searchFavorited, actualDoubanId, isBangumi, isAggregate, dynamicSourceNames, handleClick, handleToggleFavorite, handleDeleteRecord, handlePlayInNewTab, aiEnabled, actualTitle, ]); return ( <>
{ // 阻止默认右键菜单 e.preventDefault(); e.stopPropagation(); // 即将上映的电影不显示操作菜单 if (isUpcoming) { return false; } // 右键弹出操作菜单 setShowMobileActions(true); // 异步检查收藏状态,不阻塞菜单显示 if (from === 'search' && !isAggregate && actualSource && actualId && searchFavorited === null) { checkSearchFavoriteStatus(); } return false; }} onDragStart={(e) => { // 阻止拖拽 e.preventDefault(); return false; }} > {/* 海报容器 */}
{ e.preventDefault(); return false; }} > {/* 骨架屏 */} {!isLoading && } {/* 图片 */} {actualTitle} setIsLoading(true)} onError={(e) => { // 图片加载失败时的重试机制 const img = e.target as HTMLImageElement; if (!img.dataset.retried) { img.dataset.retried = 'true'; setTimeout(() => { img.src = processImageUrl(actualPoster); }, 2000); } }} style={{ // 禁用图片的默认长按效果 WebkitUserSelect: 'none', userSelect: 'none', WebkitTouchCallout: 'none', pointerEvents: 'none', // 图片不响应任何指针事件 } as React.CSSProperties} onContextMenu={(e) => { e.preventDefault(); return false; }} onDragStart={(e) => { e.preventDefault(); return false; }} /> {/* 悬浮遮罩 */}
{ e.preventDefault(); return false; }} /> {/* 播放按钮或上映倒计时 */} {isUpcoming && daysUntilRelease !== null ? (
{ e.preventDefault(); return false; }} >
{daysUntilRelease > 0 ? `${daysUntilRelease}天后上映` : daysUntilRelease === 0 ? '今日上映' : '已上映'}
) : config.showPlayButton && (
{ e.preventDefault(); return false; }} > { e.preventDefault(); return false; }} />
)} {/* 操作按钮 - 继续观看不显示桌面端悬停按钮 */} {(config.showHeart || config.showCheckCircle) && from !== 'playrecord' && (
{ e.preventDefault(); return false; }} > {config.showCheckCircle && ( { e.preventDefault(); return false; }} /> )} {config.showHeart && from !== 'search' && ( { e.preventDefault(); return false; }} /> )}
)} {/* 季度徽章 */} {seasonNumber && (
{ e.preventDefault(); return false; }} title={seasonName || `第${seasonNumber}季`} > S{seasonNumber}
)} {/* 徽章 */} {config.showRating && rate && (
{ e.preventDefault(); return false; }} > {rate}
)} {actualEpisodes && actualEpisodes > 1 && orientation === 'vertical' && (
{ e.preventDefault(); return false; }} > {/* 集数显示 */}
{ e.preventDefault(); return false; }} > 共{actualEpisodes}集
{/* 年份显示 */} {actualYear && actualYear !== 'unknown' && actualYear.trim() !== '' && (
{ e.preventDefault(); return false; }} > {actualYear.slice(-2)}年
)}
)} {/* 豆瓣链接 */} {config.showDoubanLink && actualDoubanId && actualDoubanId !== 0 && ( e.stopPropagation()} className='absolute top-2 left-2 opacity-0 -translate-x-2 transition-all duration-300 ease-in-out delay-100 sm:group-hover:opacity-100 sm:group-hover:translate-x-0' style={{ WebkitUserSelect: 'none', userSelect: 'none', WebkitTouchCallout: 'none', } as React.CSSProperties} onContextMenu={(e) => { e.preventDefault(); return false; }} >
{ e.preventDefault(); return false; }} >
)} {/* 聚合播放源指示器 */} {isAggregate && dynamicSourceNames && dynamicSourceNames.length > 0 && (() => { const uniqueSources = Array.from(new Set(dynamicSourceNames)); const sourceCount = uniqueSources.length; return (
{ e.preventDefault(); return false; }} >
{ e.preventDefault(); return false; }} > {sourceCount}
{/* 播放源详情悬浮框 */} {(() => { // 优先显示的播放源(常见的主流平台) const prioritySources = ['爱奇艺', '腾讯视频', '优酷', '芒果TV', '哔哩哔哩', 'Netflix', 'Disney+']; // 按优先级排序播放源 const sortedSources = uniqueSources.sort((a, b) => { const aIndex = prioritySources.indexOf(a); const bIndex = prioritySources.indexOf(b); if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; if (aIndex !== -1) return -1; if (bIndex !== -1) return 1; return a.localeCompare(b); }); const maxDisplayCount = 6; // 最多显示6个 const displaySources = sortedSources.slice(0, maxDisplayCount); const hasMore = sortedSources.length > maxDisplayCount; const remainingCount = sortedSources.length - maxDisplayCount; return (
{ e.preventDefault(); return false; }} >
{ e.preventDefault(); return false; }} > {/* 单列布局 */}
{displaySources.map((sourceName, index) => (
{sourceName}
))}
{/* 显示更多提示 */} {hasMore && (
+{remainingCount} 播放源
)} {/* 小箭头 */}
); })()}
); })()} {/* 横向模式:标题和进度条在海报上 */} {orientation === 'horizontal' && ( <> {/* 顶部渐变遮罩 - 用于标题背景 */}
{ e.preventDefault(); return false; }} > {/* 标题 */}
{ e.preventDefault(); return false; }} title={actualTitle} > {actualTitle}
{/* 集数信息 - 只有超过1集时才显示 */} {currentEpisode && actualEpisodes && actualEpisodes > 1 && (
{ e.preventDefault(); return false; }} > 第{currentEpisode}集 · 共{actualEpisodes}集
)}
{/* 底部渐变遮罩 - 用于进度条背景 */}
{ e.preventDefault(); return false; }} > {/* 进度条 */} {config.showProgress && progress !== undefined && origin !== 'live' && (
{/* 来源和时长显示 - 在进度条上方 */}
{/* 时长显示 - 左侧 */} {from === 'playrecord' && playTime !== undefined && totalTime !== undefined && (
{ e.preventDefault(); return false; }} > {(() => { const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); // 0分钟时不显示分钟 if (mins === 0) { return `${secs}秒`; } return `${mins}分${secs}秒`; }; return formatTime(playTime); })()}
)} {/* 来源 - 右侧 */} {config.showSourceName && source_name && !cmsData && ( { e.preventDefault(); return false; }} > {source_name} )}
{ e.preventDefault(); return false; }} >
{ e.preventDefault(); return false; }} />
)} {/* 直播时只显示来源 */} {origin === 'live' && config.showSourceName && source_name && !cmsData && (
{ e.preventDefault(); return false; }} > {source_name}
)}
)}
{/* 竖向模式:进度条和标题在海报下方 */} {orientation === 'vertical' && ( <> {/* 进度条 */} {config.showProgress && progress !== undefined && (
{ e.preventDefault(); return false; }} >
{ e.preventDefault(); return false; }} />
)} {/* 标题与来源 */}
{ e.preventDefault(); return false; }} >
{ e.preventDefault(); return false; }} > {actualTitle} {/* 自定义 tooltip */}
{ e.preventDefault(); return false; }} > {actualTitle}
{config.showSourceName && source_name && !cmsData && ( { e.preventDefault(); return false; }} > { e.preventDefault(); return false; }} > {origin === 'live' && ( )} {source_name} )}
)}
{/* 操作菜单 - 支持右键和长按触发 */} setShowMobileActions(false)} title={actualTitle} poster={processImageUrl(actualPoster)} actions={mobileActions} sources={isAggregate && dynamicSourceNames ? Array.from(new Set(dynamicSourceNames)) : undefined} isAggregate={isAggregate} sourceName={cmsData ? undefined : source_name} currentEpisode={currentEpisode} totalEpisodes={actualEpisodes} origin={origin} /> {/* AI问片面板 */} {aiEnabled && showAIChat && ( setShowAIChat(false)} context={{ title: actualTitle, year: actualYear, douban_id: actualDoubanId, tmdb_id, type: actualSearchType as 'movie' | 'tv', currentEpisode, }} welcomeMessage={`想了解《${actualTitle}》的更多信息吗?我可以帮你查询剧情、演员、评价等。`} /> )} {/* 详情面板 */} {showDetailPanel && ( 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} /> )} ); } ); export default memo(VideoCard);