stream local files to player

This commit is contained in:
2026-01-05 13:21:54 +01:00
parent 5b5cedcc98
commit 5fd2341e8e
15 changed files with 1869 additions and 275 deletions

View File

@@ -882,7 +882,6 @@ const AnimePlayer = (function() {
if (els.video) els.video.playbackRate = parseFloat(value);
}
// Volvemos al menú principal para confirmar visualmente (opcional, estilo YouTube)
_settingsView = 'main';
buildSettingsPanel();
}
@@ -904,31 +903,41 @@ const AnimePlayer = (function() {
subtitleRenderer = null;
}
// Find ASS subtitle
const assSubtitle = _currentSubtitles.find(sub =>
(sub.src && sub.src.endsWith('.ass')) ||
(sub.label && sub.label.toLowerCase().includes('ass'))
);
const activeIdx = getActiveSubtitleIndex();
if (activeIdx === -1) return;
if (!assSubtitle) {
console.log('No ASS subtitles found in current list');
return;
}
const currentSub = _currentSubtitles[activeIdx];
if (!currentSub) return;
try {
console.log('Initializing JASSUB for:', assSubtitle.label);
const src = currentSub.src.toLowerCase();
const label = (currentSub.label || '').toLowerCase();
// Check if JASSUB global is available
if (window.SubtitleRenderer && typeof window.JASSUB !== 'undefined') {
// --- CAMBIO AQUÍ: Pasamos els.subtitlesCanvas ---
subtitleRenderer = new SubtitleRenderer(els.video, els.subtitlesCanvas);
await subtitleRenderer.init(assSubtitle.src);
} else {
console.warn('JASSUB library not loaded.');
// CASO 1: ASS (Usa JASSUB)
if (src.endsWith('.ass') || label.includes('ass')) {
try {
console.log('Initializing JASSUB for:', currentSub.label);
if (window.SubtitleRenderer && typeof window.JASSUB !== 'undefined') {
subtitleRenderer = new SubtitleRenderer(els.video, els.subtitlesCanvas);
await subtitleRenderer.init(currentSub.src);
}
} catch (e) {
console.error('JASSUB setup error:', e);
}
} catch (e) {
console.error('Subtitle renderer setup error:', e);
subtitleRenderer = null;
}
// CASO 2: SRT (Usa SimpleSubtitleRenderer)
else if (src.endsWith('.srt') || label.includes('srt')) {
try {
console.log('Initializing Simple Renderer for:', currentSub.label);
if (window.SimpleSubtitleRenderer) {
subtitleRenderer = new SimpleSubtitleRenderer(els.video, els.subtitlesCanvas);
await subtitleRenderer.loadSubtitles(currentSub.src);
}
} catch (e) {
console.error('Simple Renderer setup error:', e);
}
}
else {
console.log('Using native browser rendering for VTT');
}
}
@@ -991,13 +1000,15 @@ const AnimePlayer = (function() {
_rpcActive = false;
setLoading("Checking availability...");
// Check local availability
let shouldPlayLocal = false;
try {
const check = await fetch(`/api/library/${_animeId}/units`);
const data = await check.json();
const localUnit = data.units ? data.units.find(u => u.number === targetEp) : null;
if (localUnit) shouldPlayLocal = true;
if (localUnit && els.extSelect.value === 'local') {
shouldPlayLocal = true;
}
} catch (e) {
console.warn("Availability check failed:", e);
shouldPlayLocal = (els.extSelect.value === 'local');
@@ -1216,49 +1227,64 @@ const AnimePlayer = (function() {
if (currentExt === 'local') {
try {
const localId = await getLocalEntryId();
const check = await fetch(`/api/library/${_animeId}/units`);
const data = await check.json();
const targetUnit = data.units ? data.units.find(u => u.number === parseInt(_currentEpisode)) : null;
setLoading("Fetching Local Unit Data...");
// 1. Obtener las unidades locales para encontrar el ID del episodio específico
const unitsRes = await fetch(`/api/library/${_animeId}/units`);
if (!unitsRes.ok) throw new Error("Could not fetch local units");
const unitsData = await unitsRes.json();
const targetUnit = unitsData.units ? unitsData.units.find(u => u.number === parseInt(_currentEpisode)) : null;
if (!targetUnit) {
console.log(`Episode ${_currentEpisode} not found locally.`);
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;
throw new Error(`Episode ${_currentEpisode} not found in local library`);
}
const ext = targetUnit.format || targetUnit.name.split('.').pop().toLowerCase();
setLoading("Initializing HLS Stream...");
if (![''].includes(ext)) {
setLoading(`Local files are not supported on the web player yet. Use MPV.`);
_rawVideoData = {
url: targetUnit.path,
headers: {}
};
if (els.mpvBtn) els.mpvBtn.style.display = 'flex';
return;
const manifestRes = await fetch(`/api/library/stream/anime/${unitsData.entry_id}/${targetUnit.number}/manifest`);
if (!manifestRes.ok) throw new Error("Failed to generate stream manifest");
const manifestData = await manifestRes.json();
if (manifestData.chapters && manifestData.chapters.length > 0) {
_skipIntervals = manifestData.chapters.map(c => ({
startTime: c.start,
endTime: c.end,
type: c.title.toLowerCase().includes('op') ? 'op' :
c.title.toLowerCase().includes('ed') ? 'ed' : 'chapter'
}));
renderSkipMarkers(_skipIntervals);
monitorSkipButton(_skipIntervals);
}
const localUrl = `/api/library/stream/${targetUnit.id}`;
// 4. Mapear Subtítulos WebVTT
const subs = (manifestData.subtitles || []).map(s => ({
label: s.title || s.language || `Track ${s.index}`,
srclang: s.language || 'unk',
src: s.url // URL al endpoint de conversión VTT (.vtt)
}));
// 5. Guardar referencia para MPV o descargas
_rawVideoData = {
url: localUrl,
url: manifestData.masterPlaylist,
headers: {}
};
_currentSubtitles = [];
initVideoPlayer(localUrl, 'mp4');
// 6. Cargar en el reproductor (Hls.js gestionará los audios del master.m3u8)
initVideoPlayer(manifestData.masterPlaylist, 'm3u8', subs);
} catch(e) {
console.error(e);
console.error("Local HLS Error:", e);
setLoading("Local Error: " + e.message);
// Fallback: si falla, intentar cargar desde extensión online
const localOption = els.extSelect.querySelector('option[value="local"]');
if (localOption) localOption.remove();
const fallbackSource = (_entrySource === 'local') ? 'anilist' : _entrySource;
els.extSelect.value = fallbackSource;
handleExtensionChange(true);
}
return;
}
@@ -1530,23 +1556,19 @@ const AnimePlayer = (function() {
function renderSkipMarkers(intervals) {
if (!els.progressContainer || !els.video.duration) return;
els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove());
const duration = els.video.duration;
intervals.forEach(skip => {
const startPct = (skip.startTime / duration) * 100;
const endPct = (skip.endTime / duration) * 100;
const startPct = (skip.startTime / els.video.duration) * 100;
const endPct = (skip.endTime / els.video.duration) * 100;
const range = document.createElement('div');
range.className = `skip-range ${skip.type}`; // 'op' o 'ed'
range.className = `skip-range ${skip.type}`;
range.style.left = `${startPct}%`;
range.style.width = `${endPct - startPct}%`;
els.progressContainer.appendChild(range);
createCut(startPct);
createCut(endPct);
});
}