| 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; |
|
|