Files
WaifuBoard/src/scripts/anime/player.js
2025-12-02 21:16:05 +01:00

370 lines
13 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;
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) {
console.error("Error from API:", data.error);
return;
}
const isAnilistFormat = data.title && (data.title.romaji || data.title.english);
let title = '';
let description = '';
let coverImage = '';
let averageScore = '';
let format = '';
let seasonYear = '';
let season = '';
let episodesCount = 0;
let characters = [];
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 || '';
}
document.getElementById('anime-title-details').innerText = title;
document.getElementById('anime-title-details2').innerText = 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';
if (extName) {
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);
}
}
async function loadExtensionEpisodes() {
try {
const extQuery = extName ? `?ext=${extName}` : "";
const res = await fetch(`/api/anime/${animeId}/episodes${extQuery}`);
const data = await res.json();
totalEpisodes = Array.isArray(data) ? data.length : 0;
if (Array.isArray(data) && data.length > 0) {
populateEpisodeCarousel(data);
} else {
const fallback = [];
for (let i = 1; i <= totalEpisodes; i++) {
fallback.push({ number: i, title: null, thumbnail: null });
}
populateEpisodeCarousel(fallback);
}
} catch (e) {
console.error("Error cargando episodios por extensión:", e);
totalEpisodes = 0;
}
}
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 = '<option>No Extensions</option>';
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);
subOpt.classList.toggle('active', mode === 'sub');
dubOpt.classList.toggle('active', mode === 'dub');
}
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}&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 || data.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) => 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.textTracks.length > 0) {
video.removeChild(video.textTracks[0]);
}
subtitles.forEach(sub => {
if (!sub.url) return;
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.play().catch(() => console.log("Autoplay blocked"));
}
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 = () => {
if (currentEpisode < totalEpisodes || totalEpisodes === 0) {
window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`;
}
};
if (currentEpisode <= 1) {
document.getElementById('prev-btn').disabled = true;
}
loadMetadata();
loadExtensions();