From f72fff982c420c6dd675e98f440974f10ee82422 Mon Sep 17 00:00:00 2001 From: lenafx Date: Fri, 2 Jan 2026 19:17:56 +0100 Subject: [PATCH] better handling on web player for local --- desktop/src/scripts/anime/player.js | 166 ++++++++++++++++++++++------ desktop/views/css/anime/player.css | 4 +- docker/src/scripts/anime/player.js | 166 ++++++++++++++++++++++------ docker/views/css/anime/player.css | 4 +- 4 files changed, 274 insertions(+), 66 deletions(-) diff --git a/desktop/src/scripts/anime/player.js b/desktop/src/scripts/anime/player.js index afd2376..cfe325c 100644 --- a/desktop/src/scripts/anime/player.js +++ b/desktop/src/scripts/anime/player.js @@ -121,8 +121,8 @@ const AnimePlayer = (function() { const closeBtn = document.getElementById('close-player-btn'); if(closeBtn) closeBtn.addEventListener('click', closePlayer); - if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1)); - if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1)); + if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); + if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); if (!document.getElementById('skip-overlay-btn')) { const btn = document.createElement('button'); @@ -249,17 +249,12 @@ const AnimePlayer = (function() { } } - function playEpisode(episodeNumber) { + async function playEpisode(episodeNumber) { const targetEp = parseInt(episodeNumber); if (targetEp < 1 || targetEp > _totalEpisodes) return; _currentEpisode = targetEp; - if (els.downloadBtn) { - els.downloadBtn.style.display = _isLocal ? 'none' : 'flex'; - resetDownloadButtonIcon(); - } - if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`; if(els.prevBtn) els.prevBtn.disabled = (_currentEpisode <= 1); if(els.nextBtn) els.nextBtn.disabled = (_currentEpisode >= _totalEpisodes); @@ -269,6 +264,7 @@ const AnimePlayer = (function() { _skipBtn.classList.remove('is-next'); } + // Actualizar URL y Botones const newUrl = new URL(window.location); newUrl.searchParams.set('episode', targetEp); window.history.pushState({}, '', newUrl); @@ -276,19 +272,100 @@ const AnimePlayer = (function() { if(els.playerWrapper) els.playerWrapper.style.display = 'block'; document.body.classList.add('stop-scrolling'); + // Pausar trailer si existe const trailer = document.querySelector('#trailer-player iframe'); if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); _rpcActive = false; - if (els.extSelect.value === 'local') { - loadStream(); - return; + // Mostrar carga mientras verificamos disponibilidad + setLoading("Checking availability..."); + + // --- 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 (els.serverSelect.options.length === 0) { - handleExtensionChange(true); - } else { + + 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) { + handleExtensionChange(true); + } else { + loadStream(); + } + } } } @@ -600,16 +677,18 @@ const AnimePlayer = (function() { async function handleExtensionChange(shouldPlay = true) { 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') { els.subDubToggle.style.display = 'none'; els.serverSelect.style.display = 'none'; if (shouldPlay && _currentEpisode > 0) loadStream(); return; } - if (els.manualMatchBtn) { - // No mostrar en local, sí en extensiones - els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex'; - } + _manualExtensionId = null; setLoading("Loading Extension Settings..."); @@ -679,40 +758,65 @@ const AnimePlayer = (function() { _isLocal = false; _rawVideoData = null; } + if (currentExt === 'local') { try { - 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; } - const check = await fetch(`/api/library/anime/${localId}/episodes`); - const eps = await check.json(); - if (!eps.includes(_currentEpisode)) { - els.extSelect.value = _entrySource; - return loadStream(); - } - - const ext = localUrl.split('.').pop().toLowerCase(); + const ext = targetUnit.format || targetUnit.name.split('.').pop().toLowerCase(); + // Validación de formato para reproductor web if (!['mp4'].includes(ext)) { 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'; return; } + const localUrl = `/api/library/stream/${targetUnit.id}`; + _rawVideoData = { - url: window.location.origin + localUrl, + url: localUrl, // O window.location.origin + localUrl si es relativa headers: {} }; _currentSubtitles = []; initVideoPlayer(localUrl, 'mp4'); + } catch(e) { + console.error(e); setLoading("Local Error: " + e.message); } return; diff --git a/desktop/views/css/anime/player.css b/desktop/views/css/anime/player.css index bb20b34..e7e39f4 100644 --- a/desktop/views/css/anime/player.css +++ b/desktop/views/css/anime/player.css @@ -76,7 +76,7 @@ body.stop-scrolling { display: flex; justify-content: space-between; align-items: center; - z-index: 20; + z-index: 60; 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 */ transition: opacity 0.3s ease; @@ -245,7 +245,7 @@ body.stop-scrolling { align-items: center; justify-content: center; cursor: pointer; - z-index: 30; /* Encima del video */ + z-index: 60; /* Encima del video */ transition: all 0.3s ease; opacity: 0; /* Invisibles por defecto */ } diff --git a/docker/src/scripts/anime/player.js b/docker/src/scripts/anime/player.js index afd2376..cfe325c 100644 --- a/docker/src/scripts/anime/player.js +++ b/docker/src/scripts/anime/player.js @@ -121,8 +121,8 @@ const AnimePlayer = (function() { const closeBtn = document.getElementById('close-player-btn'); if(closeBtn) closeBtn.addEventListener('click', closePlayer); - if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1)); - if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1)); + if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); + if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); if (!document.getElementById('skip-overlay-btn')) { const btn = document.createElement('button'); @@ -249,17 +249,12 @@ const AnimePlayer = (function() { } } - function playEpisode(episodeNumber) { + async function playEpisode(episodeNumber) { const targetEp = parseInt(episodeNumber); if (targetEp < 1 || targetEp > _totalEpisodes) return; _currentEpisode = targetEp; - if (els.downloadBtn) { - els.downloadBtn.style.display = _isLocal ? 'none' : 'flex'; - resetDownloadButtonIcon(); - } - if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`; if(els.prevBtn) els.prevBtn.disabled = (_currentEpisode <= 1); if(els.nextBtn) els.nextBtn.disabled = (_currentEpisode >= _totalEpisodes); @@ -269,6 +264,7 @@ const AnimePlayer = (function() { _skipBtn.classList.remove('is-next'); } + // Actualizar URL y Botones const newUrl = new URL(window.location); newUrl.searchParams.set('episode', targetEp); window.history.pushState({}, '', newUrl); @@ -276,19 +272,100 @@ const AnimePlayer = (function() { if(els.playerWrapper) els.playerWrapper.style.display = 'block'; document.body.classList.add('stop-scrolling'); + // Pausar trailer si existe const trailer = document.querySelector('#trailer-player iframe'); if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); _rpcActive = false; - if (els.extSelect.value === 'local') { - loadStream(); - return; + // Mostrar carga mientras verificamos disponibilidad + setLoading("Checking availability..."); + + // --- 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 (els.serverSelect.options.length === 0) { - handleExtensionChange(true); - } else { + + 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) { + handleExtensionChange(true); + } else { + loadStream(); + } + } } } @@ -600,16 +677,18 @@ const AnimePlayer = (function() { async function handleExtensionChange(shouldPlay = true) { 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') { els.subDubToggle.style.display = 'none'; els.serverSelect.style.display = 'none'; if (shouldPlay && _currentEpisode > 0) loadStream(); return; } - if (els.manualMatchBtn) { - // No mostrar en local, sí en extensiones - els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex'; - } + _manualExtensionId = null; setLoading("Loading Extension Settings..."); @@ -679,40 +758,65 @@ const AnimePlayer = (function() { _isLocal = false; _rawVideoData = null; } + if (currentExt === 'local') { try { - 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; } - const check = await fetch(`/api/library/anime/${localId}/episodes`); - const eps = await check.json(); - if (!eps.includes(_currentEpisode)) { - els.extSelect.value = _entrySource; - return loadStream(); - } - - const ext = localUrl.split('.').pop().toLowerCase(); + const ext = targetUnit.format || targetUnit.name.split('.').pop().toLowerCase(); + // Validación de formato para reproductor web if (!['mp4'].includes(ext)) { 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'; return; } + const localUrl = `/api/library/stream/${targetUnit.id}`; + _rawVideoData = { - url: window.location.origin + localUrl, + url: localUrl, // O window.location.origin + localUrl si es relativa headers: {} }; _currentSubtitles = []; initVideoPlayer(localUrl, 'mp4'); + } catch(e) { + console.error(e); setLoading("Local Error: " + e.message); } return; diff --git a/docker/views/css/anime/player.css b/docker/views/css/anime/player.css index bb20b34..e7e39f4 100644 --- a/docker/views/css/anime/player.css +++ b/docker/views/css/anime/player.css @@ -76,7 +76,7 @@ body.stop-scrolling { display: flex; justify-content: space-between; align-items: center; - z-index: 20; + z-index: 60; 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 */ transition: opacity 0.3s ease; @@ -245,7 +245,7 @@ body.stop-scrolling { align-items: center; justify-content: center; cursor: pointer; - z-index: 30; /* Encima del video */ + z-index: 60; /* Encima del video */ transition: all 0.3s ease; opacity: 0; /* Invisibles por defecto */ }