mirror of
https://github.com/JustAnimeCore/JustAnime.git
synced 2026-04-17 22:01: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
|
# 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 Image from 'next/image';
|
||||||
import VideoPlayer from '@/components/VideoPlayer';
|
import VideoPlayer from '@/components/VideoPlayer';
|
||||||
import EpisodeList from '@/components/EpisodeList';
|
import EpisodeList from '@/components/EpisodeList';
|
||||||
import { fetchEpisodeSources, fetchAnimeInfo } from '@/lib/api';
|
import {
|
||||||
|
fetchEpisodeSources,
|
||||||
|
fetchAnimeInfo,
|
||||||
|
fetchEpisodeServers,
|
||||||
|
fetchAnimeEpisodes
|
||||||
|
} from '@/lib/api';
|
||||||
|
|
||||||
export default function WatchPage() {
|
export default function WatchPage() {
|
||||||
const { episodeId } = useParams();
|
const { episodeId } = useParams();
|
||||||
@@ -29,6 +34,9 @@ export default function WatchPage() {
|
|||||||
const [showFullSynopsis, setShowFullSynopsis] = useState(false);
|
const [showFullSynopsis, setShowFullSynopsis] = useState(false);
|
||||||
const [autoSkip, setAutoSkip] = useState(false);
|
const [autoSkip, setAutoSkip] = useState(false);
|
||||||
const [currentEpisodeId, setCurrentEpisodeId] = useState(episodeId);
|
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
|
// Handle URL updates when currentEpisodeId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,16 +67,22 @@ export default function WatchPage() {
|
|||||||
// Log the raw episodeId from the URL for debugging
|
// Log the raw episodeId from the URL for debugging
|
||||||
console.log('[Watch] Raw episodeId from URL:', episodeId);
|
console.log('[Watch] Raw episodeId from URL:', episodeId);
|
||||||
|
|
||||||
// The URL might contain query parameters for episode number
|
// Extract animeId from the episodeId parameter
|
||||||
const [baseId, queryParams] = episodeId.split('?');
|
// The new format is: anime-name?ep=episode-number
|
||||||
console.log('[Watch] Base ID:', baseId);
|
const [baseId, queryString] = episodeId.split('?');
|
||||||
|
|
||||||
|
if (baseId) {
|
||||||
setAnimeId(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);
|
setCurrentEpisodeId(episodeId);
|
||||||
}
|
}
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!currentEpisodeId || currentEpisodeId === 'undefined') {
|
if (!currentEpisodeId || currentEpisodeId === 'undefined') {
|
||||||
setError('Invalid episode ID');
|
setError('Invalid episode ID');
|
||||||
@@ -76,18 +90,74 @@ export default function WatchPage() {
|
|||||||
return;
|
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);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setVideoSource(null);
|
setVideoSource(null);
|
||||||
|
|
||||||
try {
|
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
|
// 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);
|
setEpisodeData(data);
|
||||||
|
|
||||||
if (!data || !data.sources || data.sources.length === 0) {
|
if (!data || !data.sources || data.sources.length === 0) {
|
||||||
@@ -104,10 +174,14 @@ export default function WatchPage() {
|
|||||||
"Referer": "https://hianime.to/",
|
"Referer": "https://hianime.to/",
|
||||||
"Origin": "https://hianime.to"
|
"Origin": "https://hianime.to"
|
||||||
};
|
};
|
||||||
console.log('[Watch] No headers provided from API, using defaults:', defaultHeaders);
|
|
||||||
setVideoHeaders(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
|
// Try to find the best source in order of preference
|
||||||
// 1. HLS (m3u8) sources
|
// 1. HLS (m3u8) sources
|
||||||
// 2. High quality MP4 sources
|
// 2. High quality MP4 sources
|
||||||
@@ -143,83 +217,156 @@ export default function WatchPage() {
|
|||||||
setIsRetrying(true);
|
setIsRetrying(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('[Watch] Executing retry...');
|
console.log('[Watch] Executing retry...');
|
||||||
fetchVideoData();
|
fetchVideoSources(episodeId, dub, server);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchVideoData();
|
// Effect to refetch sources when server or dub changes
|
||||||
}, [currentEpisodeId, isDub, isRetrying]);
|
useEffect(() => {
|
||||||
|
if (currentEpisodeId && selectedServer) {
|
||||||
|
fetchVideoSources(currentEpisodeId, isDub, selectedServer);
|
||||||
|
}
|
||||||
|
}, [selectedServer, isDub]);
|
||||||
|
|
||||||
// Fetch anime info using extracted animeId
|
// Fetch anime info and episodes using animeId
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (animeId) {
|
if (animeId) {
|
||||||
const fetchAnimeDetails = async () => {
|
const fetchAnimeDetails = async () => {
|
||||||
try {
|
try {
|
||||||
setIsRetrying(true);
|
setIsRetrying(true);
|
||||||
console.log(`[Watch] Fetching anime info for ID: ${animeId}`);
|
console.log(`[Watch] Fetching anime info for ID: ${animeId}`);
|
||||||
|
|
||||||
|
// Fetch basic anime info
|
||||||
const animeData = await fetchAnimeInfo(animeId);
|
const animeData = await fetchAnimeInfo(animeId);
|
||||||
|
|
||||||
if (animeData) {
|
if (animeData) {
|
||||||
console.log('[Watch] Anime info received:', animeData.title);
|
console.log('[Watch] Anime info received:', animeData.info?.name);
|
||||||
setAnime(animeData);
|
setAnime({
|
||||||
|
id: animeId,
|
||||||
// Find the current episode in the anime episode list
|
title: animeData.info?.name || 'Unknown Anime',
|
||||||
if (animeData.episodes && animeData.episodes.length > 0) {
|
image: animeData.info?.poster || '',
|
||||||
console.log('[Watch] Episodes found:', animeData.episodes.length);
|
description: animeData.info?.description || 'No description available',
|
||||||
|
status: animeData.moreInfo?.status || 'Unknown',
|
||||||
// First try exact match
|
type: animeData.info?.stats?.type || 'TV',
|
||||||
let episode = animeData.episodes.find(ep => ep.id === episodeId);
|
totalEpisodes: animeData.info?.stats?.episodes?.sub || 0,
|
||||||
|
genres: animeData.moreInfo?.genres || []
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (episode) {
|
||||||
setCurrentEpisode(episode);
|
setCurrentEpisode(episode);
|
||||||
console.log('[Watch] Current episode found:', episode.number);
|
console.log('[Watch] Current episode found:', episode.number);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Watch] Current episode not found in episode list. Looking for:', episodeId);
|
console.warn('[Watch] Current episode not found in episode list');
|
||||||
console.log('[Watch] First few episodes:', animeData.episodes.slice(0, 3).map(ep => ep.id));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Watch] No episodes found in anime data or episodes array is empty');
|
console.warn('[Watch] No episodes found for this anime');
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('[Watch] Failed to fetch anime info or received empty response');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Watch] Error fetching anime info:', error);
|
console.error('[Watch] Error fetching anime details:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRetrying(false);
|
setIsRetrying(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAnimeDetails();
|
fetchAnimeDetails();
|
||||||
} else {
|
|
||||||
console.warn('[Watch] No animeId available to fetch anime details');
|
|
||||||
}
|
}
|
||||||
}, [animeId, episodeId]);
|
}, [animeId, currentEpisodeId]);
|
||||||
|
|
||||||
const handleDubToggle = () => {
|
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) => {
|
const handleEpisodeClick = (newEpisodeId) => {
|
||||||
if (newEpisodeId !== currentEpisodeId) {
|
if (newEpisodeId !== currentEpisodeId) {
|
||||||
// Update the URL using history API
|
// Update the URL using history API
|
||||||
const newUrl = `/watch/${newEpisodeId}`;
|
const newUrl = `/watch/${encodeURIComponent(newEpisodeId)}`;
|
||||||
window.history.pushState({ episodeId: newEpisodeId }, '', newUrl);
|
window.history.pushState({ episodeId: newEpisodeId }, '', newUrl);
|
||||||
|
|
||||||
// Update state to trigger video reload
|
// Update state to trigger video reload
|
||||||
setCurrentEpisodeId(newEpisodeId);
|
setCurrentEpisodeId(newEpisodeId);
|
||||||
|
|
||||||
// Update current episode in state
|
// Update current episode in state
|
||||||
if (anime?.episodes) {
|
if (episodes) {
|
||||||
const newEpisode = anime.episodes.find(ep => ep.id === newEpisodeId);
|
const newEpisode = episodes.find(ep => ep.id === newEpisodeId);
|
||||||
if (newEpisode) {
|
if (newEpisode) {
|
||||||
setCurrentEpisode(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 = () => {
|
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 };
|
if (currentIndex === -1) return { prev: null, next: null };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prev: currentIndex > 0 ? anime.episodes[currentIndex - 1] : null,
|
prev: currentIndex > 0 ? episodes[currentIndex - 1] : null,
|
||||||
next: currentIndex < anime.episodes.length - 1 ? anime.episodes[currentIndex + 1] : null
|
next: currentIndex < episodes.length - 1 ? episodes[currentIndex + 1] : null
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,7 +414,7 @@ export default function WatchPage() {
|
|||||||
) : videoSource ? (
|
) : videoSource ? (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
key={`${currentEpisodeId}-${isDub}`}
|
key={`${currentEpisodeId}-${isDub}-${selectedServer}`}
|
||||||
src={videoSource}
|
src={videoSource}
|
||||||
poster={anime?.image}
|
poster={anime?.image}
|
||||||
headers={videoHeaders}
|
headers={videoHeaders}
|
||||||
@@ -317,7 +443,7 @@ export default function WatchPage() {
|
|||||||
{/* Video Controls - Slimmer and without container background */}
|
{/* Video Controls - Slimmer and without container background */}
|
||||||
<div className="flex flex-col gap-4 mt-6">
|
<div className="flex flex-col gap-4 mt-6">
|
||||||
{/* Audio and Playback Controls */}
|
{/* 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 */}
|
{/* Playback Settings */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<h3 className="text-white/80 text-sm font-medium">Playback Settings</h3>
|
<h3 className="text-white/80 text-sm font-medium">Playback Settings</h3>
|
||||||
@@ -337,6 +463,30 @@ export default function WatchPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Audio Toggle */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<h3 className="text-white/80 text-sm font-medium">Audio</h3>
|
<h3 className="text-white/80 text-sm font-medium">Audio</h3>
|
||||||
@@ -367,7 +517,7 @@ export default function WatchPage() {
|
|||||||
|
|
||||||
{/* Episode Navigation */}
|
{/* Episode Navigation */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{anime?.episodes && (
|
{episodes && episodes.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -461,13 +611,14 @@ export default function WatchPage() {
|
|||||||
{anime.genres && (
|
{anime.genres && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{anime.genres.map((genre, index) => (
|
{anime.genres.map((genre, index) => (
|
||||||
<span
|
<Link
|
||||||
key={index}
|
key={index}
|
||||||
|
href={`/genres/${encodeURIComponent(genre.toLowerCase())}`}
|
||||||
className="px-3 py-1 rounded-full bg-white/5 text-white text-sm
|
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"
|
hover:bg-white/10 transition-all cursor-pointer ring-1 ring-white/10"
|
||||||
>
|
>
|
||||||
{genre}
|
{genre}
|
||||||
</span>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -480,18 +631,19 @@ export default function WatchPage() {
|
|||||||
|
|
||||||
{/* Right Side - Episode List (30%) */}
|
{/* Right Side - Episode List (30%) */}
|
||||||
<div className="w-full md:w-[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">
|
<div className="h-full max-h-[calc(100vh-2rem)] overflow-hidden">
|
||||||
<EpisodeList
|
<EpisodeList
|
||||||
episodes={anime.episodes}
|
episodes={episodes}
|
||||||
currentEpisode={currentEpisode}
|
currentEpisode={currentEpisode}
|
||||||
onEpisodeClick={handleEpisodeClick}
|
onEpisodeClick={handleEpisodeClick}
|
||||||
|
isDub={isDub}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white/5 rounded-2xl shadow-2xl p-6 ring-1 ring-white/10">
|
<div className="bg-white/5 rounded-2xl shadow-2xl p-6 ring-1 ring-white/10">
|
||||||
<div className="text-center text-gray-400">
|
<div className="text-center text-gray-400">
|
||||||
No episodes available
|
{isLoading ? 'Loading episodes...' : 'No episodes available'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
|
|
||||||
// Update active episode when currentEpisode changes
|
// Update active episode when currentEpisode changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentEpisode?.episodeId) {
|
if (currentEpisode?.id) {
|
||||||
setActiveEpisodeId(currentEpisode.episodeId);
|
setActiveEpisodeId(currentEpisode.id);
|
||||||
}
|
}
|
||||||
}, [currentEpisode]);
|
}, [currentEpisode]);
|
||||||
|
|
||||||
@@ -20,11 +20,15 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const match = path.match(/\/watch\/(.+)$/);
|
const match = path.match(/\/watch\/(.+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const episodeId = match[1];
|
const urlEpisodeId = match[1];
|
||||||
setActiveEpisodeId(episodeId);
|
setActiveEpisodeId(urlEpisodeId);
|
||||||
|
|
||||||
// Find the episode and update page
|
// 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) {
|
if (episode) {
|
||||||
const pageNumber = Math.ceil(episode.number / episodesPerPage);
|
const pageNumber = Math.ceil(episode.number / episodesPerPage);
|
||||||
setCurrentPage(pageNumber);
|
setCurrentPage(pageNumber);
|
||||||
@@ -48,6 +52,23 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
};
|
};
|
||||||
}, [episodes, episodesPerPage]);
|
}, [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(() => {
|
const filteredEpisodes = useMemo(() => {
|
||||||
if (!searchQuery) return episodes;
|
if (!searchQuery) return episodes;
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
@@ -69,24 +90,27 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isCurrentEpisode = (episode) => {
|
const isCurrentEpisode = (episode) => {
|
||||||
return episode.episodeId === activeEpisodeId;
|
if (!episode || !episode.id || !activeEpisodeId) return false;
|
||||||
|
return normalizeEpisodeId(episode.id) === normalizeEpisodeId(activeEpisodeId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEpisodeSelect = (episode, e) => {
|
const handleEpisodeSelect = (episode, e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (onEpisodeClick) {
|
if (onEpisodeClick && episode.id) {
|
||||||
onEpisodeClick(episode.episodeId);
|
onEpisodeClick(episode.id);
|
||||||
}
|
}
|
||||||
setActiveEpisodeId(episode.episodeId);
|
setActiveEpisodeId(episode.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scroll active episode into view when page changes or active episode changes
|
// Scroll active episode into view when page changes or active episode changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeEpisodeId) {
|
if (activeEpisodeId) {
|
||||||
|
setTimeout(() => {
|
||||||
const activeElement = document.querySelector(`[data-episode-id="${activeEpisodeId}"]`);
|
const activeElement = document.querySelector(`[data-episode-id="${activeEpisodeId}"]`);
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
}
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}, [activeEpisodeId, currentPage]);
|
}, [activeEpisodeId, currentPage]);
|
||||||
|
|
||||||
@@ -160,7 +184,7 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
{currentEpisodes.map((episode) => (
|
{currentEpisodes.map((episode) => (
|
||||||
<button
|
<button
|
||||||
key={episode.number}
|
key={episode.number}
|
||||||
data-episode-id={episode.episodeId}
|
data-episode-id={episode.id}
|
||||||
onClick={(e) => handleEpisodeSelect(episode, e)}
|
onClick={(e) => handleEpisodeSelect(episode, e)}
|
||||||
className={`group relative ${
|
className={`group relative ${
|
||||||
isCurrentEpisode(episode)
|
isCurrentEpisode(episode)
|
||||||
@@ -191,7 +215,7 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
{currentEpisodes.map((episode) => (
|
{currentEpisodes.map((episode) => (
|
||||||
<button
|
<button
|
||||||
key={episode.number}
|
key={episode.number}
|
||||||
data-episode-id={episode.episodeId}
|
data-episode-id={episode.id}
|
||||||
onClick={(e) => handleEpisodeSelect(episode, e)}
|
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 ${
|
className={`group flex items-center gap-3 py-2 px-3 rounded-lg transition-all duration-300 w-full text-left ${
|
||||||
isCurrentEpisode(episode)
|
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');
|
const isHlsStream = src.includes('.m3u8') || src.includes('application/vnd.apple.mpegurl');
|
||||||
|
|
||||||
if (isHlsStream && Hls.isSupported()) {
|
if (isHlsStream && Hls.isSupported()) {
|
||||||
|
console.log('[VideoPlayer] HLS is supported, initializing');
|
||||||
|
|
||||||
hls = new Hls({
|
hls = new Hls({
|
||||||
xhrSetup: (xhr) => {
|
xhrSetup: (xhr, url) => {
|
||||||
|
console.log('[VideoPlayer] HLS XHR setup for URL:', url);
|
||||||
|
|
||||||
// Set headers for HLS requests
|
// Set headers for HLS requests
|
||||||
Object.entries(videoHeaders).forEach(([key, value]) => {
|
Object.entries(videoHeaders).forEach(([key, value]) => {
|
||||||
xhr.setRequestHeader(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
|
// 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);
|
hls.attachMedia(video);
|
||||||
|
|
||||||
// Handle HLS events
|
// Handle HLS events
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
console.log('[VideoPlayer] HLS manifest parsed');
|
||||||
// Get available qualities
|
// Get available qualities
|
||||||
const levels = hls.levels.map((level, index) => ({
|
const levels = hls.levels.map((level, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
label: `${level.height}p`,
|
label: `${level.height}p`,
|
||||||
|
height: level.height,
|
||||||
selected: index === hls.currentLevel
|
selected: index === hls.currentLevel
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('[VideoPlayer] Available qualities:', levels);
|
||||||
setQualities(levels);
|
setQualities(levels);
|
||||||
setCurrentQuality(hls.currentLevel);
|
setCurrentQuality(hls.currentLevel);
|
||||||
|
|
||||||
// Auto-play when ready
|
// Auto-play when ready
|
||||||
if (isPlaying) {
|
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.kind = 'subtitles';
|
||||||
track.label = activeSubtitle.label || activeSubtitle.lang || 'Default';
|
track.label = activeSubtitle.label || activeSubtitle.lang || 'Default';
|
||||||
track.srclang = activeSubtitle.lang || 'en';
|
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;
|
track.default = true;
|
||||||
|
|
||||||
console.log('[VideoPlayer] Adding track on mount with src:', track.src);
|
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;
|
if (!videoRef.current || !subtitle) return;
|
||||||
|
|
||||||
// Ensure we have valid URL
|
// Ensure we have valid URL
|
||||||
const subtitleUrl = subtitle.src || subtitle.url;
|
let subtitleUrl = subtitle.src || subtitle.url;
|
||||||
if (!subtitleUrl) {
|
if (!subtitleUrl) {
|
||||||
console.error('[VideoPlayer] No valid URL found in subtitle:', JSON.stringify(subtitle));
|
console.error('[VideoPlayer] No valid URL found in subtitle:', JSON.stringify(subtitle));
|
||||||
return;
|
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
|
// Remove all existing tracks
|
||||||
const video = videoRef.current;
|
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 {
|
try {
|
||||||
if (!episodeId || episodeId === 'undefined') {
|
if (!episodeId || episodeId === 'undefined') {
|
||||||
console.error('Invalid episode ID provided');
|
console.error('Invalid episode ID provided');
|
||||||
return { sources: [] };
|
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}`);
|
console.log(`[API Call] Fetching sources from: ${apiUrl}`);
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
@@ -458,15 +577,17 @@ export const fetchEpisodeSources = async (episodeId, dub = false) => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('[API Response] Raw data:', data);
|
console.log('[API Response] Raw data:', data);
|
||||||
|
|
||||||
if (!data || !data.data) {
|
if (!data || !data.success || !data.data) {
|
||||||
console.error('[API Error] Empty response received');
|
console.error('[API Error] Empty response received');
|
||||||
return { sources: [] };
|
return { sources: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sources: data.data.sources,
|
sources: data.data.sources || [],
|
||||||
headers: data.data.headers || { "Referer": "https://hianime.to/" },
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching episode sources:', error);
|
console.error('Error fetching episode sources:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user