player now supports quality settings
This commit is contained in:
@@ -462,11 +462,14 @@ const AnimePlayer = (function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
attachSubtitles(subtitles);
|
|
||||||
initPlyr();
|
initPlyr();
|
||||||
|
|
||||||
|
plyrInstance.on('ready', () => {
|
||||||
|
createAudioSelector(hlsInstance);
|
||||||
|
createQualitySelector(hlsInstance);
|
||||||
|
});
|
||||||
|
|
||||||
els.video.play().catch(() => {});
|
els.video.play().catch(() => {});
|
||||||
els.loader.style.display = 'none';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -493,6 +496,45 @@ const AnimePlayer = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createQualitySelector(hls) {
|
||||||
|
const levels = hls.levels;
|
||||||
|
if (!levels || !levels.length) return;
|
||||||
|
|
||||||
|
const plyrEl = els.video.closest('.plyr');
|
||||||
|
const controls = plyrEl.querySelector('.plyr__controls');
|
||||||
|
if (!controls) return;
|
||||||
|
|
||||||
|
if (controls.querySelector('#quality-select')) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'plyr__control';
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.id = 'quality-select';
|
||||||
|
|
||||||
|
// AUTO
|
||||||
|
const auto = document.createElement('option');
|
||||||
|
auto.value = -1;
|
||||||
|
auto.textContent = 'Auto';
|
||||||
|
select.appendChild(auto);
|
||||||
|
|
||||||
|
levels.forEach((l, i) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = i;
|
||||||
|
opt.textContent = `${l.height}p`;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
select.value = hls.currentLevel;
|
||||||
|
|
||||||
|
select.onchange = () => {
|
||||||
|
hls.currentLevel = Number(select.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.appendChild(select);
|
||||||
|
controls.insertBefore(wrapper, controls.children[4]);
|
||||||
|
}
|
||||||
|
|
||||||
function createAudioSelector(hls) {
|
function createAudioSelector(hls) {
|
||||||
if (!hls.audioTracks || hls.audioTracks.length < 2) return;
|
if (!hls.audioTracks || hls.audioTracks.length < 2) return;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ const AnimePlayer = (function() {
|
|||||||
let _skipIntervals = [];
|
let _skipIntervals = [];
|
||||||
let _progressUpdated = false;
|
let _progressUpdated = false;
|
||||||
|
|
||||||
|
let _animeTitle = "Anime";
|
||||||
|
let _rpcActive = false;
|
||||||
|
|
||||||
|
let _rawVideoData = null;
|
||||||
|
let _currentSubtitles = [];
|
||||||
|
|
||||||
let _localEntryId = null;
|
let _localEntryId = null;
|
||||||
let _totalEpisodes = 0;
|
let _totalEpisodes = 0;
|
||||||
|
|
||||||
@@ -25,7 +31,10 @@ const AnimePlayer = (function() {
|
|||||||
serverSelect: null,
|
serverSelect: null,
|
||||||
extSelect: null,
|
extSelect: null,
|
||||||
subDubToggle: null,
|
subDubToggle: null,
|
||||||
epTitle: null
|
epTitle: null,
|
||||||
|
prevBtn: null,
|
||||||
|
nextBtn: null,
|
||||||
|
mpvBtn: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(animeId, initialSource, isLocal, animeData) {
|
function init(animeId, initialSource, isLocal, animeData) {
|
||||||
@@ -34,85 +43,133 @@ const AnimePlayer = (function() {
|
|||||||
_isLocal = isLocal;
|
_isLocal = isLocal;
|
||||||
_malId = animeData.idMal || null;
|
_malId = animeData.idMal || null;
|
||||||
|
|
||||||
// Guardar total de episodios (por defecto 1000 si no hay info)
|
|
||||||
_totalEpisodes = animeData.episodes || 1000;
|
_totalEpisodes = animeData.episodes || 1000;
|
||||||
|
|
||||||
|
if (animeData.title) {
|
||||||
|
_animeTitle = animeData.title.romaji || animeData.title.english || animeData.title.native || animeData.title || "Anime";
|
||||||
|
}
|
||||||
|
|
||||||
_skipIntervals = [];
|
_skipIntervals = [];
|
||||||
_localEntryId = null;
|
_localEntryId = null;
|
||||||
|
|
||||||
// --- 1. REFERENCIAS BÁSICAS DEL DOM (Asegúrate de tener todas estas) ---
|
|
||||||
els.wrapper = document.getElementById('hero-wrapper');
|
els.wrapper = document.getElementById('hero-wrapper');
|
||||||
els.playerWrapper = document.getElementById('player-wrapper');
|
els.playerWrapper = document.getElementById('player-wrapper');
|
||||||
els.video = document.getElementById('player');
|
els.video = document.getElementById('player');
|
||||||
els.loader = document.getElementById('player-loading');
|
els.loader = document.getElementById('player-loading');
|
||||||
els.loaderText = document.getElementById('player-loading-text');
|
els.loaderText = document.getElementById('player-loading-text');
|
||||||
|
|
||||||
// --- 2. REFERENCIAS QUE FALTABAN (Causantes del error) ---
|
els.mpvBtn = document.getElementById('mpv-btn');
|
||||||
|
if (els.mpvBtn) els.mpvBtn.addEventListener('click', openInMPV);
|
||||||
|
|
||||||
els.serverSelect = document.getElementById('server-select');
|
els.serverSelect = document.getElementById('server-select');
|
||||||
els.extSelect = document.getElementById('extension-select');
|
els.extSelect = document.getElementById('extension-select');
|
||||||
els.subDubToggle = document.getElementById('sd-toggle');
|
els.subDubToggle = document.getElementById('sd-toggle');
|
||||||
els.epTitle = document.getElementById('player-episode-title');
|
els.epTitle = document.getElementById('player-episode-title');
|
||||||
|
|
||||||
// --- 3. REFERENCIAS DE NAVEGACIÓN (Nuevas) ---
|
|
||||||
els.prevBtn = document.getElementById('prev-ep-btn');
|
els.prevBtn = document.getElementById('prev-ep-btn');
|
||||||
els.nextBtn = document.getElementById('next-ep-btn');
|
els.nextBtn = document.getElementById('next-ep-btn');
|
||||||
|
|
||||||
const closeBtn = document.getElementById('close-player-btn');
|
const closeBtn = document.getElementById('close-player-btn');
|
||||||
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
||||||
|
|
||||||
// --- 4. CONFIGURACIÓN DE NAVEGACIÓN ---
|
|
||||||
if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1));
|
if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1));
|
||||||
if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1));
|
if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1));
|
||||||
|
|
||||||
// --- 5. CONFIGURACIÓN BOTÓN FLOTANTE (SKIP/NEXT) ---
|
|
||||||
if (!document.getElementById('skip-overlay-btn')) {
|
if (!document.getElementById('skip-overlay-btn')) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.id = 'skip-overlay-btn';
|
btn.id = 'skip-overlay-btn';
|
||||||
// No le ponemos contenido inicial, se maneja dinámicamente
|
|
||||||
const container = document.querySelector('.player-container');
|
const container = document.querySelector('.player-container');
|
||||||
container.appendChild(btn);
|
if(container) container.appendChild(btn);
|
||||||
_skipBtn = btn;
|
_skipBtn = btn;
|
||||||
} else {
|
} else {
|
||||||
_skipBtn = document.getElementById('skip-overlay-btn');
|
_skipBtn = document.getElementById('skip-overlay-btn');
|
||||||
}
|
}
|
||||||
|
if(_skipBtn) _skipBtn.onclick = () => handleOverlayClick();
|
||||||
|
|
||||||
// Listener único para el botón flotante
|
|
||||||
_skipBtn.onclick = () => handleOverlayClick();
|
|
||||||
|
|
||||||
// --- 6. LISTENERS DE CONTROLES (Audio, Servers, Ext) ---
|
|
||||||
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
||||||
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
||||||
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
||||||
|
|
||||||
// Cargar lista inicial
|
|
||||||
loadExtensionsList();
|
loadExtensionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openInMPV() {
|
||||||
|
if (!_rawVideoData) {
|
||||||
|
alert("No video loaded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
alert("You need to be logged in.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
title: `${_animeTitle} - Episode ${_currentEpisode}`,
|
||||||
|
video: _rawVideoData,
|
||||||
|
subtitles: _currentSubtitles,
|
||||||
|
chapters: _skipIntervals,
|
||||||
|
animeId: _animeId,
|
||||||
|
episode: _currentEpisode,
|
||||||
|
entrySource: _entrySource,
|
||||||
|
token: localStorage.getItem('token')
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/watch/mpv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("MPV Request Sent");
|
||||||
|
closePlayer();
|
||||||
|
} else {
|
||||||
|
console.error("MPV Request Failed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("MPV Error:", e);
|
||||||
|
} finally {
|
||||||
|
if(els.mpvBtn) {
|
||||||
|
els.mpvBtn.innerHTML = originalContent;
|
||||||
|
els.mpvBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) {
|
||||||
|
fetch("/api/rpc", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
details: _animeTitle,
|
||||||
|
state: `Episode ${_currentEpisode}`,
|
||||||
|
mode: "watching",
|
||||||
|
startTimestamp,
|
||||||
|
endTimestamp,
|
||||||
|
paused
|
||||||
|
})
|
||||||
|
}).catch(e => console.warn("RPC Error:", e));
|
||||||
|
}
|
||||||
|
|
||||||
function handleOverlayClick() {
|
function handleOverlayClick() {
|
||||||
if (!_skipBtn) return;
|
if (!_skipBtn) return;
|
||||||
|
|
||||||
// Si es modo "Next Episode"
|
|
||||||
if (_skipBtn.classList.contains('is-next')) {
|
if (_skipBtn.classList.contains('is-next')) {
|
||||||
playEpisode(_currentEpisode + 1);
|
playEpisode(_currentEpisode + 1);
|
||||||
}
|
} else if (_skipBtn.dataset.seekTo) {
|
||||||
// Si es modo "Skip Intro/Ending" (saltar tiempo)
|
|
||||||
else if (_skipBtn.dataset.seekTo) {
|
|
||||||
els.video.currentTime = parseFloat(_skipBtn.dataset.seekTo);
|
els.video.currentTime = parseFloat(_skipBtn.dataset.seekTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ocultar tras click
|
|
||||||
_skipBtn.classList.remove('visible');
|
_skipBtn.classList.remove('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLocalEntryId() {
|
async function getLocalEntryId() {
|
||||||
if (_localEntryId) return _localEntryId;
|
if (_localEntryId) return _localEntryId;
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const res = await fetch(`/api/library/anime/${_animeId}`);
|
const res = await fetch(`/api/library/anime/${_animeId}`);
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
_localEntryId = data.id;
|
_localEntryId = data.id;
|
||||||
|
|
||||||
return _localEntryId;
|
return _localEntryId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error fetching local ID:", e);
|
console.error("Error fetching local ID:", e);
|
||||||
@@ -122,26 +179,19 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
function playEpisode(episodeNumber) {
|
function playEpisode(episodeNumber) {
|
||||||
const targetEp = parseInt(episodeNumber);
|
const targetEp = parseInt(episodeNumber);
|
||||||
|
|
||||||
// Validar límites
|
|
||||||
if (targetEp < 1 || targetEp > _totalEpisodes) return;
|
if (targetEp < 1 || targetEp > _totalEpisodes) return;
|
||||||
|
|
||||||
_currentEpisode = targetEp;
|
_currentEpisode = targetEp;
|
||||||
|
|
||||||
// Actualizar UI
|
|
||||||
if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`;
|
if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`;
|
||||||
|
|
||||||
// Habilitar/Deshabilitar flechas de navegación
|
|
||||||
if(els.prevBtn) els.prevBtn.disabled = (_currentEpisode <= 1);
|
if(els.prevBtn) els.prevBtn.disabled = (_currentEpisode <= 1);
|
||||||
if(els.nextBtn) els.nextBtn.disabled = (_currentEpisode >= _totalEpisodes);
|
if(els.nextBtn) els.nextBtn.disabled = (_currentEpisode >= _totalEpisodes);
|
||||||
|
|
||||||
// Ocultar botón flotante al cambiar de cap
|
|
||||||
if(_skipBtn) {
|
if(_skipBtn) {
|
||||||
_skipBtn.classList.remove('visible');
|
_skipBtn.classList.remove('visible');
|
||||||
_skipBtn.classList.remove('is-next');
|
_skipBtn.classList.remove('is-next');
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL Update y lógica existente...
|
|
||||||
const newUrl = new URL(window.location);
|
const newUrl = new URL(window.location);
|
||||||
newUrl.searchParams.set('episode', targetEp);
|
newUrl.searchParams.set('episode', targetEp);
|
||||||
window.history.pushState({}, '', newUrl);
|
window.history.pushState({}, '', newUrl);
|
||||||
@@ -149,10 +199,11 @@ const AnimePlayer = (function() {
|
|||||||
if(els.playerWrapper) els.playerWrapper.style.display = 'block';
|
if(els.playerWrapper) els.playerWrapper.style.display = 'block';
|
||||||
document.body.classList.add('stop-scrolling');
|
document.body.classList.add('stop-scrolling');
|
||||||
|
|
||||||
// Pausar trailer fondo si existe
|
|
||||||
const trailer = document.querySelector('#trailer-player iframe');
|
const trailer = document.querySelector('#trailer-player iframe');
|
||||||
if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
||||||
|
|
||||||
|
_rpcActive = false;
|
||||||
|
|
||||||
if (els.extSelect.value === 'local') {
|
if (els.extSelect.value === 'local') {
|
||||||
loadStream();
|
loadStream();
|
||||||
return;
|
return;
|
||||||
@@ -175,6 +226,9 @@ const AnimePlayer = (function() {
|
|||||||
document.body.classList.remove('stop-scrolling');
|
document.body.classList.remove('stop-scrolling');
|
||||||
document.body.classList.remove('watch-mode');
|
document.body.classList.remove('watch-mode');
|
||||||
_skipIntervals = [];
|
_skipIntervals = [];
|
||||||
|
_rpcActive = false;
|
||||||
|
|
||||||
|
sendRPC({ paused: true });
|
||||||
|
|
||||||
const newUrl = new URL(window.location);
|
const newUrl = new URL(window.location);
|
||||||
newUrl.searchParams.delete('episode');
|
newUrl.searchParams.delete('episode');
|
||||||
@@ -195,7 +249,6 @@ const AnimePlayer = (function() {
|
|||||||
if (_isLocal && !extensions.includes('local')) extensions.push('local');
|
if (_isLocal && !extensions.includes('local')) extensions.push('local');
|
||||||
|
|
||||||
els.extSelect.innerHTML = '';
|
els.extSelect.innerHTML = '';
|
||||||
|
|
||||||
extensions.forEach(ext => {
|
extensions.forEach(ext => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = ext;
|
opt.value = ext;
|
||||||
@@ -215,13 +268,11 @@ const AnimePlayer = (function() {
|
|||||||
} else if (els.extSelect.value) {
|
} else if (els.extSelect.value) {
|
||||||
handleExtensionChange(false);
|
handleExtensionChange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) { console.error("Error loading extensions:", e); }
|
} catch (e) { console.error("Error loading extensions:", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExtensionChange(shouldPlay = true) {
|
async function handleExtensionChange(shouldPlay = true) {
|
||||||
const selectedExt = els.extSelect.value;
|
const selectedExt = els.extSelect.value;
|
||||||
|
|
||||||
if (selectedExt === 'local') {
|
if (selectedExt === 'local') {
|
||||||
els.subDubToggle.style.display = 'none';
|
els.subDubToggle.style.display = 'none';
|
||||||
els.serverSelect.style.display = 'none';
|
els.serverSelect.style.display = 'none';
|
||||||
@@ -230,7 +281,6 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading("Loading Extension Settings...");
|
setLoading("Loading Extension Settings...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/extensions/${selectedExt}/settings`);
|
const res = await fetch(`/api/extensions/${selectedExt}/settings`);
|
||||||
const settings = await res.json();
|
const settings = await res.json();
|
||||||
@@ -239,7 +289,6 @@ const AnimePlayer = (function() {
|
|||||||
setAudioMode('sub');
|
setAudioMode('sub');
|
||||||
|
|
||||||
els.serverSelect.innerHTML = '';
|
els.serverSelect.innerHTML = '';
|
||||||
|
|
||||||
if (settings.episodeServers && settings.episodeServers.length > 0) {
|
if (settings.episodeServers && settings.episodeServers.length > 0) {
|
||||||
settings.episodeServers.forEach(srv => {
|
settings.episodeServers.forEach(srv => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
@@ -258,7 +307,6 @@ const AnimePlayer = (function() {
|
|||||||
} else {
|
} else {
|
||||||
if(els.loader) els.loader.style.display = 'none';
|
if(els.loader) els.loader.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load settings:", error);
|
console.error("Failed to load settings:", error);
|
||||||
setLoading("Failed to load extension settings.");
|
setLoading("Failed to load extension settings.");
|
||||||
@@ -285,33 +333,34 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
async function loadStream() {
|
async function loadStream() {
|
||||||
if (!_currentEpisode) return;
|
if (!_currentEpisode) return;
|
||||||
|
|
||||||
_progressUpdated = false;
|
_progressUpdated = false;
|
||||||
|
|
||||||
setLoading("Fetching Stream...");
|
setLoading("Fetching Stream...");
|
||||||
|
|
||||||
|
_rawVideoData = null;
|
||||||
|
_currentSubtitles = [];
|
||||||
|
|
||||||
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
|
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
|
||||||
|
|
||||||
const currentExt = els.extSelect.value;
|
const currentExt = els.extSelect.value;
|
||||||
|
|
||||||
if (currentExt === 'local') {
|
if (currentExt === 'local') {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const localId = await getLocalEntryId();
|
const localId = await getLocalEntryId();
|
||||||
|
|
||||||
if (!localId) {
|
if (!localId) {
|
||||||
setLoading("Local entry not found in library.");
|
setLoading("Local entry not found in library.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localUrl = `/api/library/stream/anime/${localId}/${_currentEpisode}`;
|
const localUrl = `/api/library/stream/anime/${localId}/${_currentEpisode}`;
|
||||||
|
|
||||||
console.log("Playing Local:", localUrl);
|
_rawVideoData = {
|
||||||
initVideoPlayer(localUrl, 'mp4');
|
url: window.location.origin + localUrl,
|
||||||
|
headers: {}
|
||||||
|
};
|
||||||
|
_currentSubtitles = [];
|
||||||
|
|
||||||
|
initVideoPlayer(localUrl, 'mp4');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
setLoading("Local Error: " + e.message);
|
setLoading("Local Error: " + e.message);
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,11 +368,9 @@ const AnimePlayer = (function() {
|
|||||||
const server = els.serverSelect.value || "";
|
const server = els.serverSelect.value || "";
|
||||||
const sourceParam = `&source=${_entrySource}`;
|
const sourceParam = `&source=${_entrySource}`;
|
||||||
const extParam = `&ext=${currentExt}`;
|
const extParam = `&ext=${currentExt}`;
|
||||||
|
|
||||||
const url = `/api/watch/stream?animeId=${_animeId}&episode=${_currentEpisode}&server=${encodeURIComponent(server)}&category=${_audioMode}${extParam}${sourceParam}`;
|
const url = `/api/watch/stream?animeId=${_animeId}&episode=${_currentEpisode}&server=${encodeURIComponent(server)}&category=${_audioMode}${extParam}${sourceParam}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Fetching stream:', url);
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@@ -335,6 +382,11 @@ const AnimePlayer = (function() {
|
|||||||
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
|
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
|
||||||
const headers = data.headers || {};
|
const headers = data.headers || {};
|
||||||
|
|
||||||
|
_rawVideoData = {
|
||||||
|
url: source.url,
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
|
||||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
||||||
if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
|
if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
|
||||||
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
|
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
|
||||||
@@ -345,11 +397,15 @@ const AnimePlayer = (function() {
|
|||||||
src: `/api/proxy?url=${encodeURIComponent(sub.url)}`
|
src: `/api/proxy?url=${encodeURIComponent(sub.url)}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
initVideoPlayer(proxyUrl, source.type, subtitles);
|
_currentSubtitles = (source.subtitles || []).map(sub => ({
|
||||||
|
label: sub.language,
|
||||||
|
srclang: sub.id,
|
||||||
|
src: sub.url
|
||||||
|
}));
|
||||||
|
|
||||||
|
initVideoPlayer(proxyUrl, source.type, subtitles);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLoading("Stream Error: " + err.message);
|
setLoading("Stream Error: " + err.message);
|
||||||
console.error(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,29 +432,46 @@ const AnimePlayer = (function() {
|
|||||||
container.appendChild(newVideo);
|
container.appendChild(newVideo);
|
||||||
els.video = newVideo;
|
els.video = newVideo;
|
||||||
|
|
||||||
|
els.video.addEventListener("play", () => {
|
||||||
|
if (!els.video.duration) return;
|
||||||
|
const elapsed = Math.floor(els.video.currentTime);
|
||||||
|
const start = Math.floor(Date.now() / 1000) - elapsed;
|
||||||
|
const end = start + Math.floor(els.video.duration);
|
||||||
|
sendRPC({ startTimestamp: start, endTimestamp: end });
|
||||||
|
_rpcActive = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
els.video.addEventListener("pause", () => {
|
||||||
|
if (_rpcActive) sendRPC({ paused: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
els.video.addEventListener("seeked", () => {
|
||||||
|
if (els.video.paused || !_rpcActive) return;
|
||||||
|
const elapsed = Math.floor(els.video.currentTime);
|
||||||
|
const start = Math.floor(Date.now() / 1000) - elapsed;
|
||||||
|
const end = start + Math.floor(els.video.duration);
|
||||||
|
sendRPC({ startTimestamp: start, endTimestamp: end });
|
||||||
|
});
|
||||||
|
|
||||||
if (Hls.isSupported() && (type === 'm3u8' || url.includes('.m3u8'))) {
|
if (Hls.isSupported() && (type === 'm3u8' || url.includes('.m3u8'))) {
|
||||||
console.log("Using HLS.js");
|
|
||||||
hlsInstance = new Hls();
|
hlsInstance = new Hls();
|
||||||
hlsInstance.attachMedia(video);
|
hlsInstance.attachMedia(els.video);
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
hlsInstance.loadSource(url);
|
hlsInstance.loadSource(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
attachSubtitles(subtitles);
|
|
||||||
initPlyr();
|
initPlyr();
|
||||||
|
|
||||||
|
plyrInstance.on('ready', () => {
|
||||||
|
createAudioSelector(hlsInstance);
|
||||||
|
createQualitySelector(hlsInstance);
|
||||||
|
});
|
||||||
|
|
||||||
els.video.play().catch(() => {});
|
els.video.play().catch(() => {});
|
||||||
els.loader.style.display = 'none';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
hlsInstance.on(Hls.Events.ERROR, function (event, data) {
|
|
||||||
console.error("HLS Error:", data);
|
|
||||||
if (data.fatal) {
|
|
||||||
setLoading("Playback Error: " + data.details);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
els.video.src = url;
|
els.video.src = url;
|
||||||
attachSubtitles(subtitles);
|
attachSubtitles(subtitles);
|
||||||
@@ -423,6 +496,45 @@ const AnimePlayer = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createQualitySelector(hls) {
|
||||||
|
const levels = hls.levels;
|
||||||
|
if (!levels || !levels.length) return;
|
||||||
|
|
||||||
|
const plyrEl = els.video.closest('.plyr');
|
||||||
|
const controls = plyrEl.querySelector('.plyr__controls');
|
||||||
|
if (!controls) return;
|
||||||
|
|
||||||
|
if (controls.querySelector('#quality-select')) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'plyr__control';
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.id = 'quality-select';
|
||||||
|
|
||||||
|
// AUTO
|
||||||
|
const auto = document.createElement('option');
|
||||||
|
auto.value = -1;
|
||||||
|
auto.textContent = 'Auto';
|
||||||
|
select.appendChild(auto);
|
||||||
|
|
||||||
|
levels.forEach((l, i) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = i;
|
||||||
|
opt.textContent = `${l.height}p`;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
select.value = hls.currentLevel;
|
||||||
|
|
||||||
|
select.onchange = () => {
|
||||||
|
hls.currentLevel = Number(select.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.appendChild(select);
|
||||||
|
controls.insertBefore(wrapper, controls.children[4]);
|
||||||
|
}
|
||||||
|
|
||||||
function createAudioSelector(hls) {
|
function createAudioSelector(hls) {
|
||||||
if (!hls.audioTracks || hls.audioTracks.length < 2) return;
|
if (!hls.audioTracks || hls.audioTracks.length < 2) return;
|
||||||
|
|
||||||
@@ -457,6 +569,7 @@ const AnimePlayer = (function() {
|
|||||||
|
|
||||||
function initPlyr(enableAudio = false) {
|
function initPlyr(enableAudio = false) {
|
||||||
if (plyrInstance) return;
|
if (plyrInstance) return;
|
||||||
|
|
||||||
const settings = ['captions', 'quality', 'speed'];
|
const settings = ['captions', 'quality', 'speed'];
|
||||||
if (enableAudio) settings.unshift('audio');
|
if (enableAudio) settings.unshift('audio');
|
||||||
|
|
||||||
@@ -481,32 +594,21 @@ const AnimePlayer = (function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const container = document.querySelector('.player-container');
|
const container = document.querySelector('.player-container');
|
||||||
|
plyrInstance.on('controlshidden', () => container.classList.add('ui-hidden'));
|
||||||
|
plyrInstance.on('controlsshown', () => container.classList.remove('ui-hidden'));
|
||||||
|
|
||||||
plyrInstance.on('controlshidden', () => {
|
const tracks = els.video.textTracks;
|
||||||
container.classList.add('ui-hidden');
|
if (tracks && tracks.length) tracks[0].mode = 'showing';
|
||||||
});
|
|
||||||
|
|
||||||
plyrInstance.on('ready', () => {
|
plyrInstance.on('ready', () => {
|
||||||
if (hlsInstance) createAudioSelector(hlsInstance);
|
if (hlsInstance) createAudioSelector(hlsInstance);
|
||||||
});
|
});
|
||||||
|
|
||||||
plyrInstance.on('controlsshown', () => {
|
|
||||||
container.classList.remove('ui-hidden');
|
|
||||||
});
|
|
||||||
// ------------------------
|
|
||||||
|
|
||||||
const tracks = els.video.textTracks;
|
|
||||||
if (tracks && tracks.length) {
|
|
||||||
tracks[0].mode = 'showing';
|
|
||||||
}
|
|
||||||
|
|
||||||
plyrInstance.on('timeupdate', (event) => {
|
plyrInstance.on('timeupdate', (event) => {
|
||||||
const instance = event.detail.plyr;
|
const instance = event.detail.plyr;
|
||||||
if (!instance.duration || _progressUpdated) return;
|
if (!instance.duration || _progressUpdated) return;
|
||||||
|
|
||||||
const percentage = instance.currentTime / instance.duration;
|
const percentage = instance.currentTime / instance.duration;
|
||||||
if (percentage >= 0.8) {
|
if (percentage >= 0.8) {
|
||||||
console.log("Reaching 80% - Updating Progress...");
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
_progressUpdated = true;
|
_progressUpdated = true;
|
||||||
}
|
}
|
||||||
@@ -526,7 +628,6 @@ const AnimePlayer = (function() {
|
|||||||
const label = skip.type === 'op' ? 'Opening' : 'Ending';
|
const label = skip.type === 'op' ? 'Opening' : 'Ending';
|
||||||
vtt.push(`${toVtt(skip.startTime)} --> ${toVtt(skip.endTime)}`, label, '');
|
vtt.push(`${toVtt(skip.startTime)} --> ${toVtt(skip.endTime)}`, label, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
const blob = new Blob([vtt.join('\n')], { type: 'text/vtt' });
|
const blob = new Blob([vtt.join('\n')], { type: 'text/vtt' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const track = document.createElement('track');
|
const track = document.createElement('track');
|
||||||
@@ -553,14 +654,11 @@ const AnimePlayer = (function() {
|
|||||||
async function applyAniSkip(malId, episodeNumber) {
|
async function applyAniSkip(malId, episodeNumber) {
|
||||||
if (!malId) return;
|
if (!malId) return;
|
||||||
const duration = await waitForDuration(els.video);
|
const duration = await waitForDuration(els.video);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `https://api.aniskip.com/v2/skip-times/${malId}/${episodeNumber}` +
|
const url = `https://api.aniskip.com/v2/skip-times/${malId}/${episodeNumber}` +
|
||||||
`?types[]=op&types[]=ed&episodeLength=${Math.floor(duration)}`;
|
`?types[]=op&types[]=ed&episodeLength=${Math.floor(duration)}`;
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.found) return;
|
if (!data.found) return;
|
||||||
|
|
||||||
@@ -569,11 +667,8 @@ const AnimePlayer = (function() {
|
|||||||
endTime: item.interval.endTime,
|
endTime: item.interval.endTime,
|
||||||
type: item.skipType
|
type: item.skipType
|
||||||
}));
|
}));
|
||||||
|
|
||||||
injectAniSkipChapters(_skipIntervals);
|
injectAniSkipChapters(_skipIntervals);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => renderSkipMarkers(_skipIntervals));
|
||||||
renderSkipMarkers(_skipIntervals);
|
|
||||||
});
|
|
||||||
} catch (e) { console.error('AniSkip Error:', e); }
|
} catch (e) { console.error('AniSkip Error:', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,23 +683,18 @@ const AnimePlayer = (function() {
|
|||||||
el.className = `skip-marker ${skip.type}`;
|
el.className = `skip-marker ${skip.type}`;
|
||||||
const startPct = (skip.startTime / els.video.duration) * 100;
|
const startPct = (skip.startTime / els.video.duration) * 100;
|
||||||
const endPct = (skip.endTime / els.video.duration) * 100;
|
const endPct = (skip.endTime / els.video.duration) * 100;
|
||||||
const widthPct = endPct - startPct;
|
|
||||||
el.style.left = `${startPct}%`;
|
el.style.left = `${startPct}%`;
|
||||||
el.style.width = `${widthPct}%`;
|
el.style.width = `${endPct - startPct}%`;
|
||||||
progressContainer.appendChild(el);
|
progressContainer.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
monitorSkipButton(intervals);
|
monitorSkipButton(intervals);
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorSkipButton(intervals) {
|
function monitorSkipButton(intervals) {
|
||||||
if (!_skipBtn) return;
|
if (!_skipBtn) return;
|
||||||
|
|
||||||
// Limpiar listener anterior para no acumular
|
|
||||||
els.video.removeEventListener('timeupdate', checkTime);
|
els.video.removeEventListener('timeupdate', checkTime);
|
||||||
els.video.addEventListener('timeupdate', checkTime);
|
els.video.addEventListener('timeupdate', checkTime);
|
||||||
|
|
||||||
// Auto-Next al terminar el video
|
|
||||||
els.video.addEventListener('ended', () => {
|
els.video.addEventListener('ended', () => {
|
||||||
if (_currentEpisode < _totalEpisodes) playEpisode(_currentEpisode + 1);
|
if (_currentEpisode < _totalEpisodes) playEpisode(_currentEpisode + 1);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
@@ -612,57 +702,42 @@ const AnimePlayer = (function() {
|
|||||||
function checkTime() {
|
function checkTime() {
|
||||||
const ct = els.video.currentTime;
|
const ct = els.video.currentTime;
|
||||||
const duration = els.video.duration;
|
const duration = els.video.duration;
|
||||||
|
|
||||||
// 1. Revisar intervalos de AniSkip (Opening / Ending)
|
|
||||||
const activeInterval = intervals.find(i => ct >= i.startTime && ct <= i.endTime);
|
const activeInterval = intervals.find(i => ct >= i.startTime && ct <= i.endTime);
|
||||||
|
|
||||||
if (activeInterval) {
|
if (activeInterval) {
|
||||||
// Caso OPENING
|
|
||||||
if (activeInterval.type === 'op') {
|
if (activeInterval.type === 'op') {
|
||||||
showSkipButton('Skip Intro', activeInterval.endTime, false);
|
showSkipButton('Skip Intro', activeInterval.endTime, false);
|
||||||
return;
|
return;
|
||||||
}
|
} else if (activeInterval.type === 'ed') {
|
||||||
// Caso ENDING (Funciona como Next Episode)
|
|
||||||
else if (activeInterval.type === 'ed') {
|
|
||||||
// Si hay próximo episodio, mostramos botón Next
|
|
||||||
if (_currentEpisode < _totalEpisodes) {
|
if (_currentEpisode < _totalEpisodes) {
|
||||||
showSkipButton('Next Episode', null, true);
|
showSkipButton('Next Episode', null, true);
|
||||||
} else {
|
} else {
|
||||||
// Si es el último ep, solo saltamos el ending
|
|
||||||
showSkipButton('Skip Ending', activeInterval.endTime, false);
|
showSkipButton('Skip Ending', activeInterval.endTime, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback: Si NO estamos en un intervalo AniSkip,
|
|
||||||
// pero estamos cerca del final del video (ej. faltan 90s)
|
|
||||||
if (_currentEpisode < _totalEpisodes && (duration - ct) < 90 && (duration - ct) > 0) {
|
if (_currentEpisode < _totalEpisodes && (duration - ct) < 90 && (duration - ct) > 0) {
|
||||||
// Solo mostrar si no hay un intervalo activo impidiendo esto
|
|
||||||
if (!activeInterval) {
|
if (!activeInterval) {
|
||||||
showSkipButton('Next Episode', null, true);
|
showSkipButton('Next Episode', null, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si nada de lo anterior aplica, ocultar botón
|
|
||||||
_skipBtn.classList.remove('visible');
|
_skipBtn.classList.remove('visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSkipButton(text, seekTime, isNextAction) {
|
function showSkipButton(text, seekTime, isNextAction) {
|
||||||
if (!_skipBtn) return;
|
if (!_skipBtn) return;
|
||||||
|
|
||||||
_skipBtn.innerHTML = `${text} <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M13 17l5-5-5-5M6 17l5-5-5-5"/></svg>`;
|
_skipBtn.innerHTML = `${text} <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M13 17l5-5-5-5M6 17l5-5-5-5"/></svg>`;
|
||||||
|
|
||||||
if (isNextAction) {
|
if (isNextAction) {
|
||||||
_skipBtn.classList.add('is-next'); // Estilo morado
|
_skipBtn.classList.add('is-next');
|
||||||
_skipBtn.dataset.seekTo = ''; // No busca tiempo, cambia ep
|
_skipBtn.dataset.seekTo = '';
|
||||||
} else {
|
} else {
|
||||||
_skipBtn.classList.remove('is-next'); // Estilo blanco
|
_skipBtn.classList.remove('is-next');
|
||||||
_skipBtn.dataset.seekTo = seekTime;
|
_skipBtn.dataset.seekTo = seekTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
_skipBtn.classList.add('visible');
|
_skipBtn.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user