const pathParts = window.location.pathname.split('/'); const animeId = pathParts[2]; const currentEpisode = parseInt(pathParts[3]); let audioMode = 'sub'; let currentExtension = ''; let plyrInstance; let hlsInstance; let totalEpisodes = 0; const params = new URLSearchParams(window.location.search); const firstKey = params.keys().next().value; let extName; if (firstKey) extName = firstKey; const href = extName ? `/anime/${extName}/${animeId}` : `/anime/${animeId}`; document.getElementById('back-link').href = href; document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`; async function loadMetadata() { try { const extQuery = extName ? `?ext=${extName}` : ""; const res = await fetch(`/api/anime/${animeId}${extQuery}`); const data = await res.json(); if (!data.error) { const romajiTitle = data.title.romaji || data.title.english || 'Anime Title'; document.getElementById('anime-title-details').innerText = romajiTitle; document.getElementById('anime-title-details2').innerText = romajiTitle; document.title = `Watching ${romajiTitle} - Ep ${currentEpisode}`; const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.description || 'No description available.'; document.getElementById('detail-description').innerText = tempDiv.textContent || tempDiv.innerText; document.getElementById('detail-format').innerText = data.format || '--'; document.getElementById('detail-score').innerText = data.averageScore ? `${data.averageScore}%` : '--'; const season = data.season ? data.season.charAt(0) + data.season.slice(1).toLowerCase() : ''; document.getElementById('detail-season').innerText = data.seasonYear ? `${season} ${data.seasonYear}` : '--'; document.getElementById('detail-cover-image').src = data.coverImage.large || data.coverImage.medium || ''; if (data.characters && data.characters.edges && data.characters.edges.length > 0) { populateCharacters(data.characters.edges); } if (!extName) { totalEpisodes = data.episodes || 0; if (totalEpisodes > 0) { const simpleEpisodes = []; for (let i = 1; i <= totalEpisodes; i++) { simpleEpisodes.push({ number: i, title: null, thumbnail: null, isDub: false }); } populateEpisodeCarousel(simpleEpisodes); } } else { try { const res2 = await fetch(`/api/anime/${animeId}/episodes${extQuery}`); const data2 = await res2.json(); totalEpisodes = Array.isArray(data2) ? data2.length : 0; if (Array.isArray(data2) && data2.length > 0) { populateEpisodeCarousel(data2); } } catch (e) { console.error("Error cargando episodios por extensión:", e); totalEpisodes = 0; } } if (currentEpisode >= totalEpisodes && totalEpisodes > 0) { document.getElementById('next-btn').disabled = true; } } } catch (error) { console.error('Error loading metadata:', error); } } function populateCharacters(characterEdges) { const list = document.getElementById('characters-list'); list.classList.remove('characters-list'); list.classList.add('characters-carousel'); list.innerHTML = ''; characterEdges.forEach(edge => { const character = edge.node; const voiceActor = edge.voiceActors ? edge.voiceActors.find(va => va.language === 'Japanese' || va.language === 'English') : null; if (character) { const card = document.createElement('div'); card.classList.add('character-card'); const img = document.createElement('img'); img.classList.add('character-card-img'); img.src = character.image.large || character.image.medium || ''; img.alt = character.name.full || 'Character'; const vaName = voiceActor ? (voiceActor.name.full || voiceActor.name.userPreferred) : null; const characterName = character.name.full || character.name.userPreferred || '--'; const details = document.createElement('div'); details.classList.add('character-details'); const name = document.createElement('p'); name.classList.add('character-name'); name.innerText = characterName; const actor = document.createElement('p'); actor.classList.add('actor-name'); if (vaName) { actor.innerText = `${vaName} (${voiceActor.language})`; } else { actor.innerText = 'Voice Actor: N/A'; } details.appendChild(name); details.appendChild(actor); card.appendChild(img); card.appendChild(details); list.appendChild(card); } }); } function populateEpisodeCarousel(episodesData) { const carousel = document.getElementById('episode-carousel'); carousel.innerHTML = ''; episodesData.forEach((ep, index) => { const epNumber = ep.number || ep.episodeNumber || ep.id || (index + 1); if (!epNumber) return; const extParam = extName ? `?${extName}` : ""; const hasThumbnail = ep.thumbnail && ep.thumbnail.trim() !== ''; const link = document.createElement('a'); link.href = `/watch/${animeId}/${epNumber}${extParam}`; link.classList.add('carousel-item'); link.dataset.episode = epNumber; if (!hasThumbnail) { link.classList.add('no-thumbnail'); } if (parseInt(epNumber) === currentEpisode) { link.classList.add('active-ep-carousel'); } const imgContainer = document.createElement('div'); imgContainer.classList.add('carousel-item-img-container'); if (hasThumbnail) { const img = document.createElement('img'); img.classList.add('carousel-item-img'); img.src = ep.thumbnail; img.alt = `Episode ${epNumber} Thumbnail`; imgContainer.appendChild(img); } link.appendChild(imgContainer); const info = document.createElement('div'); info.classList.add('carousel-item-info'); const title = document.createElement('p'); title.innerText = `Ep ${epNumber}: ${ep.title || 'Untitled'}`; info.appendChild(title); link.appendChild(info); carousel.appendChild(link); }); } async function loadExtensions() { try { const res = await fetch('/api/extensions/anime'); const data = await res.json(); const select = document.getElementById('extension-select'); if (data.extensions && data.extensions.length > 0) { select.innerHTML = ''; data.extensions.forEach(ext => { const opt = document.createElement('option'); opt.value = opt.innerText = ext; select.appendChild(opt); }); if (typeof extName === 'string' && data.extensions.includes(extName)) { select.value = extName; } else { select.selectedIndex = 0; } currentExtension = select.value; onExtensionChange(); } else { select.innerHTML = ''; select.disabled = true; setLoading("No anime extensions found."); } } catch (error) { console.error("Extension Error:", error); } } async function onExtensionChange() { const select = document.getElementById('extension-select'); currentExtension = select.value; setLoading("Fetching extension settings..."); try { const res = await fetch(`/api/extensions/${currentExtension}/settings`); const settings = await res.json(); const toggle = document.getElementById('sd-toggle'); if (settings.supportsDub) { toggle.style.display = 'flex'; setAudioMode('sub'); } else { toggle.style.display = 'none'; setAudioMode('sub'); } const serverSelect = document.getElementById('server-select'); serverSelect.innerHTML = ''; if (settings.episodeServers && settings.episodeServers.length > 0) { settings.episodeServers.forEach(srv => { const opt = document.createElement('option'); opt.value = srv; opt.innerText = srv; serverSelect.appendChild(opt); }); serverSelect.style.display = 'block'; } else { serverSelect.style.display = 'none'; } loadStream(); } catch (error) { console.error(error); setLoading("Failed to load extension settings."); } } function toggleAudioMode() { const newMode = audioMode === 'sub' ? 'dub' : 'sub'; setAudioMode(newMode); loadStream(); } function setAudioMode(mode) { audioMode = mode; const toggle = document.getElementById('sd-toggle'); const subOpt = document.getElementById('opt-sub'); const dubOpt = document.getElementById('opt-dub'); toggle.setAttribute('data-state', mode); if (mode === 'sub') { subOpt.classList.add('active'); dubOpt.classList.remove('active'); } else { subOpt.classList.remove('active'); dubOpt.classList.add('active'); } } async function loadStream() { if (!currentExtension) return; const serverSelect = document.getElementById('server-select'); const server = serverSelect.value || "default"; setLoading(`Loading stream (${audioMode})...`); try { const url = `/api/watch/stream?animeId=${animeId.slice(0, 30)}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`; const res = await fetch(url); const data = await res.json(); if (data.error) { setLoading(`Error: ${data.error}`); return; } if (!data.videoSources || data.videoSources.length === 0) { setLoading("No video sources found."); return; } const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0]; const headers = data.headers || {}; let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`; if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`; if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`; playVideo(proxyUrl, data.videoSources[0].subtitles); document.getElementById('loading-overlay').style.display = 'none'; } catch (error) { setLoading("Stream error. Check console."); console.error(error); } } function playVideo(url, subtitles) { const video = document.getElementById('player'); if (Hls.isSupported()) { if (hlsInstance) hlsInstance.destroy(); hlsInstance = new Hls({ xhrSetup: (xhr, url) => { xhr.withCredentials = false; } }); hlsInstance.loadSource(url); hlsInstance.attachMedia(video); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; } if (plyrInstance) plyrInstance.destroy(); while (video.firstChild) { video.removeChild(video.firstChild); } if (subtitles && subtitles.length > 0) { subtitles.forEach(sub => { const track = document.createElement('track'); track.kind = 'captions'; track.label = sub.language; track.srclang = sub.language.slice(0, 2).toLowerCase(); track.src = sub.url; if (sub.default || sub.language.toLowerCase().includes('english')) { track.default = true; } video.appendChild(track); }); } plyrInstance = new Plyr(video, { captions: { active: true, update: true, language: 'en' }, controls: [ 'play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen' ], settings: ['captions', 'quality', 'speed'] }); video.play().catch(error => { console.log("Autoplay blocked:", error); }); } function setLoading(message) { const overlay = document.getElementById('loading-overlay'); const text = document.getElementById('loading-text'); overlay.style.display = 'flex'; text.innerText = message; } const extParam = extName ? `?${extName}` : ""; document.getElementById('prev-btn').onclick = () => { if (currentEpisode > 1) { window.location.href = `/watch/${animeId}/${currentEpisode - 1}${extParam}`; } }; document.getElementById('next-btn').onclick = () => { window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; }; if (currentEpisode <= 1) { document.getElementById('prev-btn').disabled = true; } loadMetadata(); loadExtensions();