449 lines
16 KiB
JavaScript
449 lines
16 KiB
JavaScript
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 = `<p>Ep ${epNumber}: ${ep.title || 'Untitled'}</p>`;
|
|
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();
|