425 lines
14 KiB
JavaScript
425 lines
14 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) {
|
|
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 = '<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);
|
|
|
|
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(); |