mirror of
https://github.com/JustAnimeCore/JustAnime.git
synced 2026-04-18 06:11:45 +00:00
watch page details
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
# Your AniWatch API URL - replace with your own API endpoint
|
||||
ANIWATCH_API=https://your-api-url.com
|
||||
ANIWATCH_API=https://your-api-url.com/api/v2/hianime
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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,18 +90,74 @@ export default function WatchPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchVideoData = async () => {
|
||||
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 ${currentEpisodeId} (dub: ${isDub})`);
|
||||
console.log(`[Watch] Fetching video for episode ${episodeId} (dub: ${dub}, server: ${server})`);
|
||||
|
||||
// Fetch the episode sources from the API
|
||||
const data = await fetchEpisodeSources(currentEpisodeId, isDub);
|
||||
const data = await fetchEpisodeSources(episodeId, dub, server);
|
||||
|
||||
console.log('[Watch] Episode API response:', data);
|
||||
console.log('[Watch] Episode sources API response:', data);
|
||||
setEpisodeData(data);
|
||||
|
||||
if (!data || !data.sources || data.sources.length === 0) {
|
||||
@@ -104,10 +174,14 @@ export default function WatchPage() {
|
||||
"Referer": "https://hianime.to/",
|
||||
"Origin": "https://hianime.to"
|
||||
};
|
||||
console.log('[Watch] No headers provided from API, using defaults:', defaultHeaders);
|
||||
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
|
||||
@@ -143,83 +217,156 @@ export default function WatchPage() {
|
||||
setIsRetrying(true);
|
||||
setTimeout(() => {
|
||||
console.log('[Watch] Executing retry...');
|
||||
fetchVideoData();
|
||||
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}`);
|
||||
|
||||
// Fetch basic anime info
|
||||
const animeData = await fetchAnimeInfo(animeId);
|
||||
|
||||
if (animeData) {
|
||||
console.log('[Watch] Anime info received:', animeData.title);
|
||||
setAnime(animeData);
|
||||
|
||||
// Find the current episode in the anime episode list
|
||||
if (animeData.episodes && animeData.episodes.length > 0) {
|
||||
console.log('[Watch] Episodes found:', animeData.episodes.length);
|
||||
|
||||
// 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));
|
||||
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
|
||||
// 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;
|
||||
|
||||
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 {
|
||||
// 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] 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));
|
||||
console.warn('[Watch] Current episode not found in episode list');
|
||||
}
|
||||
} else {
|
||||
console.warn('[Watch] No episodes found in anime data or episodes array is empty');
|
||||
}
|
||||
} 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 ? (
|
||||
<div className="h-full">
|
||||
<VideoPlayer
|
||||
key={`${currentEpisodeId}-${isDub}`}
|
||||
key={`${currentEpisodeId}-${isDub}-${selectedServer}`}
|
||||
src={videoSource}
|
||||
poster={anime?.image}
|
||||
headers={videoHeaders}
|
||||
@@ -317,7 +443,7 @@ export default function WatchPage() {
|
||||
{/* Video Controls - Slimmer and without container background */}
|
||||
<div className="flex flex-col gap-4 mt-6">
|
||||
{/* Audio and Playback Controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center justify-between gap-y-4">
|
||||
{/* Playback Settings */}
|
||||
<div className="flex items-center gap-4">
|
||||
<h3 className="text-white/80 text-sm font-medium">Playback Settings</h3>
|
||||
@@ -337,6 +463,30 @@ export default function WatchPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Selection */}
|
||||
{availableServers.length > 0 && (
|
||||
<div className="flex items-center gap-4">
|
||||
<h3 className="text-white/80 text-sm font-medium">Servers</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{availableServers.map((server) =>
|
||||
server.serverName ? (
|
||||
<button
|
||||
key={`${server.serverName}-${server.serverId}`}
|
||||
onClick={() => handleServerChange(server.serverName.toLowerCase())}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
||||
selectedServer === server.serverName.toLowerCase()
|
||||
? 'bg-white text-black'
|
||||
: 'bg-white/5 text-gray-400 hover:text-white hover:bg-white/10 ring-1 ring-white/10'
|
||||
}`}
|
||||
>
|
||||
{server.serverName}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio Toggle */}
|
||||
<div className="flex items-center gap-4">
|
||||
<h3 className="text-white/80 text-sm font-medium">Audio</h3>
|
||||
@@ -367,7 +517,7 @@ export default function WatchPage() {
|
||||
|
||||
{/* Episode Navigation */}
|
||||
<div className="flex gap-3">
|
||||
{anime?.episodes && (
|
||||
{episodes && episodes.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -461,13 +611,14 @@ export default function WatchPage() {
|
||||
{anime.genres && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{anime.genres.map((genre, index) => (
|
||||
<span
|
||||
<Link
|
||||
key={index}
|
||||
href={`/genres/${encodeURIComponent(genre.toLowerCase())}`}
|
||||
className="px-3 py-1 rounded-full bg-white/5 text-white text-sm
|
||||
hover:bg-white/10 transition-all cursor-pointer ring-1 ring-white/10"
|
||||
>
|
||||
{genre}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -480,18 +631,19 @@ export default function WatchPage() {
|
||||
|
||||
{/* Right Side - Episode List (30%) */}
|
||||
<div className="w-full md:w-[30%]">
|
||||
{anime?.episodes ? (
|
||||
{episodes && episodes.length > 0 ? (
|
||||
<div className="h-full max-h-[calc(100vh-2rem)] overflow-hidden">
|
||||
<EpisodeList
|
||||
episodes={anime.episodes}
|
||||
episodes={episodes}
|
||||
currentEpisode={currentEpisode}
|
||||
onEpisodeClick={handleEpisodeClick}
|
||||
isDub={isDub}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white/5 rounded-2xl shadow-2xl p-6 ring-1 ring-white/10">
|
||||
<div className="text-center text-gray-400">
|
||||
No episodes available
|
||||
{isLoading ? 'Loading episodes...' : 'No episodes available'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,8 +9,8 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
||||
|
||||
// Update active episode when currentEpisode changes
|
||||
useEffect(() => {
|
||||
if (currentEpisode?.episodeId) {
|
||||
setActiveEpisodeId(currentEpisode.episodeId);
|
||||
if (currentEpisode?.id) {
|
||||
setActiveEpisodeId(currentEpisode.id);
|
||||
}
|
||||
}, [currentEpisode]);
|
||||
|
||||
@@ -20,11 +20,15 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/watch\/(.+)$/);
|
||||
if (match) {
|
||||
const episodeId = match[1];
|
||||
setActiveEpisodeId(episodeId);
|
||||
const urlEpisodeId = match[1];
|
||||
setActiveEpisodeId(urlEpisodeId);
|
||||
|
||||
// Find the episode and update page
|
||||
const episode = episodes.find(ep => ep.episodeId === episodeId);
|
||||
// Compare with both ?ep= format and plain format
|
||||
const episode = episodes.find(ep => {
|
||||
return normalizeEpisodeId(ep.id) === normalizeEpisodeId(urlEpisodeId);
|
||||
});
|
||||
|
||||
if (episode) {
|
||||
const pageNumber = Math.ceil(episode.number / episodesPerPage);
|
||||
setCurrentPage(pageNumber);
|
||||
@@ -48,6 +52,23 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
||||
};
|
||||
}, [episodes, episodesPerPage]);
|
||||
|
||||
// Helper function to normalize episode IDs for comparison
|
||||
const normalizeEpisodeId = (id) => {
|
||||
if (!id) return '';
|
||||
|
||||
// If it's already in ?ep= format
|
||||
if (id.includes('?ep=')) return id;
|
||||
|
||||
// If it's in anime-name-number format
|
||||
const match = id.match(/^(.*?)-(\d+)$/);
|
||||
if (match) {
|
||||
const [, animeId, episodeNumber] = match;
|
||||
return `${animeId}?ep=${episodeNumber}`;
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const filteredEpisodes = useMemo(() => {
|
||||
if (!searchQuery) return episodes;
|
||||
const query = searchQuery.toLowerCase();
|
||||
@@ -69,24 +90,27 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
||||
};
|
||||
|
||||
const isCurrentEpisode = (episode) => {
|
||||
return episode.episodeId === activeEpisodeId;
|
||||
if (!episode || !episode.id || !activeEpisodeId) return false;
|
||||
return normalizeEpisodeId(episode.id) === normalizeEpisodeId(activeEpisodeId);
|
||||
};
|
||||
|
||||
const handleEpisodeSelect = (episode, e) => {
|
||||
e.preventDefault();
|
||||
if (onEpisodeClick) {
|
||||
onEpisodeClick(episode.episodeId);
|
||||
if (onEpisodeClick && episode.id) {
|
||||
onEpisodeClick(episode.id);
|
||||
}
|
||||
setActiveEpisodeId(episode.episodeId);
|
||||
setActiveEpisodeId(episode.id);
|
||||
};
|
||||
|
||||
// Scroll active episode into view when page changes or active episode changes
|
||||
useEffect(() => {
|
||||
if (activeEpisodeId) {
|
||||
setTimeout(() => {
|
||||
const activeElement = document.querySelector(`[data-episode-id="${activeEpisodeId}"]`);
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [activeEpisodeId, currentPage]);
|
||||
|
||||
@@ -160,7 +184,7 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
||||
{currentEpisodes.map((episode) => (
|
||||
<button
|
||||
key={episode.number}
|
||||
data-episode-id={episode.episodeId}
|
||||
data-episode-id={episode.id}
|
||||
onClick={(e) => handleEpisodeSelect(episode, e)}
|
||||
className={`group relative ${
|
||||
isCurrentEpisode(episode)
|
||||
@@ -191,7 +215,7 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
||||
{currentEpisodes.map((episode) => (
|
||||
<button
|
||||
key={episode.number}
|
||||
data-episode-id={episode.episodeId}
|
||||
data-episode-id={episode.id}
|
||||
onClick={(e) => handleEpisodeSelect(episode, e)}
|
||||
className={`group flex items-center gap-3 py-2 px-3 rounded-lg transition-all duration-300 w-full text-left ${
|
||||
isCurrentEpisode(episode)
|
||||
|
||||
@@ -242,34 +242,54 @@ export default function VideoPlayer({ src, poster, headers = {}, subtitles = [],
|
||||
const isHlsStream = src.includes('.m3u8') || src.includes('application/vnd.apple.mpegurl');
|
||||
|
||||
if (isHlsStream && Hls.isSupported()) {
|
||||
console.log('[VideoPlayer] HLS is supported, initializing');
|
||||
|
||||
hls = new Hls({
|
||||
xhrSetup: (xhr) => {
|
||||
xhrSetup: (xhr, url) => {
|
||||
console.log('[VideoPlayer] HLS XHR setup for URL:', url);
|
||||
|
||||
// Set headers for HLS requests
|
||||
Object.entries(videoHeaders).forEach(([key, value]) => {
|
||||
xhr.setRequestHeader(key, value);
|
||||
console.log(`[VideoPlayer] Setting header: ${key}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
// Additional HLS settings for better performance
|
||||
maxBufferLength: 30,
|
||||
maxMaxBufferLength: 60,
|
||||
startLevel: -1, // Auto level selection
|
||||
capLevelToPlayerSize: true, // Limit quality based on player size
|
||||
debug: false
|
||||
});
|
||||
|
||||
window.hls = hls; // Save reference for debugging
|
||||
|
||||
// Bind HLS to video element
|
||||
hls.loadSource(getProxiedUrl(src));
|
||||
const proxiedSrc = getProxiedUrl(src);
|
||||
console.log('[VideoPlayer] Loading proxied source:', proxiedSrc);
|
||||
hls.loadSource(proxiedSrc);
|
||||
hls.attachMedia(video);
|
||||
|
||||
// Handle HLS events
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('[VideoPlayer] HLS manifest parsed');
|
||||
// Get available qualities
|
||||
const levels = hls.levels.map((level, index) => ({
|
||||
id: index,
|
||||
label: `${level.height}p`,
|
||||
height: level.height,
|
||||
selected: index === hls.currentLevel
|
||||
}));
|
||||
|
||||
console.log('[VideoPlayer] Available qualities:', levels);
|
||||
setQualities(levels);
|
||||
setCurrentQuality(hls.currentLevel);
|
||||
|
||||
// Auto-play when ready
|
||||
if (isPlaying) {
|
||||
video.play().catch(console.error);
|
||||
video.play().catch(err => {
|
||||
console.error('[VideoPlayer] Autoplay error:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -614,7 +634,20 @@ export default function VideoPlayer({ src, poster, headers = {}, subtitles = [],
|
||||
track.kind = 'subtitles';
|
||||
track.label = activeSubtitle.label || activeSubtitle.lang || 'Default';
|
||||
track.srclang = activeSubtitle.lang || 'en';
|
||||
track.src = activeSubtitle.src || activeSubtitle.url;
|
||||
|
||||
// Format subtitle URL correctly - it might be in different formats
|
||||
let subtitleUrl = activeSubtitle.src || activeSubtitle.url;
|
||||
|
||||
// Some subtitle URLs might need proxying if they're from a different origin
|
||||
if (subtitleUrl && (subtitleUrl.startsWith('http://') || subtitleUrl.startsWith('https://'))) {
|
||||
const proxyBase = process.env.NEXT_PUBLIC_CORSPROXY_URL || '';
|
||||
if (proxyBase && !subtitleUrl.includes(window.location.host)) {
|
||||
subtitleUrl = `${proxyBase}/subtitle-proxy?url=${encodeURIComponent(subtitleUrl)}`;
|
||||
console.log('[VideoPlayer] Proxying subtitle URL:', subtitleUrl);
|
||||
}
|
||||
}
|
||||
|
||||
track.src = subtitleUrl;
|
||||
track.default = true;
|
||||
|
||||
console.log('[VideoPlayer] Adding track on mount with src:', track.src);
|
||||
@@ -636,13 +669,22 @@ export default function VideoPlayer({ src, poster, headers = {}, subtitles = [],
|
||||
if (!videoRef.current || !subtitle) return;
|
||||
|
||||
// Ensure we have valid URL
|
||||
const subtitleUrl = subtitle.src || subtitle.url;
|
||||
let subtitleUrl = subtitle.src || subtitle.url;
|
||||
if (!subtitleUrl) {
|
||||
console.error('[VideoPlayer] No valid URL found in subtitle:', JSON.stringify(subtitle));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[VideoPlayer] Subtitle URL:', subtitleUrl);
|
||||
// Some subtitle URLs might need proxying if they're from a different origin
|
||||
if (subtitleUrl && (subtitleUrl.startsWith('http://') || subtitleUrl.startsWith('https://'))) {
|
||||
const proxyBase = process.env.NEXT_PUBLIC_CORSPROXY_URL || '';
|
||||
if (proxyBase && !subtitleUrl.includes(window.location.host)) {
|
||||
subtitleUrl = `${proxyBase}/subtitle-proxy?url=${encodeURIComponent(subtitleUrl)}`;
|
||||
console.log('[VideoPlayer] Proxying subtitle URL:', subtitleUrl);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[VideoPlayer] Final subtitle URL:', subtitleUrl);
|
||||
|
||||
// Remove all existing tracks
|
||||
const video = videoRef.current;
|
||||
|
||||
131
src/lib/api.js
131
src/lib/api.js
@@ -436,14 +436,133 @@ function createFallbackAnimeData(id) {
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchEpisodeSources = async (episodeId, dub = false) => {
|
||||
export const fetchAnimeEpisodes = async (animeId) => {
|
||||
try {
|
||||
if (!animeId) {
|
||||
console.error('Invalid anime ID provided');
|
||||
return { episodes: [] };
|
||||
}
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/anime/${encodeURIComponent(animeId)}/episodes`;
|
||||
console.log(`[API Call] Fetching episodes for anime: ${animeId}`);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: API_HEADERS,
|
||||
credentials: 'omit'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch episodes: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API Response] Episodes count:', data?.data?.episodes?.length || 0);
|
||||
|
||||
if (!data || !data.data) {
|
||||
console.error('[API Error] Empty response received for episodes');
|
||||
return { episodes: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
episodes: data.data.episodes || [],
|
||||
totalEpisodes: data.data.totalEpisodes || 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching anime episodes:', error);
|
||||
return { episodes: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchEpisodeServers = async (episodeId) => {
|
||||
try {
|
||||
if (!episodeId || episodeId === 'undefined') {
|
||||
console.error('Invalid episode ID provided');
|
||||
return { servers: [] };
|
||||
}
|
||||
|
||||
// Make sure the episodeId is properly formatted
|
||||
// If it has a number suffix without ?ep= format, reformat it
|
||||
let formattedEpisodeId = episodeId;
|
||||
if (!episodeId.includes('?ep=')) {
|
||||
// Extract the anime ID and episode number
|
||||
const match = episodeId.match(/^(.*?)-(\d+)$/);
|
||||
if (match) {
|
||||
const [, animeId, episodeNumber] = match;
|
||||
formattedEpisodeId = `${animeId}?ep=${episodeNumber}`;
|
||||
console.log(`[API] Reformatted episode ID from ${episodeId} to ${formattedEpisodeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/episode/servers?animeEpisodeId=${encodeURIComponent(formattedEpisodeId)}`;
|
||||
console.log(`[API Call] Fetching servers from: ${apiUrl}`);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: API_HEADERS,
|
||||
credentials: 'omit'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch episode servers: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API Response] Episode servers:', data);
|
||||
|
||||
if (!data || !data.success || !data.data) {
|
||||
console.error('[API Error] Empty response received for episode servers');
|
||||
return { servers: [] };
|
||||
}
|
||||
|
||||
// Get all servers from the response (sub, dub, raw)
|
||||
// The response has separate arrays for sub, dub, and raw servers
|
||||
const subServers = data.data.sub || [];
|
||||
const dubServers = data.data.dub || [];
|
||||
const rawServers = data.data.raw || [];
|
||||
|
||||
// Combine all servers into a single array for easier handling
|
||||
const allServers = [
|
||||
...subServers.map(s => ({ ...s, category: 'sub' })),
|
||||
...dubServers.map(s => ({ ...s, category: 'dub' })),
|
||||
...rawServers.map(s => ({ ...s, category: 'raw' }))
|
||||
];
|
||||
|
||||
return {
|
||||
servers: allServers,
|
||||
episodeId: data.data.episodeId,
|
||||
episodeNo: data.data.episodeNo,
|
||||
hasSubServers: subServers.length > 0,
|
||||
hasDubServers: dubServers.length > 0,
|
||||
hasRawServers: rawServers.length > 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching episode servers:', error);
|
||||
return { servers: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchEpisodeSources = async (episodeId, dub = false, server = 'hd-2') => {
|
||||
try {
|
||||
if (!episodeId || episodeId === 'undefined') {
|
||||
console.error('Invalid episode ID provided');
|
||||
return { sources: [] };
|
||||
}
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/episode/sources?animeEpisodeId=${episodeId}&category=${dub ? 'dub' : 'sub'}`;
|
||||
// Make sure the episodeId is properly formatted
|
||||
// If it has a number suffix without ?ep= format, reformat it
|
||||
let formattedEpisodeId = episodeId;
|
||||
if (!episodeId.includes('?ep=')) {
|
||||
// Extract the anime ID and episode number
|
||||
const match = episodeId.match(/^(.*?)-(\d+)$/);
|
||||
if (match) {
|
||||
const [, animeId, episodeNumber] = match;
|
||||
formattedEpisodeId = `${animeId}?ep=${episodeNumber}`;
|
||||
console.log(`[API] Reformatted episode ID from ${episodeId} to ${formattedEpisodeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
const category = dub ? 'dub' : 'sub';
|
||||
const serverName = server || 'hd-2'; // Default to hd-2 if server is null or empty
|
||||
const apiUrl = `${API_BASE_URL}/episode/sources?animeEpisodeId=${encodeURIComponent(formattedEpisodeId)}&category=${category}&server=${serverName}`;
|
||||
console.log(`[API Call] Fetching sources from: ${apiUrl}`);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
@@ -458,15 +577,17 @@ export const fetchEpisodeSources = async (episodeId, dub = false) => {
|
||||
const data = await response.json();
|
||||
console.log('[API Response] Raw data:', data);
|
||||
|
||||
if (!data || !data.data) {
|
||||
if (!data || !data.success || !data.data) {
|
||||
console.error('[API Error] Empty response received');
|
||||
return { sources: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
sources: data.data.sources,
|
||||
sources: data.data.sources || [],
|
||||
headers: data.data.headers || { "Referer": "https://hianime.to/" },
|
||||
subtitles: data.data.subtitles || []
|
||||
subtitles: data.data.subtitles || [],
|
||||
anilistID: data.data.anilistID || null,
|
||||
malID: data.data.malID || null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching episode sources:', error);
|
||||
|
||||
Reference in New Issue
Block a user