support for subs on local files

This commit is contained in:
2026-01-06 18:55:03 +01:00
parent 6c9f021e8d
commit ba05e08e71
12 changed files with 760 additions and 810 deletions

View File

@@ -857,27 +857,15 @@ const AnimePlayer = (function() {
if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
} else if (type === 'subtitle') {
const idx = parseInt(value);
_activeSubtitleIndex = idx; // <--- ACTUALIZAMOS EL ESTADO AQUÍ
_activeSubtitleIndex = idx;
// 1. Lógica nativa (para mantener compatibilidad interna)
if (els.video && els.video.textTracks) {
Array.from(els.video.textTracks).forEach((track, i) => {
// Si usamos JASSUB, ocultamos la nativa. Si no, mostramos la seleccionada.
track.mode = (subtitleRenderer && idx !== -1) ? 'hidden' : ((i === idx) ? 'showing' : 'hidden');
track.mode = 'hidden';
});
}
// 2. Lógica de JASSUB
if (subtitleRenderer) {
if (idx === -1) {
subtitleRenderer.dispose();
} else {
const sub = _currentSubtitles[idx];
if (sub) {
subtitleRenderer.setTrack(sub.src);
}
}
}
initSubtitleRenderer();
} else if (type === 'speed') {
if (els.video) els.video.playbackRate = parseFloat(value);
}
@@ -1185,20 +1173,6 @@ const AnimePlayer = (function() {
if(els.loader) els.loader.style.display = 'flex';
}
async function getLocalEntryId() {
if (_localEntryId) return _localEntryId;
try {
const res = await fetch(`/api/library/anime/${_animeId}`);
if (!res.ok) return null;
const data = await res.json();
_localEntryId = data.id;
return _localEntryId;
} catch (e) {
console.error("Error fetching local ID:", e);
return null;
}
}
async function loadStream() {
if (!_currentEpisode) return;
_progressUpdated = false;
@@ -1225,16 +1199,21 @@ const AnimePlayer = (function() {
_rawVideoData = null;
}
// En la función loadStream(), cuando detectas local:
if (currentExt === 'local') {
try {
setLoading("Fetching Local Unit Data...");
// 1. Obtener las unidades locales para encontrar el ID del episodio específico
// 1. Obtener unidades
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;
_localEntryId = unitsData.entry_id;
const targetUnit = unitsData.units ?
unitsData.units.find(u => u.number === parseInt(_currentEpisode)) : null;
if (!targetUnit) {
throw new Error(`Episode ${_currentEpisode} not found in local library`);
@@ -1242,46 +1221,70 @@ const AnimePlayer = (function() {
setLoading("Initializing HLS Stream...");
const manifestRes = await fetch(`/api/library/stream/anime/${unitsData.entry_id}/${targetUnit.number}/manifest`);
// 2. Obtener Manifest
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();
// DEBUG: Verificar que llega aquí
console.log("Manifest Loaded:", manifestData);
// 3. Chapters
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'
type: (c.title || '').toLowerCase().includes('op') ? 'op' :
(c.title || '').toLowerCase().includes('ed') ? 'ed' : 'chapter'
}));
renderSkipMarkers(_skipIntervals);
monitorSkipButton(_skipIntervals);
}
// 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)
}));
// 4. CORRECCIÓN DE SUBTÍTULOS
const rawSubs = manifestData.subtitles || [];
console.log("Raw Subtitles from JSON:", rawSubs);
const subs = rawSubs.map((s, index) => {
// Limpieza segura del código de idioma (ej. "English" -> "en")
let langCode = 'en';
if (s.language && typeof s.language === 'string' && s.language.length >= 2) {
langCode = s.language.substring(0, 2).toLowerCase();
}
// Título legible (Prioridad: Title > Language > Track #)
const label = s.title || s.language || `Track ${index + 1}`;
return {
label: label,
srclang: langCode,
src: s.url // Usamos la URL directa del JSON local
};
});
// ACTUALIZACIÓN CRÍTICA DEL ESTADO GLOBAL
_currentSubtitles = subs;
console.log("Processed Subtitles:", _currentSubtitles);
// 5. Guardar referencia para MPV o descargas
_rawVideoData = {
url: manifestData.masterPlaylist,
headers: {}
};
// 6. Cargar en el reproductor (Hls.js gestionará los audios del master.m3u8)
// Inicializar video pasando explícitamente los subs procesados
initVideoPlayer(manifestData.masterPlaylist, 'm3u8', subs);
} catch(e) {
console.error("Local HLS Error:", e);
setLoading("Local Error: " + e.message);
// Fallback: si falla, intentar cargar desde extensión online
// Fallback logic...
const localOption = els.extSelect.querySelector('option[value="local"]');
if (localOption) localOption.remove();
const fallbackSource = (_entrySource === 'local') ? 'anilist' : _entrySource;
els.extSelect.value = fallbackSource;
handleExtensionChange(true);

View File

@@ -1,60 +1,46 @@
const BASE_PATH = '/src/scripts/jassub/';
class SubtitleRenderer {
// 1. Aceptamos 'canvas' en el constructor
constructor(video, canvas) {
this.video = video;
this.canvas = canvas;
this.instance = null;
this.currentUrl = null;
}
async init(subtitleUrl) {
if (!this.video || !this.canvas) return; // 2. Verificamos canvas
if (!this.video || !this.canvas) return;
this.dispose();
const finalUrl = subtitleUrl.includes('/api/proxy')
? subtitleUrl
: `/api/proxy?url=${encodeURIComponent(subtitleUrl)}`;
this.currentUrl = finalUrl;
try {
this.instance = new JASSUB({
video: this.video,
canvas: this.canvas,
subUrl: finalUrl,
workerUrl: `${BASE_PATH}jassub-worker.js`,
wasmUrl: `${BASE_PATH}jassub-worker.wasm`,
modernWasmUrl: `${BASE_PATH}jassub-worker-modern.wasm`,
blendMode: 'js',
asyncRender: true,
onDemand: true,
targetFps: 60,
debug: false
});
console.log('JASSUB initialized for:', finalUrl);
} catch (e) {
console.error("JASSUB Init Error:", e);
}
}
resize() {
if (this.instance && this.instance.resize) {
this.instance.resize();
}
}
setTrack(url) {
const finalUrl = url.includes('/api/proxy')
? url
: `/api/proxy?url=${encodeURIComponent(url)}`;
if (this.instance) {
this.instance.setTrackByUrl(finalUrl);
this.currentUrl = finalUrl;
@@ -62,7 +48,6 @@ class SubtitleRenderer {
this.init(url);
}
}
dispose() {
if (this.instance) {
try {
@@ -74,8 +59,6 @@ class SubtitleRenderer {
}
}
}
// Simple Renderer remains unchanged for SRT/VTT (Non-ASS)
class SimpleSubtitleRenderer {
constructor(video, canvas) {
this.video = video;
@@ -83,11 +66,9 @@ class SimpleSubtitleRenderer {
this.ctx = canvas.getContext('2d');
this.cues = [];
this.destroyed = false;
this.setupCanvas();
this.video.addEventListener('timeupdate', () => this.render());
}
setupCanvas() {
const updateSize = () => {
if (!this.video || !this.canvas) return;
@@ -99,19 +80,31 @@ class SimpleSubtitleRenderer {
window.addEventListener('resize', updateSize);
this.resizeHandler = updateSize;
}
async loadSubtitles(url) {
try {
const response = await fetch(`/api/proxy?url=${encodeURIComponent(url)}`);
let finalUrl = url;
const isLocal = url.startsWith('/');
const isAlreadyProxied = url.includes('/api/proxy');
if (!isLocal && !isAlreadyProxied && (url.startsWith('http:') || url.startsWith('https:'))) {
finalUrl = `/api/proxy?url=${encodeURIComponent(url)}`;
}
console.log('Fetching subtitles from:', finalUrl);
const response = await fetch(finalUrl);
if (!response.ok) throw new Error(`Status: ${response.status}`);
const text = await response.text();
this.cues = this.parseSRT(text);
} catch (error) {
console.error('Failed to load subtitles:', error);
}
}
setTrack(url) {
this.cues = [];
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.loadSubtitles(url);
}
parseSRT(srtText) {
const blocks = srtText.trim().split('\n\n');
const normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const blocks = normalizedText.trim().split('\n\n');
return blocks.map(block => {
const lines = block.split('\n');
if (lines.length < 3) return null;
@@ -119,11 +112,12 @@ class SimpleSubtitleRenderer {
if (!timeMatch) return null;
const start = parseInt(timeMatch[1]) * 3600 + parseInt(timeMatch[2]) * 60 + parseInt(timeMatch[3]) + parseInt(timeMatch[4]) / 1000;
const end = parseInt(timeMatch[5]) * 3600 + parseInt(timeMatch[6]) * 60 + parseInt(timeMatch[7]) + parseInt(timeMatch[8]) / 1000;
const text = lines.slice(2).join('\n');
let text = lines.slice(2).join('\n');
text = text.replace(/<[^>]*>/g, '');
text = text.replace(/\{[^}]*\}/g, '');
return { start, end, text };
}).filter(Boolean);
}
render() {
if (this.destroyed) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
@@ -131,7 +125,6 @@ class SimpleSubtitleRenderer {
const cue = this.cues.find(c => currentTime >= c.start && currentTime <= c.end);
if (cue) this.drawSubtitle(cue.text);
}
drawSubtitle(text) {
const lines = text.split('\n');
const fontSize = Math.max(20, this.canvas.height * 0.04);
@@ -149,13 +142,11 @@ class SimpleSubtitleRenderer {
this.ctx.fillText(line, this.canvas.width / 2, y);
});
}
dispose() {
this.destroyed = true;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
if (this.resizeHandler) window.removeEventListener('resize', this.resizeHandler);
}
}
window.SubtitleRenderer = SubtitleRenderer;
window.SimpleSubtitleRenderer = SimpleSubtitleRenderer;