web / frontend /src /pages /TvShowPlayerPage.tsx
Chandima Prabhath
update
beeb302
raw
history blame
14.1 kB
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<string | null>(null);
const [showInfo, setShowInfo] = useState<any>(null);
const [showName, setShowName] = useState<string>('');
const [seasons, setSeasons] = useState<Season[]>([]);
const [selectedSeason, setSelectedSeason] = useState<string>('');
const [selectedEpisode, setSelectedEpisode] = useState<string>('');
const [activeEpisodeIndex, setActiveEpisodeIndex] = useState<number>(0);
const [activeSeasonIndex, setActiveSeasonIndex] = useState<number>(0);
const [showEpisodeSelector, setShowEpisodeSelector] = useState(false);
const [playbackProgress, setPlaybackProgress] = useState<PlaybackProgress>({});
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 (
<div className="flex items-center justify-center min-h-screen bg-black">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-red-600"></div>
</div>
);
}
// To force reload of video component when changing episodes
const tvShowPlayerKey = `${selectedSeason}-${selectedEpisode}-${needsReload ? 'reload' : 'loaded'}`;
return (
<div className="min-h-screen bg-black relative overflow-hidden">
{/* Episodes panel */}
{showEpisodeSelector && (
<div className="fixed inset-0 z-40 bg-black/80" onClick={() => setShowEpisodeSelector(false)}>
<div
className="fixed right-0 top-0 bottom-0 w-full md:w-1/3 lg:w-1/4 bg-gray-900 z-50 overflow-y-auto"
onClick={e => e.stopPropagation()}
>
<EpisodesPanel
seasons={seasons}
selectedSeason={selectedSeason}
selectedEpisode={selectedEpisode}
playbackProgress={playbackProgress}
onSelectEpisode={handleSelectEpisode}
onClose={() => setShowEpisodeSelector(false)}
showTitle={showName || 'Episodes'}
/>
</div>
</div>
)}
{/* TV Show Player component with key to force reload */}
<TVShowPlayer
key={tvShowPlayerKey}
videoTitle={title || ''}
season={selectedSeason}
episode={selectedEpisode}
movieTitle={title || ''}
contentRatings={showInfo?.data?.contentRatings || []}
startTime={getStartTime()}
onClosePlayer={handleBack}
onProgressUpdate={handleProgressUpdate}
onVideoEnded={loadNextEpisode}
onShowEpisodes={() => setShowEpisodeSelector(true)}
/>
</div>
);
};
export default TvShowPlayerPage;