diff --git a/.env.example b/.env.example index c556dbc..42e7c0b 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ # Your AniWatch API URL - replace with your own API endpoint -ANIWATCH_API=https://your-api-url.com \ No newline at end of file +ANIWATCH_API=https://your-api-url.com/api/v2/hianime \ No newline at end of file diff --git a/src/app/watch/[episodeId]/page.js b/src/app/watch/[episodeId]/page.js index b0a5a56..c80eda1 100644 --- a/src/app/watch/[episodeId]/page.js +++ b/src/app/watch/[episodeId]/page.js @@ -6,7 +6,12 @@ import Link from 'next/link'; import Image from 'next/image'; import VideoPlayer from '@/components/VideoPlayer'; import EpisodeList from '@/components/EpisodeList'; -import { fetchEpisodeSources, fetchAnimeInfo } from '@/lib/api'; +import { + fetchEpisodeSources, + fetchAnimeInfo, + fetchEpisodeServers, + fetchAnimeEpisodes +} from '@/lib/api'; export default function WatchPage() { const { episodeId } = useParams(); @@ -29,6 +34,9 @@ export default function WatchPage() { 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(() => { @@ -59,16 +67,22 @@ export default function WatchPage() { // Log the raw episodeId from the URL for debugging console.log('[Watch] Raw episodeId from URL:', episodeId); - // The URL might contain query parameters for episode number - const [baseId, queryParams] = episodeId.split('?'); - console.log('[Watch] Base ID:', baseId); + // Extract animeId from the episodeId parameter + // The new format is: anime-name?ep=episode-number + const [baseId, queryString] = episodeId.split('?'); + + if (baseId) { + setAnimeId(baseId); + console.log('[Watch] Extracted anime ID:', baseId); + } else { + console.warn('[Watch] Could not extract anime ID from episode ID:', episodeId); + } - setAnimeId(baseId); setCurrentEpisodeId(episodeId); } }, [episodeId]); - // Fetch episode sources first to ensure we have data even if anime info fails + // First fetch episode servers to get available servers and subtitles useEffect(() => { if (!currentEpisodeId || currentEpisodeId === 'undefined') { setError('Invalid episode ID'); @@ -76,150 +90,283 @@ export default function WatchPage() { return; } - const fetchVideoData = async () => { + const fetchServers = async () => { setIsLoading(true); - setError(null); - setVideoSource(null); try { - console.log(`[Watch] Fetching video for episode ${currentEpisodeId} (dub: ${isDub})`); + console.log(`[Watch] Fetching servers for episode ${currentEpisodeId}`); - // Fetch the episode sources from the API - const data = await fetchEpisodeSources(currentEpisodeId, isDub); + // Fetch available servers from the API + const data = await fetchEpisodeServers(currentEpisodeId); - console.log('[Watch] Episode 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); + if (!data || !data.servers || data.servers.length === 0) { + console.warn('[Watch] No servers available for this episode'); } else { - // Set default headers if none provided - const defaultHeaders = { - "Referer": "https://hianime.to/", - "Origin": "https://hianime.to" - }; - console.log('[Watch] No headers provided from API, using defaults:', defaultHeaders); - setVideoHeaders(defaultHeaders); + // 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}`); + } } - // 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); + // Continue to fetch video sources with the selected server + fetchVideoSources(currentEpisodeId, isDub, selectedServer); } 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...'); - fetchVideoData(); - }, 2000); - } + 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 + if (data.subtitles && data.subtitles.length > 0) { + setSubtitles(data.subtitles); + } + + // 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); + } + } + }; - fetchVideoData(); - }, [currentEpisodeId, isDub, isRetrying]); + // Effect to refetch sources when server or dub changes + useEffect(() => { + if (currentEpisodeId && selectedServer) { + fetchVideoSources(currentEpisodeId, isDub, selectedServer); + } + }, [selectedServer, isDub]); - // Fetch anime info using extracted animeId + // 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}`); - const animeData = await fetchAnimeInfo(animeId); + // Fetch basic anime info + const animeData = await fetchAnimeInfo(animeId); if (animeData) { - console.log('[Watch] Anime info received:', animeData.title); - setAnime(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 the current episode in the anime episode list - if (animeData.episodes && animeData.episodes.length > 0) { - console.log('[Watch] Episodes found:', animeData.episodes.length); + // Find current episode in episode list + // Handle both formats: anime-name?ep=episode-number or anime-name-episode-number + const findCurrentEpisode = () => { + // Extract episode number from the URL + const [, queryString] = currentEpisodeId.split('?'); + let currentEpisodeNumber; - // First try exact match - let episode = animeData.episodes.find(ep => ep.id === episodeId); - - // If not found, try to find by checking if episodeId is contained in ep.id - if (!episode && episodeId.includes('$episode$')) { - const episodeIdPart = episodeId.split('$episode$')[1]; - episode = animeData.episodes.find(ep => ep.id.includes(episodeIdPart)); - } - - if (episode) { - setCurrentEpisode(episode); - console.log('[Watch] Current episode found:', episode.number); + if (queryString && queryString.startsWith('ep=')) { + // If it's in the format anime-name?ep=episode-number + currentEpisodeNumber = queryString.replace('ep=', ''); + console.log('[Watch] Current episode number from ?ep= format:', currentEpisodeNumber); } else { - console.warn('[Watch] Current episode not found in episode list. Looking for:', episodeId); - console.log('[Watch] First few episodes:', animeData.episodes.slice(0, 3).map(ep => ep.id)); + // If it's in the format anime-name-episode-number + const match = currentEpisodeId.match(/-(\d+)$/); + if (match && match[1]) { + currentEpisodeNumber = match[1]; + console.log('[Watch] Current episode number from dash format:', currentEpisodeNumber); + } } + + if (currentEpisodeNumber) { + // Try to find the episode by number + return episodesData.episodes.find(ep => + ep.number && ep.number.toString() === currentEpisodeNumber.toString() + ); + } + + // If no match by number, try to match by full ID + return episodesData.episodes.find(ep => ep.id === currentEpisodeId); + }; + + const episode = findCurrentEpisode(); + if (episode) { + setCurrentEpisode(episode); + console.log('[Watch] Current episode found:', episode.number); } else { - console.warn('[Watch] No episodes found in anime data or episodes array is empty'); + console.warn('[Watch] Current episode not found in episode list'); } } else { - console.error('[Watch] Failed to fetch anime info or received empty response'); + console.warn('[Watch] No episodes found for this anime'); } } catch (error) { - console.error('[Watch] Error fetching anime info:', error); + console.error('[Watch] Error fetching anime details:', error); } finally { setIsRetrying(false); } }; fetchAnimeDetails(); - } else { - console.warn('[Watch] No animeId available to fetch anime details'); } - }, [animeId, episodeId]); + }, [animeId, currentEpisodeId]); const handleDubToggle = () => { - setIsDub(!isDub); + 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) { // Update the URL using history API - const newUrl = `/watch/${newEpisodeId}`; + const newUrl = `/watch/${encodeURIComponent(newEpisodeId)}`; window.history.pushState({ episodeId: newEpisodeId }, '', newUrl); // Update state to trigger video reload setCurrentEpisodeId(newEpisodeId); // Update current episode in state - if (anime?.episodes) { - const newEpisode = anime.episodes.find(ep => ep.id === newEpisodeId); + if (episodes) { + const newEpisode = episodes.find(ep => ep.id === newEpisodeId); if (newEpisode) { setCurrentEpisode(newEpisode); } @@ -227,36 +374,15 @@ export default function WatchPage() { } }; - const handleRetryAnimeInfo = () => { - if (animeId) { - setIsRetrying(true); - fetchAnimeInfo(animeId) - .then(data => { - if (data) { - setAnime(data); - console.log('[Watch] Anime info retry succeeded:', data.title); - } else { - console.error('[Watch] Anime info retry failed: empty response'); - } - }) - .catch(error => { - console.error('[Watch] Anime info retry error:', error); - }) - .finally(() => { - setIsRetrying(false); - }); - } - }; - const findAdjacentEpisodes = () => { - if (!anime?.episodes || !currentEpisodeId) return { prev: null, next: null }; + if (!episodes || !currentEpisode) return { prev: null, next: null }; - const currentIndex = anime.episodes.findIndex(ep => ep.id === currentEpisodeId); + const currentIndex = episodes.findIndex(ep => ep.number === currentEpisode.number); if (currentIndex === -1) return { prev: null, next: null }; return { - prev: currentIndex > 0 ? anime.episodes[currentIndex - 1] : null, - next: currentIndex < anime.episodes.length - 1 ? anime.episodes[currentIndex + 1] : null + prev: currentIndex > 0 ? episodes[currentIndex - 1] : null, + next: currentIndex < episodes.length - 1 ? episodes[currentIndex + 1] : null }; }; @@ -288,7 +414,7 @@ export default function WatchPage() { ) : videoSource ? (