'use client'; import { useState, useEffect } from 'react'; import { useParams, useRouter, usePathname, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import Image from 'next/image'; import VideoPlayer from '@/components/VideoPlayer'; import EpisodeList from '@/components/EpisodeList'; import { fetchEpisodeSources, fetchAnimeInfo, fetchEpisodeServers, fetchAnimeEpisodes } from '@/lib/api'; export default function WatchPage() { const { episodeId } = useParams(); const router = useRouter(); const pathname = usePathname(); const [videoSource, setVideoSource] = useState(null); const [anime, setAnime] = useState(null); const [currentEpisode, setCurrentEpisode] = useState(null); const [isDub, setIsDub] = useState(false); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [videoHeaders, setVideoHeaders] = useState({}); const [subtitles, setSubtitles] = useState([]); const [thumbnails, setThumbnails] = useState(null); const [animeId, setAnimeId] = useState(null); const [episodeData, setEpisodeData] = useState(null); const [isRetrying, setIsRetrying] = useState(false); const [currentPage, setCurrentPage] = useState(1); const episodesPerPage = 100; const [showFullSynopsis, setShowFullSynopsis] = useState(false); const [autoSkip, setAutoSkip] = useState(false); const [currentEpisodeId, setCurrentEpisodeId] = useState(episodeId); const [availableServers, setAvailableServers] = useState([]); const [selectedServer, setSelectedServer] = useState('hd-2'); const [episodes, setEpisodes] = useState([]); // Handle URL updates when currentEpisodeId changes useEffect(() => { if (currentEpisodeId && currentEpisodeId !== episodeId) { const newUrl = `/watch/${currentEpisodeId}`; window.history.pushState({ episodeId: currentEpisodeId }, '', newUrl); } }, [currentEpisodeId, episodeId]); // Listen for popstate (browser back/forward) events useEffect(() => { const handlePopState = (event) => { const path = window.location.pathname; const match = path.match(/\/watch\/(.+)$/); if (match) { const newEpisodeId = match[1]; setCurrentEpisodeId(newEpisodeId); } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); // Extract animeId from the URL useEffect(() => { if (episodeId) { // Log the raw episodeId from the URL for debugging console.log('[Watch] Raw episodeId from URL:', episodeId); // Extract animeId from the episodeId parameter // The API response contains episode.id in the format "anime-id?ep=episode-number" let extractedAnimeId = episodeId; // If the ID contains a query parameter, extract just the anime ID if (episodeId.includes('?')) { extractedAnimeId = episodeId.split('?')[0]; } setAnimeId(extractedAnimeId); console.log('[Watch] Extracted anime ID:', extractedAnimeId); setCurrentEpisodeId(episodeId); } }, [episodeId]); // First fetch episode servers to get available servers and subtitles useEffect(() => { if (!currentEpisodeId || currentEpisodeId === 'undefined') { setError('Invalid episode ID'); setIsLoading(false); return; } const fetchServers = async () => { setIsLoading(true); try { console.log(`[Watch] Fetching servers for episode ${currentEpisodeId}`); // Fetch available servers from the API const data = await fetchEpisodeServers(currentEpisodeId); if (!data || !data.servers || data.servers.length === 0) { console.warn('[Watch] No servers available for this episode'); } else { // Filter servers based on current audio preference (sub/dub) const filteredServers = data.servers.filter(server => server.category === (isDub ? 'dub' : 'sub') ); setAvailableServers(filteredServers); console.log(`[Watch] Available ${isDub ? 'dub' : 'sub'} servers:`, filteredServers); // Set default server if available // First try to find HD-1 server let preferredServer = filteredServers.find(server => server.serverName && server.serverName.toLowerCase() === 'hd-2' ); // If not found, look for vidstreaming if (!preferredServer) { preferredServer = filteredServers.find(server => server.serverName && server.serverName.toLowerCase().includes('vidstreaming') ); } if (preferredServer && preferredServer.serverName) { setSelectedServer(preferredServer.serverName.toLowerCase()); console.log(`[Watch] Selected preferred server: ${preferredServer.serverName}`); } else if (filteredServers.length > 0 && filteredServers[0].serverName) { setSelectedServer(filteredServers[0].serverName.toLowerCase()); console.log(`[Watch] Selected first available server: ${filteredServers[0].serverName}`); } } // Continue to fetch video sources with the selected server fetchVideoSources(currentEpisodeId, isDub, selectedServer); } catch (error) { console.error('[Watch] Error fetching episode servers:', error); // Continue to sources even if servers fail fetchVideoSources(currentEpisodeId, isDub, selectedServer); } }; fetchServers(); }, [currentEpisodeId, isDub]); // Fetch video sources function const fetchVideoSources = async (episodeId, dub, server) => { setIsLoading(true); setError(null); setVideoSource(null); try { console.log(`[Watch] Fetching video for episode ${episodeId} (dub: ${dub}, server: ${server})`); // Fetch the episode sources from the API const data = await fetchEpisodeSources(episodeId, dub, server); console.log('[Watch] Episode sources API response:', data); setEpisodeData(data); if (!data || !data.sources || data.sources.length === 0) { throw new Error('No video sources available for this episode'); } // Extract headers if they exist in the response if (data.headers) { console.log('[Watch] Headers from API:', data.headers); setVideoHeaders(data.headers); } else { // Set default headers if none provided const defaultHeaders = { "Referer": "https://hianime.to/", "Origin": "https://hianime.to" }; setVideoHeaders(defaultHeaders); } // Set subtitles if available in the sources response // Check both subtitles and tracks fields since API might return either const subtitleData = data.subtitles || data.tracks || []; if (subtitleData.length > 0) { // Filter out thumbnails from subtitles array const filteredSubtitles = subtitleData.filter(sub => sub.lang && sub.lang.toLowerCase() !== 'thumbnails' ); // Look for thumbnails separately const thumbnailTrack = subtitleData.find(sub => sub.lang && sub.lang.toLowerCase() === 'thumbnails' ); if (thumbnailTrack && thumbnailTrack.url) { console.log('[Watch] Found thumbnails track:', thumbnailTrack.url); setThumbnails(thumbnailTrack.url); } if (filteredSubtitles.length > 0) { console.log('[Watch] Found subtitles:', filteredSubtitles.length); setSubtitles(filteredSubtitles); } } // Try to find the best source in order of preference // 1. HLS (m3u8) sources // 2. High quality MP4 sources const hlsSource = data.sources.find(src => src.isM3U8); const mp4Source = data.sources.find(src => !src.isM3U8); let selectedSource = null; if (hlsSource && hlsSource.url) { console.log('[Watch] Selected HLS source:', hlsSource.url); selectedSource = hlsSource.url; } else if (mp4Source && mp4Source.url) { console.log('[Watch] Selected MP4 source:', mp4Source.url); selectedSource = mp4Source.url; } else if (data.sources[0] && data.sources[0].url) { console.log('[Watch] Falling back to first available source:', data.sources[0].url); selectedSource = data.sources[0].url; } else { throw new Error('No valid video URLs found'); } setVideoSource(selectedSource); setIsLoading(false); } catch (error) { console.error('[Watch] Error fetching video sources:', error); setError(error.message || 'Failed to load video'); setIsLoading(false); // If this is the first try, attempt to retry once if (!isRetrying) { console.log('[Watch] First error, attempting retry...'); setIsRetrying(true); setTimeout(() => { console.log('[Watch] Executing retry...'); fetchVideoSources(episodeId, dub, server); }, 2000); } } }; // Effect to refetch sources when server or dub changes useEffect(() => { if (currentEpisodeId && selectedServer) { fetchVideoSources(currentEpisodeId, isDub, selectedServer); } }, [selectedServer, isDub]); // Fetch anime info and episodes using animeId useEffect(() => { if (animeId) { const fetchAnimeDetails = async () => { try { setIsRetrying(true); console.log(`[Watch] Fetching anime info for ID: ${animeId}`); // Fetch basic anime info const animeData = await fetchAnimeInfo(animeId); if (animeData) { console.log('[Watch] Anime info received:', animeData.info?.name); setAnime({ id: animeId, title: animeData.info?.name || 'Unknown Anime', image: animeData.info?.poster || '', description: animeData.info?.description || 'No description available', status: animeData.moreInfo?.status || 'Unknown', type: animeData.info?.stats?.type || 'TV', totalEpisodes: animeData.info?.stats?.episodes?.sub || 0, genres: animeData.moreInfo?.genres || [] }); } // Fetch episodes separately const episodesData = await fetchAnimeEpisodes(animeId); if (episodesData && episodesData.episodes && episodesData.episodes.length > 0) { console.log('[Watch] Episodes found:', episodesData.episodes.length); setEpisodes(episodesData.episodes); // Find current episode in episode list const findCurrentEpisode = () => { // Find the episode by direct ID match const directMatch = episodesData.episodes.find(ep => ep.id === currentEpisodeId); if (directMatch) { console.log('[Watch] Found episode by direct ID match:', directMatch.number); return directMatch; } // If no match found, return first episode as fallback console.warn('[Watch] Could not find matching episode, falling back to first episode'); return episodesData.episodes[0]; }; const episode = findCurrentEpisode(); if (episode) { setCurrentEpisode(episode); console.log('[Watch] Current episode found:', episode.number); } else { console.warn('[Watch] Current episode not found in episode list'); } } else { console.warn('[Watch] No episodes found for this anime'); } } catch (error) { console.error('[Watch] Error fetching anime details:', error); } finally { setIsRetrying(false); } }; fetchAnimeDetails(); } }, [animeId, currentEpisodeId]); const handleDubToggle = () => { setIsDub(prev => { const newDubState = !prev; // Refetch servers for the new audio type fetchEpisodeServers(currentEpisodeId).then(data => { if (data && data.servers && data.servers.length > 0) { // Filter servers based on new audio preference const filteredServers = data.servers.filter(server => server.category === (newDubState ? 'dub' : 'sub') ); setAvailableServers(filteredServers); // Update selected server if needed // First try to find HD-1 server let preferredServer = filteredServers.find(server => server.serverName && server.serverName.toLowerCase() === 'hd-2' ); // If not found, look for vidstreaming if (!preferredServer) { preferredServer = filteredServers.find(server => server.serverName && server.serverName.toLowerCase().includes('vidstreaming') ); } if (preferredServer && preferredServer.serverName) { setSelectedServer(preferredServer.serverName.toLowerCase()); console.log(`[Watch] Selected preferred server: ${preferredServer.serverName}`); } else if (filteredServers.length > 0 && filteredServers[0].serverName) { setSelectedServer(filteredServers[0].serverName.toLowerCase()); console.log(`[Watch] Selected first available server: ${filteredServers[0].serverName}`); } } }); return newDubState; }); }; const handleServerChange = (server) => { setSelectedServer(server); }; const handleEpisodeClick = (newEpisodeId) => { if (newEpisodeId !== currentEpisodeId) { console.log(`[Watch] Episode clicked, ID: ${newEpisodeId}`); // Use the episode ID directly as it should already be in the correct format // from the API response (animeId?ep=episodeNumber) // Update the URL using history API const newUrl = `/watch/${newEpisodeId}`; window.history.pushState({ episodeId: newEpisodeId }, '', newUrl); // Update state to trigger video reload setCurrentEpisodeId(newEpisodeId); // Update current episode in state if (episodes) { const newEpisode = episodes.find(ep => ep.id === newEpisodeId); if (newEpisode) { setCurrentEpisode(newEpisode); } } } }; const findAdjacentEpisodes = () => { if (!episodes || !currentEpisode) return { prev: null, next: null }; const currentIndex = episodes.findIndex(ep => ep.number === currentEpisode.number); if (currentIndex === -1) return { prev: null, next: null }; return { prev: currentIndex > 0 ? episodes[currentIndex - 1] : null, next: currentIndex < episodes.length - 1 ? episodes[currentIndex + 1] : null }; }; const { prev, next } = findAdjacentEpisodes(); return (
{/* Left Side - Video Player (70%) */}
{/* Video Player Container */}
{error ? (
Error: {error}

The video source couldn't be loaded. Please try again or check back later.

) : isLoading ? (
Loading video...
) : videoSource ? (
) : (
No video source available

Please try again or check back later.

)}
{/* Video Controls - Slimmer and without container background */}
{/* Audio and Playback Controls */}
{/* Playback Settings */}

Playback Settings

{/* Auto Skip Checkbox */} {(episodeData?.intro || episodeData?.outro) && ( )}
{/* Server Selection */} {availableServers.length > 0 && (

Servers

{availableServers.map((server) => server.serverName ? ( ) : null )}
)} {/* Audio Toggle */}

Audio

{/* Episode Navigation */}
{episodes && episodes.length > 0 && ( <> )}
{/* Anime Info Section */} {anime && (
{/* Cover Image */}
{anime.title}
{/* Details */}

{anime.title}

{/* Status Bar */}
{anime.status} {anime.type} {anime.totalEpisodes} Episodes
{/* Synopsis Section */}

Synopsis

{anime.description}
{/* Genres */} {anime.genres && (
{anime.genres.map((genre, index) => ( {genre} ))}
)}
)}
{/* Right Side - Episode List (30%) */}
{episodes && episodes.length > 0 ? (
) : (
{isLoading ? 'Loading episodes...' : 'No episodes available'}
)}
); }