better handling on web player for local

This commit is contained in:
2026-01-02 19:17:56 +01:00
parent 942cab2f25
commit f72fff982c
4 changed files with 274 additions and 66 deletions

View File

@@ -121,8 +121,8 @@ const AnimePlayer = (function() {
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);
if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1)); if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1);
if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1)); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1);
if (!document.getElementById('skip-overlay-btn')) { if (!document.getElementById('skip-overlay-btn')) {
const btn = document.createElement('button'); const btn = document.createElement('button');
@@ -249,17 +249,12 @@ const AnimePlayer = (function() {
} }
} }
function playEpisode(episodeNumber) { async function playEpisode(episodeNumber) {
const targetEp = parseInt(episodeNumber); const targetEp = parseInt(episodeNumber);
if (targetEp < 1 || targetEp > _totalEpisodes) return; if (targetEp < 1 || targetEp > _totalEpisodes) return;
_currentEpisode = targetEp; _currentEpisode = targetEp;
if (els.downloadBtn) {
els.downloadBtn.style.display = _isLocal ? 'none' : 'flex';
resetDownloadButtonIcon();
}
if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`; if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`;
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);
@@ -269,6 +264,7 @@ const AnimePlayer = (function() {
_skipBtn.classList.remove('is-next'); _skipBtn.classList.remove('is-next');
} }
// Actualizar URL y Botones
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);
@@ -276,21 +272,102 @@ 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 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; _rpcActive = false;
if (els.extSelect.value === 'local') { // Mostrar carga mientras verificamos disponibilidad
loadStream(); setLoading("Checking availability...");
return;
// --- LÓGICA DE AUTO-DETECCIÓN LOCAL ---
let shouldPlayLocal = false;
try {
// Consultamos a la API si ESTE episodio específico existe localmente
const check = await fetch(`/api/library/${_animeId}/units`);
const data = await check.json();
// Buscamos el episodio en la respuesta
const localUnit = data.units ? data.units.find(u => u.number === targetEp) : null;
if (localUnit) {
shouldPlayLocal = true;
} }
} catch (e) {
console.warn("Availability check failed:", e);
// Si falla el check (ej: error de red), mantenemos el modo actual por seguridad
shouldPlayLocal = (els.extSelect.value === 'local');
}
if (shouldPlayLocal) {
els.manualMatchBtn.style.display = 'none';
}
else{
els.manualMatchBtn.style.display = 'flex';
}
if (shouldPlayLocal) {
// CASO 1: El episodio EXISTE localmente
console.log(`Episode ${targetEp} found locally. Switching to Local.`);
// 1. Asegurar que 'local' está en el dropdown y seleccionarlo
let localOption = els.extSelect.querySelector('option[value="local"]');
if (!localOption) {
localOption = document.createElement('option');
localOption.value = 'local';
localOption.innerText = 'Local';
els.extSelect.appendChild(localOption);
}
els.extSelect.value = 'local';
// 2. Ocultar controles que no son para local
if(els.subDubToggle) els.subDubToggle.style.display = 'none';
if(els.serverSelect) els.serverSelect.style.display = 'none';
// 3. Cargar stream
loadStream();
} else {
// CASO 2: El episodio NO existe localmente (es Remoto)
// Si estábamos en modo 'local', tenemos que cambiar a una extensión
if (els.extSelect.value === 'local') {
console.log(`Episode ${targetEp} not local. Switching to Extension.`);
// 1. Quitar la opción local para evitar errores (opcional)
const localOption = els.extSelect.querySelector('option[value="local"]');
if (localOption) localOption.remove();
// 2. Restaurar la fuente original (Anilist, Gogo, etc)
// Usamos _entrySource, pero si era 'local', forzamos 'anilist' para evitar bucle
let fallbackSource = (_entrySource !== 'local') ? _entrySource : 'anilist';
// Verificar si esa fuente existe en el select, si no, usar la primera disponible
if (!els.extSelect.querySelector(`option[value="${fallbackSource}"]`)) {
if (els.extSelect.options.length > 0) {
fallbackSource = els.extSelect.options[0].value;
}
}
els.extSelect.value = fallbackSource;
// 3. Como cambiamos de Local -> Extensión, necesitamos cargar los servidores de nuevo
// handleExtensionChange(true) se encarga de cargar settings, servers y luego hacer play.
handleExtensionChange(true);
} else {
// Ya estábamos en modo remoto.
// Si por alguna razón no hay servidores cargados, recargamos la extensión.
if (els.serverSelect.options.length === 0) { if (els.serverSelect.options.length === 0) {
handleExtensionChange(true); handleExtensionChange(true);
} else { } else {
loadStream(); loadStream();
} }
} }
}
}
async function downloadEpisode() { async function downloadEpisode() {
if (!_rawVideoData || !_rawVideoData.url) { if (!_rawVideoData || !_rawVideoData.url) {
@@ -600,16 +677,18 @@ const AnimePlayer = (function() {
async function handleExtensionChange(shouldPlay = true) { async function handleExtensionChange(shouldPlay = true) {
const selectedExt = els.extSelect.value; const selectedExt = els.extSelect.value;
if (els.manualMatchBtn) {
// Si es local, lo ocultamos. Si es extensión, lo mostramos.
els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex';
}
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';
if (shouldPlay && _currentEpisode > 0) loadStream(); if (shouldPlay && _currentEpisode > 0) loadStream();
return; return;
} }
if (els.manualMatchBtn) {
// No mostrar en local, sí en extensiones
els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex';
}
_manualExtensionId = null; _manualExtensionId = null;
setLoading("Loading Extension Settings..."); setLoading("Loading Extension Settings...");
@@ -679,40 +758,65 @@ const AnimePlayer = (function() {
_isLocal = false; _isLocal = false;
_rawVideoData = null; _rawVideoData = null;
} }
if (currentExt === 'local') { if (currentExt === 'local') {
try { try {
const localId = await getLocalEntryId(); const localId = await getLocalEntryId();
if (!localId) {
setLoading("Local entry not found in library."); // Paso 1: Obtener lista de archivos
const check = await fetch(`/api/library/${_animeId}/units`);
const data = await check.json();
// Paso 2: Buscar si el episodio actual existe
const targetUnit = data.units ? data.units.find(u => u.number === parseInt(_currentEpisode)) : null;
// Paso 3: Si NO existe localmente
if (!targetUnit) {
console.log(`Episode ${_currentEpisode} not found locally. Removing option.`);
const localOption = els.extSelect.querySelector('option[value="local"]');
if (localOption) localOption.remove();
const fallbackSource = (_entrySource === 'local') ? 'anilist' : _entrySource;
if (els.extSelect.querySelector(`option[value="${fallbackSource}"]`)) {
els.extSelect.value = fallbackSource;
} else if (els.extSelect.options.length > 0) {
els.extSelect.selectedIndex = 0;
}
handleExtensionChange(true);
return; return;
} }
const check = await fetch(`/api/library/anime/${localId}/episodes`);
const eps = await check.json();
if (!eps.includes(_currentEpisode)) { const ext = targetUnit.format || targetUnit.name.split('.').pop().toLowerCase();
els.extSelect.value = _entrySource;
return loadStream();
}
const ext = localUrl.split('.').pop().toLowerCase();
// Validación de formato para reproductor web
if (!['mp4'].includes(ext)) { if (!['mp4'].includes(ext)) {
setLoading( setLoading(
`Currently the web player only supports mp4 files.` `Format '${ext}' not supported in web player. Use MPV.`
); );
// Aseguramos que el botón de MPV tenga la data necesaria aunque falle el web player
_rawVideoData = {
url: targetUnit.path, // O la URL de stream correspondiente
headers: {}
};
if (els.mpvBtn) els.mpvBtn.style.display = 'flex'; if (els.mpvBtn) els.mpvBtn.style.display = 'flex';
return; return;
} }
const localUrl = `/api/library/stream/${targetUnit.id}`;
_rawVideoData = { _rawVideoData = {
url: window.location.origin + localUrl, url: localUrl, // O window.location.origin + localUrl si es relativa
headers: {} headers: {}
}; };
_currentSubtitles = []; _currentSubtitles = [];
initVideoPlayer(localUrl, 'mp4'); initVideoPlayer(localUrl, 'mp4');
} catch(e) { } catch(e) {
console.error(e);
setLoading("Local Error: " + e.message); setLoading("Local Error: " + e.message);
} }
return; return;

View File

@@ -76,7 +76,7 @@ body.stop-scrolling {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
z-index: 20; z-index: 60;
background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%); background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%);
pointer-events: none; /* Permite clickear el video a través del header vacío */ pointer-events: none; /* Permite clickear el video a través del header vacío */
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
@@ -245,7 +245,7 @@ body.stop-scrolling {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
z-index: 30; /* Encima del video */ z-index: 60; /* Encima del video */
transition: all 0.3s ease; transition: all 0.3s ease;
opacity: 0; /* Invisibles por defecto */ opacity: 0; /* Invisibles por defecto */
} }

View File

@@ -121,8 +121,8 @@ const AnimePlayer = (function() {
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);
if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1)); if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1);
if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1)); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1);
if (!document.getElementById('skip-overlay-btn')) { if (!document.getElementById('skip-overlay-btn')) {
const btn = document.createElement('button'); const btn = document.createElement('button');
@@ -249,17 +249,12 @@ const AnimePlayer = (function() {
} }
} }
function playEpisode(episodeNumber) { async function playEpisode(episodeNumber) {
const targetEp = parseInt(episodeNumber); const targetEp = parseInt(episodeNumber);
if (targetEp < 1 || targetEp > _totalEpisodes) return; if (targetEp < 1 || targetEp > _totalEpisodes) return;
_currentEpisode = targetEp; _currentEpisode = targetEp;
if (els.downloadBtn) {
els.downloadBtn.style.display = _isLocal ? 'none' : 'flex';
resetDownloadButtonIcon();
}
if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`; if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`;
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);
@@ -269,6 +264,7 @@ const AnimePlayer = (function() {
_skipBtn.classList.remove('is-next'); _skipBtn.classList.remove('is-next');
} }
// Actualizar URL y Botones
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);
@@ -276,21 +272,102 @@ 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 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; _rpcActive = false;
if (els.extSelect.value === 'local') { // Mostrar carga mientras verificamos disponibilidad
loadStream(); setLoading("Checking availability...");
return;
// --- LÓGICA DE AUTO-DETECCIÓN LOCAL ---
let shouldPlayLocal = false;
try {
// Consultamos a la API si ESTE episodio específico existe localmente
const check = await fetch(`/api/library/${_animeId}/units`);
const data = await check.json();
// Buscamos el episodio en la respuesta
const localUnit = data.units ? data.units.find(u => u.number === targetEp) : null;
if (localUnit) {
shouldPlayLocal = true;
} }
} catch (e) {
console.warn("Availability check failed:", e);
// Si falla el check (ej: error de red), mantenemos el modo actual por seguridad
shouldPlayLocal = (els.extSelect.value === 'local');
}
if (shouldPlayLocal) {
els.manualMatchBtn.style.display = 'none';
}
else{
els.manualMatchBtn.style.display = 'flex';
}
if (shouldPlayLocal) {
// CASO 1: El episodio EXISTE localmente
console.log(`Episode ${targetEp} found locally. Switching to Local.`);
// 1. Asegurar que 'local' está en el dropdown y seleccionarlo
let localOption = els.extSelect.querySelector('option[value="local"]');
if (!localOption) {
localOption = document.createElement('option');
localOption.value = 'local';
localOption.innerText = 'Local';
els.extSelect.appendChild(localOption);
}
els.extSelect.value = 'local';
// 2. Ocultar controles que no son para local
if(els.subDubToggle) els.subDubToggle.style.display = 'none';
if(els.serverSelect) els.serverSelect.style.display = 'none';
// 3. Cargar stream
loadStream();
} else {
// CASO 2: El episodio NO existe localmente (es Remoto)
// Si estábamos en modo 'local', tenemos que cambiar a una extensión
if (els.extSelect.value === 'local') {
console.log(`Episode ${targetEp} not local. Switching to Extension.`);
// 1. Quitar la opción local para evitar errores (opcional)
const localOption = els.extSelect.querySelector('option[value="local"]');
if (localOption) localOption.remove();
// 2. Restaurar la fuente original (Anilist, Gogo, etc)
// Usamos _entrySource, pero si era 'local', forzamos 'anilist' para evitar bucle
let fallbackSource = (_entrySource !== 'local') ? _entrySource : 'anilist';
// Verificar si esa fuente existe en el select, si no, usar la primera disponible
if (!els.extSelect.querySelector(`option[value="${fallbackSource}"]`)) {
if (els.extSelect.options.length > 0) {
fallbackSource = els.extSelect.options[0].value;
}
}
els.extSelect.value = fallbackSource;
// 3. Como cambiamos de Local -> Extensión, necesitamos cargar los servidores de nuevo
// handleExtensionChange(true) se encarga de cargar settings, servers y luego hacer play.
handleExtensionChange(true);
} else {
// Ya estábamos en modo remoto.
// Si por alguna razón no hay servidores cargados, recargamos la extensión.
if (els.serverSelect.options.length === 0) { if (els.serverSelect.options.length === 0) {
handleExtensionChange(true); handleExtensionChange(true);
} else { } else {
loadStream(); loadStream();
} }
} }
}
}
async function downloadEpisode() { async function downloadEpisode() {
if (!_rawVideoData || !_rawVideoData.url) { if (!_rawVideoData || !_rawVideoData.url) {
@@ -600,16 +677,18 @@ const AnimePlayer = (function() {
async function handleExtensionChange(shouldPlay = true) { async function handleExtensionChange(shouldPlay = true) {
const selectedExt = els.extSelect.value; const selectedExt = els.extSelect.value;
if (els.manualMatchBtn) {
// Si es local, lo ocultamos. Si es extensión, lo mostramos.
els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex';
}
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';
if (shouldPlay && _currentEpisode > 0) loadStream(); if (shouldPlay && _currentEpisode > 0) loadStream();
return; return;
} }
if (els.manualMatchBtn) {
// No mostrar en local, sí en extensiones
els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex';
}
_manualExtensionId = null; _manualExtensionId = null;
setLoading("Loading Extension Settings..."); setLoading("Loading Extension Settings...");
@@ -679,40 +758,65 @@ const AnimePlayer = (function() {
_isLocal = false; _isLocal = false;
_rawVideoData = null; _rawVideoData = null;
} }
if (currentExt === 'local') { if (currentExt === 'local') {
try { try {
const localId = await getLocalEntryId(); const localId = await getLocalEntryId();
if (!localId) {
setLoading("Local entry not found in library."); // Paso 1: Obtener lista de archivos
const check = await fetch(`/api/library/${_animeId}/units`);
const data = await check.json();
// Paso 2: Buscar si el episodio actual existe
const targetUnit = data.units ? data.units.find(u => u.number === parseInt(_currentEpisode)) : null;
// Paso 3: Si NO existe localmente
if (!targetUnit) {
console.log(`Episode ${_currentEpisode} not found locally. Removing option.`);
const localOption = els.extSelect.querySelector('option[value="local"]');
if (localOption) localOption.remove();
const fallbackSource = (_entrySource === 'local') ? 'anilist' : _entrySource;
if (els.extSelect.querySelector(`option[value="${fallbackSource}"]`)) {
els.extSelect.value = fallbackSource;
} else if (els.extSelect.options.length > 0) {
els.extSelect.selectedIndex = 0;
}
handleExtensionChange(true);
return; return;
} }
const check = await fetch(`/api/library/anime/${localId}/episodes`);
const eps = await check.json();
if (!eps.includes(_currentEpisode)) { const ext = targetUnit.format || targetUnit.name.split('.').pop().toLowerCase();
els.extSelect.value = _entrySource;
return loadStream();
}
const ext = localUrl.split('.').pop().toLowerCase();
// Validación de formato para reproductor web
if (!['mp4'].includes(ext)) { if (!['mp4'].includes(ext)) {
setLoading( setLoading(
`Currently the web player only supports mp4 files.` `Format '${ext}' not supported in web player. Use MPV.`
); );
// Aseguramos que el botón de MPV tenga la data necesaria aunque falle el web player
_rawVideoData = {
url: targetUnit.path, // O la URL de stream correspondiente
headers: {}
};
if (els.mpvBtn) els.mpvBtn.style.display = 'flex'; if (els.mpvBtn) els.mpvBtn.style.display = 'flex';
return; return;
} }
const localUrl = `/api/library/stream/${targetUnit.id}`;
_rawVideoData = { _rawVideoData = {
url: window.location.origin + localUrl, url: localUrl, // O window.location.origin + localUrl si es relativa
headers: {} headers: {}
}; };
_currentSubtitles = []; _currentSubtitles = [];
initVideoPlayer(localUrl, 'mp4'); initVideoPlayer(localUrl, 'mp4');
} catch(e) { } catch(e) {
console.error(e);
setLoading("Local Error: " + e.message); setLoading("Local Error: " + e.message);
} }
return; return;

View File

@@ -76,7 +76,7 @@ body.stop-scrolling {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
z-index: 20; z-index: 60;
background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%); background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%);
pointer-events: none; /* Permite clickear el video a través del header vacío */ pointer-events: none; /* Permite clickear el video a través del header vacío */
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
@@ -245,7 +245,7 @@ body.stop-scrolling {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
z-index: 30; /* Encima del video */ z-index: 60; /* Encima del video */
transition: all 0.3s ease; transition: all 0.3s ease;
opacity: 0; /* Invisibles por defecto */ opacity: 0; /* Invisibles por defecto */
} }