import React, { useEffect, useState } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { getTvShowMetadata } from '../lib/api'; import { useToast } from '@/hooks/use-toast'; import TVShowPlayer from '../components/TVShowPlayer'; import EpisodesPanel from '../components/EpisodesPanel'; interface FileStructureItem { type: string; path: string; contents?: FileStructureItem[]; size?: number; } interface Episode { episode_number: number; name: string; overview: string; still_path: string; air_date: string; runtime: number; fileName?: string; // The actual file name with extension } interface Season { season_number: number; name: string; episodes: Episode[]; } interface PlaybackProgress { [key: string]: { currentTime: number; duration: number; lastPlayed: string; completed: boolean; }; } const TvShowPlayerPage = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showInfo, setShowInfo] = useState(null); const [showName, setShowName] = useState(''); const [seasons, setSeasons] = useState([]); const [selectedSeason, setSelectedSeason] = useState(''); const [selectedEpisode, setSelectedEpisode] = useState(''); const [activeEpisodeIndex, setActiveEpisodeIndex] = useState(0); const [activeSeasonIndex, setActiveSeasonIndex] = useState(0); const [showEpisodeSelector, setShowEpisodeSelector] = useState(false); const [playbackProgress, setPlaybackProgress] = useState({}); const [needsReload, setNeedsReload] = useState(false); // Flag to trigger video reload const { title } = useParams<{ title: string }>(); const [searchParams] = useSearchParams(); const seasonParam = searchParams.get('season'); const episodeParam = searchParams.get('episode'); const navigate = useNavigate(); const { toast } = useToast(); // Helper function to extract episode info from file path const extractEpisodeInfoFromPath = (filePath: string): Episode | null => { const fileName = filePath.split('/').pop() || filePath; const episodeRegex = /S(\d+)E(\d+)\s*-\s*(.+?)(?=\.\w+$)/i; const match = fileName.match(episodeRegex); if (match) { const episodeNumber = parseInt(match[2], 10); const episodeName = match[3].trim(); const isHD = fileName.toLowerCase().includes('720p') || fileName.toLowerCase().includes('1080p') || fileName.toLowerCase().includes('hdtv'); return { episode_number: episodeNumber, name: episodeName, overview: '', still_path: '/placeholder.svg', air_date: '', runtime: isHD ? 24 : 22, fileName: fileName }; } return null; }; // Helper function to extract season info from directory path const getSeasonInfoFromPath = (path: string): { number: number, name: string } => { const seasonRegex = /Season\s*(\d+)/i; const specialsRegex = /Specials/i; if (specialsRegex.test(path)) { return { number: 0, name: 'Specials' }; } const match = path.match(seasonRegex); if (match) { return { number: parseInt(match[1], 10), name: `Season ${match[1]}` }; } return { number: 1, name: 'Season 1' }; }; // Process the file structure to extract seasons and episodes const processTvShowFileStructure = (fileStructure: any): Season[] => { if (!fileStructure || !fileStructure.contents) { return []; } const extractedSeasons: Season[] = []; // Find season directories const seasonDirectories = fileStructure.contents.filter( (item: FileStructureItem) => item.type === 'directory' ); seasonDirectories.forEach((seasonDir: FileStructureItem) => { if (!seasonDir.contents) return; const seasonInfo = getSeasonInfoFromPath(seasonDir.path); const episodesArr: Episode[] = []; // Process files in this season directory seasonDir.contents.forEach((item: FileStructureItem) => { if (item.type === 'file') { const episode = extractEpisodeInfoFromPath(item.path); if (episode) { episodesArr.push(episode); } } }); // Sort episodes by episode number episodesArr.sort((a, b) => a.episode_number - b.episode_number); if (episodesArr.length > 0) { extractedSeasons.push({ season_number: seasonInfo.number, name: seasonInfo.name, episodes: episodesArr }); } }); // Sort seasons by season number extractedSeasons.sort((a, b) => a.season_number - b.season_number); return extractedSeasons; }; // Select first available episode when none is specified const selectFirstAvailableEpisode = (seasons: Season[]) => { if (seasons.length === 0) return; // First try to find Season 1 const regularSeason = seasons.find(s => s.season_number === 1); // If not available, use the first available season (could be Specials/Season 0) const firstSeason = regularSeason || seasons[0]; if (firstSeason && firstSeason.episodes.length > 0) { setSelectedSeason(firstSeason.name); setSelectedEpisode(firstSeason.episodes[0].fileName || ''); setActiveSeasonIndex(seasons.indexOf(firstSeason)); setActiveEpisodeIndex(0); } }; // Load playback progress from localStorage const loadPlaybackProgress = () => { try { const storedProgress = localStorage.getItem(`playback-${title}`); if (storedProgress) { setPlaybackProgress(JSON.parse(storedProgress)); } } catch (error) { console.error("Failed to load playback progress:", error); } }; // Save playback progress to localStorage const savePlaybackProgress = (episodeId: string, currentTime: number, duration: number, completed: boolean = false) => { try { const newProgress = { ...playbackProgress, [episodeId]: { currentTime, duration, lastPlayed: new Date().toISOString(), completed } }; localStorage.setItem(`playback-${title}`, JSON.stringify(newProgress)); setPlaybackProgress(newProgress); } catch (error) { console.error("Failed to save playback progress:", error); } }; // Function to load the next episode and reset the video const loadNextEpisode = () => { if (!seasons.length) return; const currentSeason = seasons[activeSeasonIndex]; if (!currentSeason) return; // If there's another episode in the current season if (activeEpisodeIndex < currentSeason.episodes.length - 1) { const nextEpisode = currentSeason.episodes[activeEpisodeIndex + 1]; setSelectedEpisode(nextEpisode.fileName || ''); setActiveEpisodeIndex(activeEpisodeIndex + 1); setNeedsReload(true); // Flag to reload the video // Update URL without page reload navigate(`/tv-show/${encodeURIComponent(title || '')}/watch?season=${encodeURIComponent(currentSeason.name)}&episode=${encodeURIComponent(nextEpisode.fileName || '')}`, { replace: true }); toast({ title: "Playing Next Episode", description: `${nextEpisode.name}`, }); } // If there's another season available else if (activeSeasonIndex < seasons.length - 1) { const nextSeason = seasons[activeSeasonIndex + 1]; if (nextSeason.episodes.length > 0) { const firstEpisode = nextSeason.episodes[0]; setSelectedSeason(nextSeason.name); setSelectedEpisode(firstEpisode.fileName || ''); setActiveSeasonIndex(activeSeasonIndex + 1); setActiveEpisodeIndex(0); setNeedsReload(true); // Flag to reload the video // Update URL without page reload navigate(`/tv-show/${encodeURIComponent(title || '')}/watch?season=${encodeURIComponent(nextSeason.name)}&episode=${encodeURIComponent(firstEpisode.fileName || '')}`, { replace: true }); toast({ title: "Starting Next Season", description: `${nextSeason.name}: ${firstEpisode.name}`, }); } } else { toast({ title: "End of Series", description: "You've watched all available episodes.", }); } }; // Watch for changes in seasons or episodes selection useEffect(() => { if (seasons.length && selectedSeason && selectedEpisode) { // Find active season and episode indexes const seasonIndex = seasons.findIndex(s => s.name === selectedSeason); if (seasonIndex >= 0) { setActiveSeasonIndex(seasonIndex); const episodeIndex = seasons[seasonIndex].episodes.findIndex( e => e.fileName === selectedEpisode ); if (episodeIndex >= 0) { setActiveEpisodeIndex(episodeIndex); } } } }, [seasons, selectedSeason, selectedEpisode]); useEffect(() => { const fetchData = async () => { if (!title) return; try { setLoading(true); setError(null); // Load saved playback progress loadPlaybackProgress(); // Get TV show metadata first const showData = await getTvShowMetadata(title); setShowInfo(showData); console.log('TV Show Metadata:', showData); setShowName(showInfo?.data?.translations?.nameTranslations?.find((t: any) => t.language === 'eng')?.name || showInfo?.name || ''); // Process seasons and episodes from file structure if (showData && showData.file_structure) { const processedSeasons = processTvShowFileStructure(showData.file_structure); setSeasons(processedSeasons); // Set selected season and episode from URL params or select first available if (seasonParam && episodeParam) { setSelectedSeason(seasonParam); setSelectedEpisode(episodeParam); } else { selectFirstAvailableEpisode(processedSeasons); } } } catch (error) { console.error(`Error fetching metadata for ${title}:`, error); setError('Failed to load episode data'); toast({ title: "Error Loading Data", description: "Please try again later", variant: "destructive" }); } finally { setLoading(false); } }; fetchData(); }, [title, seasonParam, episodeParam, toast]); const handleBack = () => { navigate(`/tv-show/${encodeURIComponent(title || '')}`); }; const getEpisodeNumber = () => { if (!selectedEpisode) return "1"; const episodeMatch = selectedEpisode.match(/E(\d+)/i); return episodeMatch ? episodeMatch[1] : "1"; }; const handleSelectEpisode = (seasonName: string, episode: Episode) => { // Only reload if we're changing episodes if (selectedEpisode !== episode.fileName) { setSelectedSeason(seasonName); setSelectedEpisode(episode.fileName || ''); setNeedsReload(true); // Update URL navigate(`/tv-show/${encodeURIComponent(title || '')}/watch?season=${encodeURIComponent(seasonName)}&episode=${encodeURIComponent(episode.fileName || '')}`, { replace: true }); } setShowEpisodeSelector(false); }; const handleProgressUpdate = (currentTime: number, duration: number) => { if (!selectedEpisode || !title) return; const episodeId = `${selectedSeason}-${selectedEpisode}`; const isCompleted = (currentTime / duration) > 0.9; // Mark as completed if watched 90% savePlaybackProgress(episodeId, currentTime, duration, isCompleted); }; const getStartTime = () => { if (!selectedSeason || !selectedEpisode) return 0; const episodeId = `${selectedSeason}-${selectedEpisode}`; const progress = playbackProgress[episodeId]; if (progress && !progress.completed) { return progress.currentTime; } return 0; }; const episodeTitle = showInfo ? `${showInfo.data?.name} ${selectedSeason}E${getEpisodeNumber()}` : `Episode`; // Reset needs reload flag when video has been updated useEffect(() => { if (needsReload) { setNeedsReload(false); } }, [selectedEpisode, selectedSeason]); if (loading) { return (
); } // To force reload of video component when changing episodes const tvShowPlayerKey = `${selectedSeason}-${selectedEpisode}-${needsReload ? 'reload' : 'loaded'}`; return (
{/* Episodes panel */} {showEpisodeSelector && (
setShowEpisodeSelector(false)}>
e.stopPropagation()} > setShowEpisodeSelector(false)} showTitle={showName || 'Episodes'} />
)} {/* TV Show Player component with key to force reload */} setShowEpisodeSelector(true)} />
); }; export default TvShowPlayerPage;