|
import React, { useEffect, useState, useRef } from 'react'; |
|
import { getMovieLinkByTitle, getMovieCard } from '../lib/api'; |
|
import { useToast } from '@/hooks/use-toast'; |
|
import VideoPlayer from './VideoPlayer'; |
|
import VideoPlayerControls from './VideoPlayerControls'; |
|
import { Loader2, Play } from 'lucide-react'; |
|
import { MovieCardData } from './ContentCard'; |
|
|
|
interface ProgressData { |
|
status: string; |
|
progress: number; |
|
downloaded: number; |
|
total: number; |
|
} |
|
|
|
interface MoviePlayerProps { |
|
movieTitle: string; |
|
videoUrl?: string; |
|
contentRatings?: any[]; |
|
poster?: string; |
|
startTime?: number; |
|
onClosePlayer?: () => void; |
|
onProgressUpdate?: (currentTime: number, duration: number) => void; |
|
onVideoEnded?: () => void; |
|
showNextButton?: boolean; |
|
} |
|
|
|
const MoviePlayer: React.FC<MoviePlayerProps> = ({ |
|
movieTitle, |
|
videoUrl, |
|
contentRatings, |
|
poster, |
|
startTime = 0, |
|
onClosePlayer, |
|
onProgressUpdate, |
|
onVideoEnded, |
|
showNextButton = false |
|
}) => { |
|
const [videoUrlState, setVideoUrlState] = useState<string | null>(videoUrl || null); |
|
const [loading, setLoading] = useState(!videoUrl); |
|
const [error, setError] = useState<string | null>(null); |
|
const [progress, setProgress] = useState<ProgressData | null>(null); |
|
const [videoFetched, setVideoFetched] = useState(!!videoUrl); |
|
const [cardData, setCardData] = useState<MovieCardData | null>(null); |
|
const [selectedImage, setSelectedImage] = useState<string | null>(null); |
|
const [imageLoaded, setImageLoaded] = useState(false); |
|
const { toast } = useToast(); |
|
|
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null); |
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null); |
|
const videoFetchedRef = useRef(!!videoUrl); |
|
const [ratingInfo, setRatingInfo] = useState<{ rating: string; description: string } | null>(null); |
|
const [currentTime, setCurrentTime] = useState(startTime); |
|
const containerRef = useRef<HTMLDivElement>(null); |
|
const videoRef = useRef<HTMLVideoElement>(null); |
|
|
|
|
|
useEffect(() => { |
|
setImageLoaded(false); |
|
}, [selectedImage]); |
|
|
|
|
|
const handleProgressUpdate = (time: number, duration: number) => { |
|
setCurrentTime(time); |
|
onProgressUpdate?.(time, duration); |
|
}; |
|
|
|
|
|
const handleSeek = (time: number) => { |
|
if (videoRef.current) { |
|
videoRef.current.currentTime = time; |
|
setCurrentTime(time); |
|
} |
|
}; |
|
|
|
|
|
const selectRandomImage = (card: MovieCardData | null) => { |
|
if (!card) return null; |
|
if (card.banner && card.banner.length > 0) { |
|
return card.banner[Math.floor(Math.random() * card.banner.length)].image; |
|
} |
|
if (card.portrait && card.portrait.length > 0) { |
|
return card.portrait[Math.floor(Math.random() * card.portrait.length)].image; |
|
} |
|
return card.image; |
|
}; |
|
|
|
|
|
const fetchMovieLink = async () => { |
|
if (videoFetchedRef.current || videoUrlState) return; |
|
|
|
try { |
|
const response = await getMovieLinkByTitle(movieTitle); |
|
if (response.url) { |
|
pollingIntervalRef.current && clearInterval(pollingIntervalRef.current); |
|
setVideoUrlState(response.url); |
|
setVideoFetched(true); |
|
videoFetchedRef.current = true; |
|
setLoading(false); |
|
} else if (response.progress_url) { |
|
if (!pollingIntervalRef.current) { |
|
pollingIntervalRef.current = setInterval(async () => { |
|
try { |
|
const res = await fetch(response.progress_url!); |
|
const data = await res.json(); |
|
setProgress(data.progress); |
|
if (data.progress.progress >= 100) { |
|
clearInterval(pollingIntervalRef.current!); |
|
timeoutRef.current = setTimeout(fetchMovieLink, 5000); |
|
} |
|
} catch (e) { |
|
console.error(e); |
|
} |
|
}, 2000); |
|
} |
|
} else { |
|
throw new Error('No URL or progress URL'); |
|
} |
|
} catch (e) { |
|
console.error('Error fetching movie link:', e); |
|
setError('Failed to load video'); |
|
toast({ title: 'Error', description: 'Could not load the video', variant: 'destructive' }); |
|
setLoading(false); |
|
} |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
const fetchCard = async () => { |
|
try { |
|
const movieData = await getMovieCard(movieTitle); |
|
setCardData(movieData); |
|
const img = selectRandomImage(movieData); |
|
setSelectedImage(img); |
|
|
|
if (!poster) { |
|
poster = movieData.image || poster; |
|
} |
|
|
|
const ratings = contentRatings && contentRatings.length > 0 |
|
? contentRatings |
|
: movieData.content_ratings || []; |
|
if (ratings.length) { |
|
const us = ratings.find((r: any) => r.country === 'usa') || ratings[0]; |
|
setRatingInfo({ rating: us.name || 'NR', description: us.description || '' }); |
|
} |
|
} catch (e) { |
|
console.error('Failed to fetch movie card:', e); |
|
} |
|
}; |
|
fetchCard(); |
|
}, [movieTitle, contentRatings, poster]); |
|
|
|
|
|
useEffect(() => { |
|
if (!videoUrlState) { |
|
fetchMovieLink(); |
|
} else { |
|
setVideoFetched(true); |
|
videoFetchedRef.current = true; |
|
setLoading(false); |
|
} |
|
return () => { |
|
pollingIntervalRef.current && clearInterval(pollingIntervalRef.current); |
|
timeoutRef.current && clearTimeout(timeoutRef.current); |
|
}; |
|
}, [movieTitle, videoUrlState]); |
|
|
|
|
|
useEffect(() => { |
|
if (videoUrlState) setLoading(false); |
|
}, [videoUrlState]); |
|
|
|
|
|
if (error) { |
|
return ( |
|
<div className="fixed inset-0 z-50 bg-black flex flex-col items-center justify-center"> |
|
<div className="text-4xl mb-4 text-theme-error">😢</div> |
|
<h2 className="text-2xl font-bold mb-2 text-white">Error Playing Movie</h2> |
|
<p className="text-gray-400 mb-6">{error}</p> |
|
<button |
|
onClick={onClosePlayer} |
|
className="px-6 py-2 bg-theme-primary hover:bg-theme-primary-hover rounded font-medium transition-colors text-white" |
|
> |
|
Back to Browse |
|
</button> |
|
</div> |
|
); |
|
} |
|
|
|
|
|
if (loading || !videoFetched || !videoUrlState) { |
|
return ( |
|
<> |
|
<div className="relative w-full h-full"> |
|
<div className="absolute inset-0"> |
|
<img |
|
src={selectedImage} |
|
onLoad={() => setImageLoaded(true)} |
|
onError={(e) => { |
|
(e.target as HTMLImageElement).src = '/placeholder.svg'; |
|
}} |
|
className={`w-full h-full object-cover transition-opacity duration-700 ease-in-out ${ |
|
imageLoaded ? 'opacity-100' : 'opacity-0' |
|
}`} |
|
/> |
|
<div className="absolute inset-0 bg-gradient-to-t from-netflix-black via-netflix-black/50 to-transparent" /> |
|
<div className="absolute inset-0 bg-gradient-to-r from-netflix-black/80 via-netflix-black/40 to-transparent" /> |
|
</div> |
|
</div> |
|
<div className="fixed inset-0 z-50 flex flex-col items-center backdrop-blur-sm justify-center"> |
|
<div className="text-center max-w-md px-6"> |
|
<div className="mb-6 flex justify-center"> |
|
{poster ? ( |
|
<img src={poster} alt={movieTitle} className="h-auto w-24 rounded-lg shadow-lg" /> |
|
) : ( |
|
<div className="flex items-center justify-center h-24 w-24 bg-theme-primary/20 rounded-lg"> |
|
<Play className="h-12 w-12 text-theme-primary" /> |
|
</div> |
|
)} |
|
</div> |
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-4"> |
|
{progress && progress.progress < 100 |
|
? `Preparing "${movieTitle}"` |
|
: `Loading "${movieTitle}"` |
|
} |
|
</h2> |
|
{progress ? ( |
|
<> |
|
<p className="text-gray-300 mb-4"> |
|
{progress.progress < 5 |
|
? 'Initializing your stream...' |
|
: progress.progress < 100 |
|
? 'Your stream is being prepared.' |
|
: 'Almost ready! Starting playback soon...'} |
|
</p> |
|
<div className="relative w-full h-2 bg-gray-800 rounded-full overflow-hidden mb-2"> |
|
<div |
|
className="absolute top-0 left-0 h-full bg-gradient-to-r from-theme-primary to-theme-primary-light transition-all duration-300" |
|
style={{ width: `${Math.min(100, Math.max(0, progress.progress))}%` }} |
|
/> |
|
</div> |
|
<p className="text-sm text-gray-400"> |
|
{Math.round(progress.progress)}% complete |
|
</p> |
|
</> |
|
) : ( |
|
<div className="flex justify-center"> |
|
<Loader2 className="h-8 w-8 animate-spin text-theme-primary" /> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</> |
|
); |
|
} |
|
|
|
|
|
return ( |
|
<div ref={containerRef} className="fixed inset-0 h-screen w-screen overflow-hidden"> |
|
<VideoPlayer |
|
url={videoUrlState} |
|
title={movieTitle} |
|
poster={selectedImage || undefined} |
|
startTime={startTime} |
|
onClose={onClosePlayer} |
|
onProgressUpdate={handleProgressUpdate} |
|
onVideoEnded={onVideoEnded} |
|
showNextButton={showNextButton} |
|
contentRating={ratingInfo} |
|
containerRef={containerRef} |
|
videoRef={videoRef} |
|
/> |
|
<VideoPlayerControls |
|
title={movieTitle} |
|
currentTime={currentTime} |
|
duration={videoRef.current?.duration || 0} |
|
onSeek={handleSeek} |
|
/> |
|
</div> |
|
); |
|
}; |
|
|
|
export default MoviePlayer; |
|
|