diff --git a/src/app/watch/[episodeId]/page.js b/src/app/watch/[episodeId]/page.js
index b2c73d1..4bbcf1b 100644
--- a/src/app/watch/[episodeId]/page.js
+++ b/src/app/watch/[episodeId]/page.js
@@ -68,12 +68,33 @@ export default function WatchPage() {
console.log('[Watch] Raw episodeId from URL:', episodeId);
// Extract animeId from the episodeId parameter
- // The new format is: anime-name?ep=episode-number
- const [baseId, queryString] = episodeId.split('?');
+ // Handle different possible formats:
+ // 1. anime-name?ep=episode-number (standard format)
+ // 2. anime-name-episode-number (legacy format)
- if (baseId) {
- setAnimeId(baseId);
- console.log('[Watch] Extracted anime ID:', baseId);
+ let extractedAnimeId;
+ let episodeNumber;
+
+ if (episodeId.includes('?ep=')) {
+ // Format: anime-name?ep=episode-number
+ const [baseId, queryString] = episodeId.split('?');
+ extractedAnimeId = baseId;
+ episodeNumber = queryString.replace('ep=', '');
+ console.log(`[Watch] Format detected: standard (anime-name?ep=episode-number)`);
+ } else if (episodeId.includes('-')) {
+ // Format: anime-name-episode-number
+ const match = episodeId.match(/^(.*?)-(\d+)$/);
+ if (match) {
+ extractedAnimeId = match[1];
+ episodeNumber = match[2];
+ console.log(`[Watch] Format detected: legacy (anime-name-episode-number)`);
+ }
+ }
+
+ if (extractedAnimeId) {
+ setAnimeId(extractedAnimeId);
+ console.log('[Watch] Extracted anime ID:', extractedAnimeId);
+ console.log('[Watch] Extracted episode number:', episodeNumber);
} else {
console.warn('[Watch] Could not extract anime ID from episode ID:', episodeId);
}
@@ -283,32 +304,35 @@ export default function WatchPage() {
// 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 to find the episode by direct ID match
+ const directMatch = episodesData.episodes.find(ep => ep.id === currentEpisodeId);
+ if (directMatch) {
+ console.log('[Watch] Found episode by direct ID match:', directMatch.number);
+ return directMatch;
+ }
- if (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);
+ // As a fallback, try to match by episode number
+ // Extract episode number from the URL if it's in the format anime-id?ep=number
+ if (currentEpisodeId.includes('?ep=')) {
+ const [, queryString] = currentEpisodeId.split('?');
+ if (queryString) {
+ const episodeNumber = queryString.replace('ep=', '');
+ console.log('[Watch] Trying to find by episode number:', episodeNumber);
+
+ const numberMatch = episodesData.episodes.find(ep =>
+ ep.number && ep.number.toString() === episodeNumber.toString()
+ );
+
+ if (numberMatch) {
+ console.log('[Watch] Found episode by number:', numberMatch.number);
+ return numberMatch;
+ }
}
}
- 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);
+ // If no match found, return first episode as fallback
+ console.warn('[Watch] Could not find matching episode, falling back to first episode');
+ return episodesData.episodes[0];
};
const episode = findCurrentEpisode();
@@ -377,6 +401,11 @@ export default function WatchPage() {
const handleEpisodeClick = (newEpisodeId) => {
if (newEpisodeId !== currentEpisodeId) {
+ console.log(`[Watch] Episode clicked, ID: ${newEpisodeId}`);
+
+ // Use the episode ID directly as it should already be in the correct format
+ // from the API response (animeId?ep=episodeNumber)
+
// Update the URL using history API
const newUrl = `/watch/${encodeURIComponent(newEpisodeId)}`;
window.history.pushState({ episodeId: newEpisodeId }, '', newUrl);
diff --git a/src/components/AnimeCard.js b/src/components/AnimeCard.js
index 4143b15..4736b54 100644
--- a/src/components/AnimeCard.js
+++ b/src/components/AnimeCard.js
@@ -1,11 +1,15 @@
'use client';
+import React, { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import Link from 'next/link';
-import { useState } from 'react';
+import { fetchAnimeEpisodes } from '@/lib/api';
export default function AnimeCard({ anime, isRecent }) {
const [imageError, setImageError] = useState(false);
+ const [firstEpisodeId, setFirstEpisodeId] = useState(null);
+ const [isHovered, setIsHovered] = useState(false);
+ const timerRef = useRef(null);
if (!anime) return null;
@@ -14,17 +18,60 @@ export default function AnimeCard({ anime, isRecent }) {
setImageError(true);
};
+ // Fetch first episode ID when component is hovered
+ useEffect(() => {
+ const fetchFirstEpisode = async () => {
+ if (anime?.id && isHovered && !firstEpisodeId) {
+ try {
+ const response = await fetchAnimeEpisodes(anime.id);
+ if (response.episodes && response.episodes.length > 0) {
+ // Get the first episode's episodeId
+ setFirstEpisodeId(response.episodes[0].episodeId);
+ console.log(`[AnimeCard] First episode ID for ${anime.name}: ${response.episodes[0].episodeId}`);
+ }
+ } catch (error) {
+ console.error(`[AnimeCard] Error fetching episodes for ${anime.id}:`, error);
+ }
+ }
+ };
+
+ fetchFirstEpisode();
+ }, [anime?.id, isHovered, firstEpisodeId]);
+
+ const handleMouseEnter = () => {
+ // Clear any existing timers
+ if (timerRef.current) clearTimeout(timerRef.current);
+ // Set a small delay to prevent API calls for quick mouseovers
+ timerRef.current = setTimeout(() => {
+ setIsHovered(true);
+ }, 300); // Delay to prevent unnecessary API calls
+ };
+
+ const handleMouseLeave = () => {
+ // Clear the timer if the user moves the mouse away quickly
+ if (timerRef.current) clearTimeout(timerRef.current);
+ setIsHovered(false);
+ };
+
// Get image URL with fallback
const imageSrc = imageError ? '/images/placeholder.png' : anime.poster;
// Generate appropriate links
const infoLink = `/anime/${anime.id}`;
- const watchLink = isRecent
- ? `/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`
- : `/anime/${anime.id}`;
+
+ // Build the watch URL based on the first episode ID or fallback
+ const watchLink = isRecent ? (
+ firstEpisodeId
+ ? `/watch/${firstEpisodeId}`
+ : `/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`
+ ) : infoLink;
return (
-
+
{/* Image card linking to watch page */}
{
+ const fetchFirstEpisode = async () => {
+ if (anime?.info?.id) {
+ setIsLoadingEpisodes(true);
+ try {
+ const response = await fetchAnimeEpisodes(anime.info.id);
+ if (response.episodes && response.episodes.length > 0) {
+ // Get the first episode's episodeId
+ setFirstEpisodeId(response.episodes[0].episodeId);
+ console.log(`[AnimeDetails] First episode ID: ${response.episodes[0].episodeId}`);
+ }
+ } catch (error) {
+ console.error('[AnimeDetails] Error fetching episodes:', error);
+ } finally {
+ setIsLoadingEpisodes(false);
+ }
+ }
+ };
+
+ fetchFirstEpisode();
+ }, [anime?.info?.id]);
+
if (!anime?.info) {
return null;
}
@@ -29,6 +55,11 @@ export default function AnimeDetails({ anime }) {
const hasCharacters = info.characterVoiceActor?.length > 0 || info.charactersVoiceActors?.length > 0;
const hasVideos = info.promotionalVideos && info.promotionalVideos.length > 0;
+ // Build the watch URL based on the first episode ID or fallback
+ const watchUrl = firstEpisodeId
+ ? `/watch/${firstEpisodeId}`
+ : `/watch/${info.id}?ep=1`; // Fallback to old format if API fetch fails
+
// Video modal for promotional videos
const VideoModal = ({ video, onClose }) => {
if (!video) return null;
@@ -208,7 +239,7 @@ export default function AnimeDetails({ anime }) {
{/* Watch Button - Full Width on Mobile */}
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
diff --git a/src/components/EpisodeList.js b/src/components/EpisodeList.js
index 46c783c..fb5e478 100644
--- a/src/components/EpisodeList.js
+++ b/src/components/EpisodeList.js
@@ -52,21 +52,11 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
};
}, [episodes, episodesPerPage]);
- // Helper function to normalize episode IDs for comparison
+ // Helper function for episode ID comparison
+ // The API returns episode IDs in the format: animeId?ep=episodeNumber
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;
+ // Simply return the ID as-is since the API already provides the correct format
+ return id || '';
};
const filteredEpisodes = useMemo(() => {
@@ -97,9 +87,11 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
const handleEpisodeSelect = (episode, e) => {
e.preventDefault();
if (onEpisodeClick && episode.id) {
+ // Use the episode ID directly as it's already in the correct format from the API
+ console.log(`[EpisodeList] Selected episode: ${episode.number}, ID: ${episode.id}`);
onEpisodeClick(episode.id);
+ setActiveEpisodeId(episode.id);
}
- setActiveEpisodeId(episode.id);
};
// Scroll active episode into view when page changes or active episode changes
diff --git a/src/components/SpotlightCarousel.js b/src/components/SpotlightCarousel.js
index 8210baa..c30f153 100644
--- a/src/components/SpotlightCarousel.js
+++ b/src/components/SpotlightCarousel.js
@@ -1,10 +1,11 @@
'use client';
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination, EffectFade } from 'swiper/modules';
+import { fetchAnimeEpisodes } from '@/lib/api';
// Import Swiper styles
import 'swiper/css';
@@ -14,12 +15,88 @@ import 'swiper/css/effect-fade';
const SpotlightCarousel = ({ items = [] }) => {
const [isClient, setIsClient] = useState(false);
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [autoplay, setAutoplay] = useState(true);
+ const [progress, setProgress] = useState(0);
+ const [episodeIds, setEpisodeIds] = useState({});
+ const intervalRef = useRef(null);
+ const progressIntervalRef = useRef(null);
// Handle hydration mismatch
useEffect(() => {
setIsClient(true);
}, []);
+ // Fetch first episode IDs for all spotlight items
+ useEffect(() => {
+ const fetchEpisodeData = async () => {
+ const episodeData = {};
+
+ for (const item of items) {
+ if (item.id) {
+ try {
+ const response = await fetchAnimeEpisodes(item.id);
+ if (response.episodes && response.episodes.length > 0) {
+ episodeData[item.id] = response.episodes[0].episodeId;
+ }
+ } catch (error) {
+ console.error(`[SpotlightCarousel] Error fetching episodes for ${item.id}:`, error);
+ }
+ }
+ }
+
+ setEpisodeIds(episodeData);
+ };
+
+ if (items && items.length > 0) {
+ fetchEpisodeData();
+ }
+ }, [items]);
+
+ // Autoplay functionality
+ useEffect(() => {
+ if (autoplay && items.length > 1) {
+ // Clear any existing intervals
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
+
+ // Set up new intervals
+ setProgress(0);
+ progressIntervalRef.current = setInterval(() => {
+ setProgress(prev => {
+ const newProgress = prev + 1;
+ return newProgress <= 100 ? newProgress : prev;
+ });
+ }, 50); // Update every 50ms to get smooth progress
+
+ intervalRef.current = setTimeout(() => {
+ setCurrentIndex(prevIndex => (prevIndex + 1) % items.length);
+ setProgress(0);
+ }, 5000);
+ }
+
+ return () => {
+ if (intervalRef.current) clearTimeout(intervalRef.current);
+ if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
+ };
+ }, [autoplay, currentIndex, items.length]);
+
+ const handleDotClick = (index) => {
+ setCurrentIndex(index);
+ setProgress(0);
+ // Reset autoplay timer when manually changing slides
+ if (intervalRef.current) clearTimeout(intervalRef.current);
+ if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
+ if (autoplay) {
+ intervalRef.current = setTimeout(() => {
+ setCurrentIndex((index + 1) % items.length);
+ }, 5000);
+ }
+ };
+
+ const handleMouseEnter = () => setAutoplay(false);
+ const handleMouseLeave = () => setAutoplay(true);
+
// If no items or not on client yet, show loading state
if (!isClient || !items.length) {
return (
@@ -32,6 +109,13 @@ const SpotlightCarousel = ({ items = [] }) => {
);
}
+ const currentItem = items[currentIndex];
+
+ // Get the watch URL for the current item
+ const watchUrl = episodeIds[currentItem.id]
+ ? `/watch/${episodeIds[currentItem.id]}`
+ : `/watch/${currentItem.id}?ep=1`; // Fallback to old format if API fetch fails
+
return (
{
}}
loop={true}
className="rounded-xl overflow-hidden"
+ onSlideChange={(swiper) => {
+ setCurrentIndex(swiper.realIndex);
+ setProgress(0);
+ // Reset autoplay timer when manually changing slides
+ if (intervalRef.current) clearTimeout(intervalRef.current);
+ if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
+ if (autoplay) {
+ intervalRef.current = setTimeout(() => {
+ setCurrentIndex((swiper.realIndex + 1) % items.length);
+ }, 5000);
+ }
+ }}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
>
{items.map((anime, index) => (
@@ -131,7 +229,7 @@ const SpotlightCarousel = ({ items = [] }) => {
{/* Buttons - Below title on mobile, right side on desktop */}
diff --git a/src/lib/api.js b/src/lib/api.js
index 52fc204..8d6efae 100644
--- a/src/lib/api.js
+++ b/src/lib/api.js
@@ -463,6 +463,9 @@ export const fetchAnimeEpisodes = async (animeId) => {
return { episodes: [] };
}
+ // API already returns episodeId in correct format (animeId?ep=episodeNumber)
+ // So we simply use it directly without any formatting
+
return {
episodes: data.data.episodes || [],
totalEpisodes: data.data.totalEpisodes || 0
@@ -480,20 +483,11 @@ export const fetchEpisodeServers = async (episodeId) => {
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}`);
- }
- }
+ console.log(`[API] Processing episode ID: ${episodeId}`);
- const apiUrl = `${API_BASE_URL}/episode/servers?animeEpisodeId=${encodeURIComponent(formattedEpisodeId)}`;
+ // episodeId should already be in the correct format (animeId?ep=episodeNumber)
+ // from the API response, so we use it directly
+ const apiUrl = `${API_BASE_URL}/episode/servers?animeEpisodeId=${encodeURIComponent(episodeId)}`;
console.log(`[API Call] Fetching servers from: ${apiUrl}`);
const response = await fetch(apiUrl, {
@@ -547,22 +541,13 @@ export const fetchEpisodeSources = async (episodeId, dub = false, server = 'hd-2
return { sources: [] };
}
- // 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}`);
- }
- }
+ console.log(`[API] Processing episode ID for sources: ${episodeId}`);
+ // episodeId should already be in the correct format (animeId?ep=episodeNumber)
+ // from the API response, so we use it directly
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}`;
+ const apiUrl = `${API_BASE_URL}/episode/sources?animeEpisodeId=${encodeURIComponent(episodeId)}&category=${category}&server=${serverName}`;
console.log(`[API Call] Fetching sources from: ${apiUrl}`);
const response = await fetch(apiUrl, {