import { Suspense } from 'react'; import VideoPlayer from './VideoPlayer'; import Link from 'next/link'; import WatchActions from './WatchActions'; import SubscribeButton from '../components/SubscribeButton'; import NextVideoClient from './NextVideoClient'; import WatchFeed from './WatchFeed'; import PlaylistPanel from './PlaylistPanel'; import Comments from './Comments'; import { API_BASE, VideoData } from '../constants'; import { cookies } from 'next/headers'; import { getRelatedVideos, getSuggestedVideos, getSearchVideos } from '../actions'; import { addRegion, getRandomModifier } from '../utils'; const REGION_LABELS: Record = { VN: 'Vietnam', US: 'United States', JP: 'Japan', KR: 'South Korea', IN: 'India', GB: 'United Kingdom', GLOBAL: '', }; interface VideoInfo { title: string; description: string; uploader: string; channel_id: string; view_count: number; thumbnail?: string; } async function getVideoInfo(id: string): Promise { try { const res = await fetch(`${API_BASE}/api/get_stream_info?v=${id}`, { cache: 'no-store' }); if (!res.ok) return null; const data = await res.json(); return { title: data.title || `Video ${id}`, description: data.description || '', uploader: data.uploader || 'Unknown', channel_id: data.channel_id || '', view_count: data.view_count || 0, thumbnail: data.thumbnail || `https://i.ytimg.com/vi/${id}/hqdefault.jpg`, }; } catch (e) { console.error(e); return null; } } function formatNumber(num: number): string { if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(0) + 'K'; return num.toString(); } export default async function WatchPage({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { const awaitParams = await searchParams; const v = awaitParams.v as string; const list = awaitParams.list as string; const isMix = list?.startsWith('RD'); if (!v) { return
No video ID provided
; } const info = await getVideoInfo(v); const cookieStore = await cookies(); const regionCode = cookieStore.get('region')?.value || 'VN'; const regionLabel = REGION_LABELS[regionCode] || ''; const randomMod = getRandomModifier(); // Fetch initial mix const promises = [ getSuggestedVideos(12), getRelatedVideos(v, 12), getSearchVideos(addRegion("trending", regionLabel) + ' ' + randomMod, 6) ]; const [suggestedRes, relatedRes, trendingRes] = await Promise.all(promises); const interleavedList: VideoData[] = []; const seenIds = new Set(); let sIdx = 0, rIdx = 0, tIdx = 0; while (sIdx < suggestedRes.length || rIdx < relatedRes.length || tIdx < trendingRes.length) { for (let i = 0; i < 2 && sIdx < suggestedRes.length; i++) { const vid = suggestedRes[sIdx++]; if (!seenIds.has(vid.id) && vid.id !== v) { interleavedList.push(vid); seenIds.add(vid.id); } } for (let i = 0; i < 2 && rIdx < relatedRes.length; i++) { const vid = relatedRes[rIdx++]; if (!seenIds.has(vid.id) && vid.id !== v) { interleavedList.push(vid); seenIds.add(vid.id); } } for (let i = 0; i < 1 && tIdx < trendingRes.length; i++) { const vid = trendingRes[tIdx++]; if (!seenIds.has(vid.id) && vid.id !== v) { interleavedList.push(vid); seenIds.add(vid.id); } } } let initialMix = interleavedList; const initialRelated = relatedRes.filter(vid => vid.id !== v); const initialSuggestions = suggestedRes.filter(vid => vid.id !== v); // If not currently inside a mix, inject a Mix Card at the start if (!isMix && info) { const mixCard: VideoData = { id: v, title: `Mix - ${info.uploader || 'Auto-generated'}`, uploader: info.uploader || 'KV-Tube', thumbnail: info.thumbnail || `https://i.ytimg.com/vi/${v}/hqdefault.jpg`, view_count: 0, duration: '50+', list_id: `RD${v}`, is_mix: true }; initialMix.unshift(mixCard); } // Always build a Mix playlist let playlistVideos: VideoData[] = []; const mixBaseId = isMix ? list.replace('RD', '') : v; const mixListId = isMix ? list : `RD${v}`; { const baseInfo = isMix ? await getVideoInfo(mixBaseId) : info; // Seed the playlist with the base video if (baseInfo) { playlistVideos.push({ id: mixBaseId, title: baseInfo.title, uploader: baseInfo.uploader, thumbnail: baseInfo.thumbnail || `https://i.ytimg.com/vi/${mixBaseId}/hqdefault.jpg`, view_count: baseInfo.view_count, duration: '' }); } // Multi-source search to build a rich playlist (15-25 videos) const uploaderName = baseInfo?.uploader || ''; const videoTitle = baseInfo?.title || ''; const titleKeywords = videoTitle.split(/[\s\-|]+/).filter((w: string) => w.length > 2).slice(0, 4).join(' '); const mixPromises = [ uploaderName ? getSearchVideos(uploaderName, 10) : Promise.resolve([]), titleKeywords ? getSearchVideos(titleKeywords, 10) : Promise.resolve([]), getRelatedVideos(mixBaseId, 10), ]; const [byUploader, byTitle, byRelated] = await Promise.all(mixPromises); const seenMixIds = new Set(playlistVideos.map(p => p.id)); const sources = [byUploader, byTitle, byRelated]; let added = 0; const maxPlaylist = 25; let idx = [0, 0, 0]; while (added < maxPlaylist) { let anyAdded = false; for (let s = 0; s < sources.length; s++) { while (idx[s] < sources[s].length && added < maxPlaylist) { const vid = sources[s][idx[s]++]; if (!seenMixIds.has(vid.id)) { seenMixIds.add(vid.id); playlistVideos.push(vid); added++; anyAdded = true; break; } } } if (!anyAdded) break; } } // Determine the next video let nextVideoId = ''; let nextListId: string | undefined = undefined; if (playlistVideos.length > 0) { const currentIndex = playlistVideos.findIndex(p => p.id === v); if (currentIndex >= 0 && currentIndex < playlistVideos.length - 1) { nextVideoId = playlistVideos[currentIndex + 1].id; nextListId = mixListId; } else { nextVideoId = initialMix.length > 0 && initialMix[0].is_mix ? (initialMix[1]?.id || '') : (initialMix[0]?.id || ''); } } else { const firstRealVideo = initialMix.find(vid => !vid.is_mix); nextVideoId = firstRealVideo ? firstRealVideo.id : ''; } return (
{nextVideoId && }

{info?.title || `Video ${v}`}

{info && (
{info.uploader}
)} {info && (
{formatNumber(info.view_count)} views
{info.description || 'No description available.'}
)}
{/* Comments as a separate flex child for responsive reordering */}
{playlistVideos.length > 0 && ( )}
); }