player now supports quality settings

This commit is contained in:
2025-12-31 15:42:34 +01:00
parent 776079d5c6
commit 7b3c559d03
2 changed files with 226 additions and 109 deletions

View File

@@ -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;

View File

@@ -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();
els.video.play().catch(() => {}); plyrInstance.on('ready', () => {
els.loader.style.display = 'none'; createAudioSelector(hlsInstance);
createQualitySelector(hlsInstance);
}); });
hlsInstance.on(Hls.Events.ERROR, function (event, data) { els.video.play().catch(() => {});
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');
} }