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; let animeTitle = ""; let aniSkipData = null; let isAnilist = false; let malId = null; const params = new URLSearchParams(window.location.search); const firstKey = params.keys().next().value; let extName; if (firstKey) extName = firstKey; // URL de retroceso: Si es local, volvemos a la vista de Anilist normal const href = (extName && extName !== 'local') ? `/anime/${extName}/${animeId}` : `/anime/${animeId}`; document.getElementById('back-link').href = href; document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`; let localEntryId = null; async function checkLocal() { try { const res = await fetch(`/api/library/anime/${animeId}`); if (!res.ok) return null; const data = await res.json(); return data.id; } catch { return null; } } async function loadAniSkip(malId, episode, duration) { try { const res = await fetch(`https://api.aniskip.com/v2/skip-times/${malId}/${episode}?types[]=op&types[]=ed&episodeLength=${duration}`); if (!res.ok) return null; const data = await res.json(); return data.results || []; } catch (error) { console.error('Error loading AniSkip data:', error); return null; } } async function loadMetadata() { localEntryId = await checkLocal(); try { const sourceQuery = (extName === 'local' || !extName) ? "source=anilist" : `source=${extName}`; const res = await fetch(`/api/anime/${animeId}?${sourceQuery}`); const data = await res.json(); if (data.error) { console.error("Error from API:", data.error); return; } const isAnilistFormat = data.title && (data.title.romaji || data.title.english); let title = '', description = '', coverImage = '', averageScore = '', format = '', seasonYear = '', season = ''; if (isAnilistFormat) { title = data.title.romaji || data.title.english || data.title.native || 'Anime Title'; description = data.description || 'No description available.'; coverImage = data.coverImage?.large || data.coverImage?.medium || ''; averageScore = data.averageScore ? `${data.averageScore}%` : '--'; format = data.format || '--'; season = data.season ? data.season.charAt(0) + data.season.slice(1).toLowerCase() : ''; seasonYear = data.seasonYear || ''; } else { title = data.title || 'Anime Title'; description = data.summary || 'No description available.'; coverImage = data.image || ''; averageScore = data.score ? `${Math.round(data.score * 10)}%` : '--'; format = '--'; season = data.season || ''; seasonYear = data.year || ''; } if (isAnilistFormat && data.idMal) { isAnilist = true; malId = data.idMal; } else { isAnilist = false; malId = null; } document.getElementById('anime-title-details').innerText = title; document.getElementById('anime-title-details2').innerText = title; animeTitle = title; document.title = `Watching ${title} - Ep ${currentEpisode}`; const tempDiv = document.createElement('div'); tempDiv.innerHTML = description; document.getElementById('detail-description').innerText = tempDiv.textContent || tempDiv.innerText || 'No description available.'; document.getElementById('detail-format').innerText = format; document.getElementById('detail-score').innerText = averageScore; document.getElementById('detail-season').innerText = season && seasonYear ? `${season} ${seasonYear}` : (season || seasonYear || '--'); document.getElementById('detail-cover-image').src = coverImage || '/default-cover.jpg'; // Solo cargamos episodios de extensión si hay extensión real y no es local if (extName && extName !== 'local') { await loadExtensionEpisodes(); } else { if (data.nextAiringEpisode?.episode) { totalEpisodes = data.nextAiringEpisode.episode - 1; } else if (data.episodes) { totalEpisodes = data.episodes; } else { totalEpisodes = 12; } const simpleEpisodes = []; for (let i = 1; i <= totalEpisodes; i++) { simpleEpisodes.push({ number: i, title: null, thumbnail: null, isDub: false }); } populateEpisodeCarousel(simpleEpisodes); } if (currentEpisode >= totalEpisodes && totalEpisodes > 0) { document.getElementById('next-btn').disabled = true; } } catch (error) { console.error('Error loading metadata:', error); } await loadExtensions(); } async function applyAniSkip(video) { if (!isAnilist || !malId) return; aniSkipData = await loadAniSkip(malId, currentEpisode, Math.floor(video.duration)); if (!aniSkipData || aniSkipData.length === 0) return; const markers = []; aniSkipData.forEach(item => { const { startTime, endTime } = item.interval; markers.push({ start: startTime, end: endTime, label: item.skipType === 'op' ? 'Opening' : 'Ending' }); }); if (plyrInstance && markers.length > 0) { setTimeout(() => { const progressContainer = document.querySelector('.plyr__progress'); if (!progressContainer) return; const oldMarkers = progressContainer.querySelector('.plyr__markers'); if (oldMarkers) oldMarkers.remove(); const markersContainer = document.createElement('div'); markersContainer.className = 'plyr__markers'; markers.forEach(marker => { const markerElement = document.createElement('div'); markerElement.className = 'plyr__marker'; markerElement.dataset.label = marker.label; const startPercent = (marker.start / video.duration) * 100; const widthPercent = ((marker.end - marker.start) / video.duration) * 100; markerElement.style.left = `${startPercent}%`; markerElement.style.width = `${widthPercent}%`; markerElement.addEventListener('click', (e) => { e.stopPropagation(); video.currentTime = marker.start; }); markersContainer.appendChild(markerElement); }); progressContainer.appendChild(markersContainer); }, 500); } } async function loadExtensionEpisodes() { try { const res = await fetch(`/api/anime/${animeId}/episodes?source=${extName}`); const data = await res.json(); totalEpisodes = Array.isArray(data) ? data.length : 0; populateEpisodeCarousel(Array.isArray(data) ? data : []); } catch (e) { console.error("Error cargando episodios:", e); } } 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 !== 'local') ? `?${extName}` : ""; const hasThumbnail = ep.thumbnail && ep.thumbnail.trim() !== ''; const link = document.createElement('a'); link.href = `/watch/${animeId}/${epNumber}${extParam}`; link.classList.add('carousel-item'); 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.src = ep.thumbnail; img.classList.add('carousel-item-img'); imgContainer.appendChild(img); } link.appendChild(imgContainer); const info = document.createElement('div'); info.classList.add('carousel-item-info'); info.innerHTML = `

Ep ${epNumber}: ${ep.title || 'Untitled'}

`; 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'); let extensions = data.extensions || []; if (extName === 'local' && !extensions.includes('local')) { extensions.push('local'); } select.innerHTML = ''; extensions.forEach(ext => { const opt = document.createElement('option'); opt.value = opt.innerText = ext; select.appendChild(opt); }); if (extName && extensions.includes(extName)) { select.value = extName; } else if (extensions.length > 0) { select.value = extensions[0]; } currentExtension = select.value; onExtensionChange(); } catch (error) { console.error("Extension Error:", error); } } async function onExtensionChange() { const select = document.getElementById('extension-select'); currentExtension = select.value; if (currentExtension === 'local') { document.getElementById('sd-toggle').style.display = 'none'; document.getElementById('server-select').style.display = 'none'; loadStream(); return; } setLoading("Fetching extension settings..."); try { const res = await fetch(`/api/extensions/${currentExtension}/settings`); const settings = await res.json(); const toggle = document.getElementById('sd-toggle'); toggle.style.display = settings.supportsDub ? 'flex' : 'none'; setAudioMode('sub'); const serverSelect = document.getElementById('server-select'); serverSelect.innerHTML = ''; if (settings.episodeServers?.length > 0) { settings.episodeServers.forEach(srv => { const opt = document.createElement('option'); opt.value = opt.innerText = srv; serverSelect.appendChild(opt); }); serverSelect.style.display = 'block'; } else { serverSelect.style.display = 'none'; } loadStream(); } catch (error) { setLoading("Failed to load settings."); } } async function loadStream() { if (!currentExtension) return; if (currentExtension === 'local') { if (!localEntryId) { setLoading("No existe en local"); return; } const localUrl = `/api/library/stream/anime/${localEntryId}/${currentEpisode}`; playVideo(localUrl, []); document.getElementById('loading-overlay').style.display = 'none'; return; } const serverSelect = document.getElementById('server-select'); const server = serverSelect.value || "default"; setLoading(`Loading stream (${audioMode})...`); try { const sourc = (extName && extName !== 'local') ? `&source=${extName}` : "&source=anilist"; const url = `/api/watch/stream?animeId=${animeId}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}${sourc}`; const res = await fetch(url); const data = await res.json(); if (data.error || !data.videoSources?.length) { setLoading(data.error || "No video sources."); 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, source.subtitles || data.subtitles || []); document.getElementById('loading-overlay').style.display = 'none'; } catch (error) { setLoading("Stream error."); } } function playVideo(url, subtitles = []) { const video = document.getElementById('player'); const isLocal = url.includes('/api/library/stream/'); if (!isLocal && Hls.isSupported()) { if (hlsInstance) hlsInstance.destroy(); hlsInstance = new Hls({ xhrSetup: (xhr) => xhr.withCredentials = false }); hlsInstance.loadSource(url); hlsInstance.attachMedia(video); } else { if (hlsInstance) hlsInstance.destroy(); video.src = url; } if (plyrInstance) plyrInstance.destroy(); while (video.textTracks.length > 0) video.removeChild(video.textTracks[0]); subtitles.forEach(sub => { const track = document.createElement('track'); track.kind = 'captions'; track.label = sub.language || 'Unknown'; 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.addEventListener('loadedmetadata', () => applyAniSkip(video)); } async function sendProgress() { const token = localStorage.getItem('token'); if (!token) return; const source = (extName && extName !== 'local') ? extName : "anilist"; const body = { entry_id: animeId, source: source, entry_type: "ANIME", status: 'CURRENT', progress: currentEpisode }; try { await fetch('/api/list/entry', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }); } catch (err) { console.error('Error updating progress:', err); } } // Botones y Toggle document.getElementById('sd-toggle').onclick = () => { audioMode = audioMode === 'sub' ? 'dub' : 'sub'; setAudioMode(audioMode); loadStream(); }; function setAudioMode(mode) { const toggle = document.getElementById('sd-toggle'); toggle.setAttribute('data-state', mode); document.getElementById('opt-sub').classList.toggle('active', mode === 'sub'); document.getElementById('opt-dub').classList.toggle('active', mode === 'dub'); } function setLoading(message) { document.getElementById('loading-text').innerText = message; document.getElementById('loading-overlay').style.display = 'flex'; } const extParam = (extName && extName !== 'local') ? `?${extName}` : ""; document.getElementById('prev-btn').onclick = () => { if (currentEpisode > 1) window.location.href = `/watch/${animeId}/${currentEpisode - 1}${extParam}`; }; document.getElementById('next-btn').onclick = () => { if (currentEpisode < totalEpisodes || totalEpisodes === 0) window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; }; if (currentEpisode <= 1) document.getElementById('prev-btn').disabled = true; // Actualizar progreso cada 1 minuto si el video está reproduciéndose setInterval(() => { if (plyrInstance && !plyrInstance.paused) sendProgress(); }, 60000); loadMetadata();