1035 lines
46 KiB
JavaScript
1035 lines
46 KiB
JavaScript
|
let epgPrograms = [];
|
||
|
let epgChannelIcons = {};
|
||
|
let epgDataByChannelId = {};
|
||
|
let epgChannelDisplayNames = {};
|
||
|
let epgLastLoadTime = null;
|
||
|
let orderedEpgChannelIds = [];
|
||
|
let epgMatchInProgress = false;
|
||
|
|
||
|
let epgMinTimeMs = 0;
|
||
|
let epgMaxTimeMs = 0;
|
||
|
let epgCurrentTimeLineInterval = null;
|
||
|
let infobarHideTimeout = null;
|
||
|
let epgProgressUpdateIntervalId = null;
|
||
|
let dynamicMovistarEpgUpdateIntervalId = null;
|
||
|
|
||
|
function bindEpgEvents() {
|
||
|
$('#openEpgModalBtn').on('click', () => $('#epgModal').modal('show'));
|
||
|
|
||
|
$('#epgModal').on('shown.bs.modal', () => {
|
||
|
$('#epgUrlInputModal').val(userSettings.lastEpgUrl || userSettings.defaultEpgUrl);
|
||
|
const now = new Date().getTime();
|
||
|
if (orderedEpgChannelIds.length === 0 || !epgLastLoadTime || (epgLastLoadTime && now - epgLastLoadTime > 60 * 60 * 1000)) {
|
||
|
}
|
||
|
populateEPGChannelsModal(); renderEPGTimeBar(); renderEPGProgramsLazy(); startCurrentTimeLineUpdater();
|
||
|
});
|
||
|
|
||
|
$('#loadEpgBtnModal').on('click', () => {
|
||
|
const url = $('#epgUrlInputModal').val().trim();
|
||
|
if (url) loadEpgFromUrl(url); else showNotification('Introduce una URL EPG válida.', 'error');
|
||
|
});
|
||
|
|
||
|
let isSyncingScrollEPG = false;
|
||
|
const epgChannelListEl = $('#epgChannelsList')[0];
|
||
|
const epgProgramContainerEl = $('#epgProgramsContainer')[0];
|
||
|
const epgTimeBarHeaderElParent = $('#epgTimeBar').parent()[0];
|
||
|
|
||
|
function syncEpgScroll(source, target1, target2, isVertical) {
|
||
|
if (isSyncingScrollEPG) return;
|
||
|
isSyncingScrollEPG = true;
|
||
|
if (isVertical) {
|
||
|
if(target1) target1.scrollTop = source.scrollTop;
|
||
|
if (target2) target2.scrollTop = source.scrollTop;
|
||
|
} else {
|
||
|
if(target1) target1.scrollLeft = source.scrollLeft;
|
||
|
if (target2) target2.scrollLeft = source.scrollLeft;
|
||
|
}
|
||
|
requestAnimationFrame(() => isSyncingScrollEPG = false);
|
||
|
}
|
||
|
|
||
|
if (epgChannelListEl && epgProgramContainerEl && epgTimeBarHeaderElParent) {
|
||
|
$(epgChannelListEl).on('scroll', function () { syncEpgScroll(this, epgProgramContainerEl, null, true); });
|
||
|
$(epgProgramContainerEl).on('scroll', function () {
|
||
|
syncEpgScroll(this, epgChannelListEl, null, true);
|
||
|
syncEpgScroll(this, epgTimeBarHeaderElParent, null, false);
|
||
|
});
|
||
|
$(epgTimeBarHeaderElParent).on('scroll', function () { syncEpgScroll(this, epgProgramContainerEl, null, false); });
|
||
|
}
|
||
|
|
||
|
$('#forceEpgRematchBtn').on('click', () => {
|
||
|
if (channels.length > 0 && (Object.keys(epgDataByChannelId).length > 0 || userSettings.useMovistarVodAsEpg)) {
|
||
|
matchChannelsWithEpg(true)
|
||
|
.then(async () => {
|
||
|
if (userSettings.useMovistarVodAsEpg) {
|
||
|
const today = new Date();
|
||
|
const yyyy = today.getFullYear();
|
||
|
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||
|
const dd = String(today.getDate()).padStart(2, '0');
|
||
|
await updateEpgWithMovistarVodData(`${yyyy}-${mm}-${dd}`);
|
||
|
}
|
||
|
})
|
||
|
.catch(error => {
|
||
|
console.error("Error durante el re-emparejamiento forzado desde botón:", error);
|
||
|
showNotification("Error durante el re-emparejamiento EPG.", "error");
|
||
|
showLoading(false);
|
||
|
});
|
||
|
} else {
|
||
|
showNotification('Carga una lista M3U y un EPG (o activa VOD EPG en ajustes) primero.', 'info');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function getEpgChannelIcon(epgId) {
|
||
|
if (epgId && epgId.startsWith('movistar.')) {
|
||
|
const m3uChannel = channels.find(ch => ch.effectiveEpgId === epgId);
|
||
|
return m3uChannel ? m3uChannel['tvg-logo'] : null;
|
||
|
}
|
||
|
return epgChannelIcons[epgId] || null;
|
||
|
}
|
||
|
|
||
|
function getEpgDataForChannel(epgId) {
|
||
|
return epgDataByChannelId[epgId] || [];
|
||
|
}
|
||
|
|
||
|
function getEpgLastLoadTimestamp() {
|
||
|
return epgLastLoadTime;
|
||
|
}
|
||
|
|
||
|
function getMovistarServiceUidFromM3UChannel(m3uChannel) {
|
||
|
if (!m3uChannel) return null;
|
||
|
|
||
|
if (m3uChannel['tvg-id']) {
|
||
|
const tvgId = String(m3uChannel['tvg-id']);
|
||
|
let match = tvgId.match(/^(?:CVXCH)?(\d{3,6})(?:\.MS)?$/i);
|
||
|
if (match && match[1]) return match[1];
|
||
|
match = tvgId.match(/\.(\d{3,6})$/);
|
||
|
if (match && match[1]) return match[1];
|
||
|
}
|
||
|
|
||
|
if (m3uChannel.attributes && m3uChannel.attributes['ch-number'] && /^\d{3,6}$/.test(m3uChannel.attributes['ch-number'])) {
|
||
|
return m3uChannel.attributes['ch-number'];
|
||
|
}
|
||
|
|
||
|
if (m3uChannel.url) {
|
||
|
const url = m3uChannel.url;
|
||
|
let match = url.match(/\/CVXCH(\d{3,6})\//i);
|
||
|
if (match && match[1]) return match[1];
|
||
|
match = url.match(/\/(\d{3,6})\/vxfmt=dp\//i);
|
||
|
if (match && match[1]) return match[1];
|
||
|
match = url.match(/\/(?:deliverty)\/([A-Za-z0-9_-]+?)(?:\.MS)?\//i);
|
||
|
if (match && match[1] && /^\d{3,6}$/.test(match[1].replace('.MS',''))) return match[1].replace('.MS','');
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
async function matchChannelsWithEpg(forceRematch = false) {
|
||
|
if (epgMatchInProgress && !forceRematch) return;
|
||
|
if (channels.length === 0 || (Object.keys(epgDataByChannelId).length === 0 && !userSettings.useMovistarVodAsEpg)) {
|
||
|
channels.forEach(ch => { ch.effectiveEpgId = null; });
|
||
|
if (typeof filterAndRenderChannels === 'function') {
|
||
|
if (currentFilter !== 'all') { filterAndRenderChannels(); } else { renderChannels(); }
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
epgMatchInProgress = true;
|
||
|
if (forceRematch) {
|
||
|
showLoading(true, 'Re-emparejando EPG, esto puede tardar...');
|
||
|
}
|
||
|
|
||
|
let matchesFoundByTvgId = 0;
|
||
|
let matchesFoundByName = 0;
|
||
|
let matchesFoundByMovistarId = 0;
|
||
|
const batchSize = 200;
|
||
|
|
||
|
for (let i = 0; i < channels.length; i += batchSize) {
|
||
|
const batch = channels.slice(i, i + batchSize);
|
||
|
for (const m3uChannel of batch) {
|
||
|
m3uChannel.effectiveEpgId = null;
|
||
|
const tvgId = (m3uChannel['tvg-id'] || '').toLowerCase().trim();
|
||
|
|
||
|
if (tvgId && epgDataByChannelId[tvgId]) {
|
||
|
m3uChannel.effectiveEpgId = tvgId;
|
||
|
matchesFoundByTvgId++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (userSettings.useMovistarVodAsEpg && !m3uChannel.effectiveEpgId) {
|
||
|
const serviceUid = getMovistarServiceUidFromM3UChannel(m3uChannel);
|
||
|
if (serviceUid) {
|
||
|
const movistarEpgId = `movistar.${serviceUid}`;
|
||
|
m3uChannel.effectiveEpgId = movistarEpgId;
|
||
|
matchesFoundByMovistarId++;
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (forceRematch) {
|
||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (userSettings.enableEpgNameMatching && Object.keys(epgChannelDisplayNames).length > 0) {
|
||
|
const epgChannelIds = Object.keys(epgChannelDisplayNames);
|
||
|
for (let i = 0; i < channels.length; i += batchSize) {
|
||
|
const batch = channels.slice(i, i + batchSize);
|
||
|
for (const m3uChannel of batch) {
|
||
|
if (m3uChannel.effectiveEpgId) continue;
|
||
|
let bestMatch = { id: null, score: 0 };
|
||
|
const m3uName = m3uChannel.name;
|
||
|
|
||
|
for (const epgId of epgChannelIds) {
|
||
|
const epgName = epgChannelDisplayNames[epgId];
|
||
|
if (epgName) {
|
||
|
const similarity = getStringSimilarity(m3uName, epgName);
|
||
|
if (similarity > bestMatch.score) {
|
||
|
bestMatch = { id: epgId, score: similarity };
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (bestMatch.id && bestMatch.score >= userSettings.epgNameMatchThreshold) {
|
||
|
m3uChannel.effectiveEpgId = bestMatch.id;
|
||
|
matchesFoundByName++;
|
||
|
}
|
||
|
}
|
||
|
if (forceRematch) {
|
||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (forceRematch) {
|
||
|
showLoading(false);
|
||
|
}
|
||
|
epgMatchInProgress = false;
|
||
|
|
||
|
let notificationMsg = `EPG Re-emparejado: ${matchesFoundByTvgId} por ID (XMLTV), ${matchesFoundByName} por nombre (XMLTV).`;
|
||
|
if (userSettings.useMovistarVodAsEpg) {
|
||
|
notificationMsg += ` ${matchesFoundByMovistarId} canales Movistar identificados para VOD EPG.`;
|
||
|
}
|
||
|
|
||
|
if (forceRematch) {
|
||
|
if (matchesFoundByTvgId > 0 || matchesFoundByName > 0 || matchesFoundByMovistarId > 0) {
|
||
|
showNotification(notificationMsg, 'success');
|
||
|
} else {
|
||
|
showNotification('No se encontraron nuevas coincidencias EPG.', 'info');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (typeof filterAndRenderChannels === 'function') {
|
||
|
if (currentFilter !== 'all') { filterAndRenderChannels(); } else { renderChannels(); }
|
||
|
}
|
||
|
if (typeof updateEPGProgressBarOnCards === 'function') {
|
||
|
updateEPGProgressBarOnCards();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function transformMovistarVodProgram(vodProgram) {
|
||
|
if (!vodProgram || !vodProgram.FechaHoraInicio || !vodProgram.FechaHoraFin || !vodProgram.CanalServiceUid2) return null;
|
||
|
return {
|
||
|
channel: `movistar.${vodProgram.CanalServiceUid2}`,
|
||
|
startDt: new Date(parseInt(vodProgram.FechaHoraInicio)),
|
||
|
stopDt: new Date(parseInt(vodProgram.FechaHoraFin)),
|
||
|
title: vodProgram.Titulo || 'Programa sin título',
|
||
|
desc: vodProgram.Ficha?.Descripcion || vodProgram.Ficha?.SinopsisLarga || vodProgram.Ficha?.Sinopsis || '',
|
||
|
category: vodProgram.GeneroComAntena || '',
|
||
|
icon: vodProgram.ImagenMiniatura || vodProgram.Ficha?.Imagen || '',
|
||
|
date: vodProgram.Ficha?.Anno || '',
|
||
|
isMovistarVod: true,
|
||
|
CanalServiceUid2: vodProgram.CanalServiceUid2
|
||
|
};
|
||
|
}
|
||
|
|
||
|
async function updateEpgWithMovistarVodData(dateString, vodProgramsForDate = null) {
|
||
|
if (!userSettings.useMovistarVodAsEpg || channels.length === 0) return;
|
||
|
|
||
|
let programsToProcess = vodProgramsForDate;
|
||
|
if (!programsToProcess) {
|
||
|
try {
|
||
|
const cachedRecord = await getMovistarVodData(dateString);
|
||
|
if (cachedRecord && cachedRecord.data) {
|
||
|
programsToProcess = cachedRecord.data;
|
||
|
} else {
|
||
|
return;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
console.error(`Error obteniendo VOD de caché para EPG (${dateString}):`, e);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!programsToProcess || programsToProcess.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let updatedChannelCount = 0;
|
||
|
channels.forEach(m3uChannel => {
|
||
|
if (m3uChannel.effectiveEpgId && m3uChannel.effectiveEpgId.startsWith('movistar.')) {
|
||
|
const serviceUid2 = m3uChannel.effectiveEpgId.split('.')[1];
|
||
|
if (serviceUid2) {
|
||
|
const channelVodPrograms = programsToProcess.filter(p => p.CanalServiceUid2 === serviceUid2);
|
||
|
if (channelVodPrograms.length > 0) {
|
||
|
const transformedPrograms = channelVodPrograms.map(transformMovistarVodProgram).filter(Boolean);
|
||
|
transformedPrograms.sort((a, b) => a.startDt - b.startDt);
|
||
|
epgDataByChannelId[m3uChannel.effectiveEpgId] = transformedPrograms;
|
||
|
|
||
|
if (!epgChannelDisplayNames[m3uChannel.effectiveEpgId]) {
|
||
|
epgChannelDisplayNames[m3uChannel.effectiveEpgId] = m3uChannel.name || `Movistar ${serviceUid2}`;
|
||
|
}
|
||
|
if (!orderedEpgChannelIds.includes(m3uChannel.effectiveEpgId)) {
|
||
|
orderedEpgChannelIds.push(m3uChannel.effectiveEpgId);
|
||
|
}
|
||
|
updatedChannelCount++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (updatedChannelCount > 0) {
|
||
|
if (typeof filterAndRenderChannels === 'function') filterAndRenderChannels();
|
||
|
if (typeof updateEPGProgressBarOnCards === 'function') updateEPGProgressBarOnCards();
|
||
|
if ($('#epgModal').is(':visible')) {
|
||
|
populateEPGChannelsModal();
|
||
|
renderEPGProgramsLazy();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function startDynamicEpgUpdaters() {
|
||
|
if (epgProgressUpdateIntervalId) clearInterval(epgProgressUpdateIntervalId);
|
||
|
updateEPGProgressBarOnCards();
|
||
|
epgProgressUpdateIntervalId = setInterval(updateEPGProgressBarOnCards, 60000);
|
||
|
|
||
|
if (dynamicMovistarEpgUpdateIntervalId) clearInterval(dynamicMovistarEpgUpdateIntervalId);
|
||
|
|
||
|
let lastCheckedDateForVodEpg = new Date().toDateString();
|
||
|
|
||
|
dynamicMovistarEpgUpdateIntervalId = setInterval(async () => {
|
||
|
const now = new Date();
|
||
|
if (userSettings.useMovistarVodAsEpg && now.toDateString() !== lastCheckedDateForVodEpg) {
|
||
|
lastCheckedDateForVodEpg = now.toDateString();
|
||
|
const yyyy = now.getFullYear();
|
||
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||
|
const dd = String(now.getDate()).padStart(2, '0');
|
||
|
showNotification("Cargando EPG de Movistar VOD para el nuevo día...", "info");
|
||
|
await updateEpgWithMovistarVodData(`${yyyy}-${mm}-${dd}`);
|
||
|
}
|
||
|
|
||
|
updateEPGProgressBarOnCards();
|
||
|
|
||
|
Object.values(playerInstances).forEach(instance => {
|
||
|
if (instance && instance.container && $(instance.container).is(':visible')) {
|
||
|
updatePlayerInfobar(instance.channel, instance.container.querySelector('.player-infobar'));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
}, 60000);
|
||
|
}
|
||
|
|
||
|
function stopDynamicEpgUpdaters() {
|
||
|
if (epgProgressUpdateIntervalId) clearInterval(epgProgressUpdateIntervalId);
|
||
|
if (dynamicMovistarEpgUpdateIntervalId) clearInterval(dynamicMovistarEpgUpdateIntervalId);
|
||
|
epgProgressUpdateIntervalId = null;
|
||
|
dynamicMovistarEpgUpdateIntervalId = null;
|
||
|
}
|
||
|
|
||
|
function updateEPGProgressBarOnCards() {
|
||
|
$('.channel-card').each(function() {
|
||
|
const card = $(this);
|
||
|
const channelUrl = card.data('url');
|
||
|
const channel = channels.find(c => c.url === channelUrl);
|
||
|
const progressBarContainer = card.find('.epg-progress-bar-container');
|
||
|
const progressBar = card.find('.epg-progress-bar');
|
||
|
|
||
|
if (userSettings.cardShowEpg && channel && channel.effectiveEpgId && epgDataByChannelId[channel.effectiveEpgId] && progressBarContainer.length) {
|
||
|
const programsForChannel = epgDataByChannelId[channel.effectiveEpgId];
|
||
|
const now = new Date();
|
||
|
const currentProgram = programsForChannel.find(p => now >= p.startDt && now < p.stopDt);
|
||
|
|
||
|
if (currentProgram) {
|
||
|
const programDuration = currentProgram.stopDt.getTime() - currentProgram.startDt.getTime();
|
||
|
const elapsed = now.getTime() - currentProgram.startDt.getTime();
|
||
|
let progressPercent = 0;
|
||
|
if (programDuration > 0) {
|
||
|
progressPercent = Math.min(100, (elapsed / programDuration) * 100);
|
||
|
}
|
||
|
progressBar.css('width', progressPercent + '%');
|
||
|
progressBarContainer.show();
|
||
|
} else {
|
||
|
progressBar.css('width', '0%');
|
||
|
progressBarContainer.hide();
|
||
|
}
|
||
|
} else if (progressBarContainer.length) {
|
||
|
progressBar.css('width', '0%');
|
||
|
progressBarContainer.hide();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async function loadEpgFromUrl(url) {
|
||
|
if (!url) {
|
||
|
showNotification('URL EPG inválida o vacía.', 'error');
|
||
|
$('#epgChannelsList, #epgPrograms, #epgTimeBar').empty().html('<p class="p-3 text-warning text-center">Introduce una URL EPG válida o usa la predeterminada.</p>');
|
||
|
epgPrograms = [];
|
||
|
epgLastLoadTime = null;
|
||
|
matchChannelsWithEpg();
|
||
|
return;
|
||
|
}
|
||
|
showLoading(true, 'Cargando y procesando EPG XMLTV...');
|
||
|
try {
|
||
|
const response = await fetch(url);
|
||
|
if (!response.ok) {
|
||
|
const errorBody = await response.text().catch(() => '');
|
||
|
throw new Error(`Error HTTP ${response.status} - ${response.statusText}${errorBody ? ': ' + errorBody.substring(0, 100) + '...' : ''}`);
|
||
|
}
|
||
|
const xmlText = await response.text();
|
||
|
if (!xmlText || xmlText.trim() === '') throw new Error('Archivo EPG XMLTV vacío o inaccesible.');
|
||
|
const parser = new DOMParser();
|
||
|
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
||
|
const parseErrorNode = xmlDoc.querySelector('parsererror');
|
||
|
if (parseErrorNode) {
|
||
|
const errorText = parseErrorNode.textContent || 'Error de parseo XMLTV desconocido.';
|
||
|
throw new Error('XMLTV mal formado: ' + errorText.substring(0, 200) + (errorText.length > 200 ? '...' : ''));
|
||
|
}
|
||
|
|
||
|
const existingMovistarEpgData = {};
|
||
|
Object.keys(epgDataByChannelId).forEach(key => {
|
||
|
if (key.startsWith('movistar.')) {
|
||
|
existingMovistarEpgData[key] = epgDataByChannelId[key];
|
||
|
}
|
||
|
});
|
||
|
epgDataByChannelId = {...existingMovistarEpgData};
|
||
|
epgPrograms = [];
|
||
|
epgChannelIcons = {};
|
||
|
|
||
|
const newChannelDisplayNames = {};
|
||
|
const existingMovistarDisplayNames = {};
|
||
|
Object.keys(epgChannelDisplayNames).forEach(key => {
|
||
|
if (key.startsWith('movistar.')) {
|
||
|
existingMovistarDisplayNames[key] = epgChannelDisplayNames[key];
|
||
|
}
|
||
|
});
|
||
|
epgChannelDisplayNames = existingMovistarDisplayNames;
|
||
|
|
||
|
const parsedData = parseXMLTV(xmlDoc);
|
||
|
|
||
|
epgPrograms.push(...parsedData.programs);
|
||
|
Object.assign(epgChannelIcons, parsedData.channelIcons);
|
||
|
Object.assign(epgChannelDisplayNames, parsedData.channelDisplayNames);
|
||
|
|
||
|
for (const chId in parsedData.dataByChannelId) {
|
||
|
epgDataByChannelId[chId] = parsedData.dataByChannelId[chId];
|
||
|
}
|
||
|
|
||
|
const existingMovistarIds = orderedEpgChannelIds.filter(id => id.startsWith('movistar.'));
|
||
|
const newIdsFromXml = parsedData.orderedEpgIds;
|
||
|
const newIdsSet = new Set(newIdsFromXml);
|
||
|
const finalCombinedIds = [...newIdsFromXml];
|
||
|
existingMovistarIds.forEach(moviId => {
|
||
|
if (!newIdsSet.has(moviId)) {
|
||
|
finalCombinedIds.push(moviId);
|
||
|
}
|
||
|
});
|
||
|
orderedEpgChannelIds = finalCombinedIds;
|
||
|
|
||
|
epgMinTimeMs = parsedData.minTimeMs;
|
||
|
epgMaxTimeMs = parsedData.maxTimeMs;
|
||
|
|
||
|
epgLastLoadTime = new Date().getTime();
|
||
|
userSettings.lastEpgUrl = url;
|
||
|
localStorage.setItem('zenithUserSettings', JSON.stringify(userSettings));
|
||
|
|
||
|
$('#epgChannelsList, #epgPrograms, #epgTimeBar').empty();
|
||
|
await matchChannelsWithEpg();
|
||
|
|
||
|
if (userSettings.useMovistarVodAsEpg) {
|
||
|
const today = new Date();
|
||
|
const yyyy = today.getFullYear();
|
||
|
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||
|
const dd = String(today.getDate()).padStart(2, '0');
|
||
|
await updateEpgWithMovistarVodData(`${yyyy}-${mm}-${dd}`);
|
||
|
}
|
||
|
|
||
|
if (orderedEpgChannelIds.length > 0) {
|
||
|
populateEPGChannelsModal(); renderEPGTimeBar(); renderEPGProgramsLazy(); startCurrentTimeLineUpdater();
|
||
|
showNotification(`EPG XMLTV cargado (${parsedData.programs.length} programas para ${parsedData.orderedEpgIds.length} canales XMLTV).`, 'success');
|
||
|
} else {
|
||
|
$('#epgChannelsList').html('<p class="p-3 text-secondary text-center">No se encontraron canales en el EPG XMLTV.</p>');
|
||
|
$('#epgPrograms').html('<p class="p-3 text-secondary text-center">No hay programas para mostrar.</p>').css('min-height', '200px');
|
||
|
showNotification('EPG XMLTV cargado, pero sin canales o programas válidos para mostrar.', 'warning');
|
||
|
}
|
||
|
} catch (err) {
|
||
|
showNotification(`Error cargando EPG XMLTV desde "${escapeHtml(url)}": ${err.message}`, 'error');
|
||
|
console.error("EPG XMLTV Load Error:", err);
|
||
|
$('#epgChannelsList').html('<p class="p-3 text-danger text-center">Error cargando canales EPG XMLTV.</p>');
|
||
|
$('#epgPrograms').html('<p class="p-3 text-danger text-center">Error cargando programas EPG XMLTV.</p>');
|
||
|
epgLastLoadTime = null;
|
||
|
matchChannelsWithEpg();
|
||
|
} finally { showLoading(false); }
|
||
|
}
|
||
|
|
||
|
function parseXMLTV(xmlDoc) {
|
||
|
const channelIcons = {};
|
||
|
const channelDisplayNames = {};
|
||
|
const programs = [];
|
||
|
const dataByChannelId = {};
|
||
|
const orderedIdsFromXml = [];
|
||
|
const seenChannelIds = new Set();
|
||
|
|
||
|
const now = new Date();
|
||
|
const startTimeLimit = new Date(now.getTime() - 6 * 3600 * 1000);
|
||
|
const endTimeLimit = new Date(now.getTime() + 48 * 3600 * 1000);
|
||
|
|
||
|
let minTimestamp = Infinity;
|
||
|
let maxTimestamp = -Infinity;
|
||
|
|
||
|
xmlDoc.querySelectorAll('channel').forEach(node => {
|
||
|
const id = (node.getAttribute('id') || '').toLowerCase().trim();
|
||
|
if (id && !seenChannelIds.has(id)) {
|
||
|
seenChannelIds.add(id);
|
||
|
orderedIdsFromXml.push(id);
|
||
|
const iconNode = node.querySelector('icon[src]');
|
||
|
if (iconNode) {
|
||
|
const iconSrc = iconNode.getAttribute('src');
|
||
|
if (iconSrc && !/blank|tv-icon|placeholder|no-logo|noimage|default/i.test(iconSrc)) {
|
||
|
channelIcons[id] = iconSrc;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const displayNameNode = node.querySelector('display-name');
|
||
|
if (displayNameNode && displayNameNode.textContent) {
|
||
|
channelDisplayNames[id] = displayNameNode.textContent.trim();
|
||
|
} else if (id) {
|
||
|
channelDisplayNames[id] = id;
|
||
|
}
|
||
|
dataByChannelId[id] = [];
|
||
|
}
|
||
|
});
|
||
|
|
||
|
xmlDoc.querySelectorAll('programme').forEach(node => {
|
||
|
const startAttr = node.getAttribute('start'); const stopAttr = node.getAttribute('stop');
|
||
|
const channelId = (node.getAttribute('channel') || '').toLowerCase().trim();
|
||
|
if (!startAttr || !stopAttr || !channelId) return;
|
||
|
|
||
|
const startDt = parseEPGDate(startAttr); const stopDt = parseEPGDate(stopAttr);
|
||
|
if (!startDt || !stopDt) { console.warn("Invalid date in EPG program:", startAttr, stopAttr); return; }
|
||
|
if (stopDt <= startDt) { console.warn("EPG program end time is before start time:", startAttr, stopAttr); return; }
|
||
|
|
||
|
if (stopDt < startTimeLimit || startDt > endTimeLimit) { return; }
|
||
|
|
||
|
minTimestamp = Math.min(minTimestamp, startDt.getTime());
|
||
|
maxTimestamp = Math.max(maxTimestamp, stopDt.getTime());
|
||
|
|
||
|
const programData = {
|
||
|
channel: channelId, start: startAttr, stop: stopAttr, startDt: startDt, stopDt: stopDt,
|
||
|
title: node.querySelector('title')?.textContent?.trim() || 'Programa sin título',
|
||
|
desc: node.querySelector('desc')?.textContent?.trim() || '',
|
||
|
category: node.querySelector('category')?.textContent?.trim() || '',
|
||
|
icon: node.querySelector('icon[src]')?.getAttribute('src') || '',
|
||
|
date: node.querySelector('date')?.textContent?.trim() || '',
|
||
|
};
|
||
|
programs.push(programData);
|
||
|
if (dataByChannelId[channelId]) {
|
||
|
dataByChannelId[channelId].push(programData);
|
||
|
} else {
|
||
|
dataByChannelId[channelId] = [programData];
|
||
|
if(!seenChannelIds.has(channelId)) {
|
||
|
seenChannelIds.add(channelId);
|
||
|
orderedIdsFromXml.push(channelId);
|
||
|
if(!channelDisplayNames[channelId]) channelDisplayNames[channelId] = channelId;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
for (const chId in dataByChannelId) {
|
||
|
dataByChannelId[chId].sort((a, b) => a.startDt - b.bstartDt);
|
||
|
}
|
||
|
|
||
|
if (minTimestamp === Infinity) minTimestamp = startTimeLimit.getTime();
|
||
|
if (maxTimestamp === -Infinity) maxTimestamp = endTimeLimit.getTime();
|
||
|
|
||
|
return { programs, channelIcons, channelDisplayNames, dataByChannelId, orderedEpgIds: orderedIdsFromXml, minTimeMs: minTimestamp, maxTimeMs: maxTimestamp };
|
||
|
}
|
||
|
|
||
|
function parseEPGDate(dateString) {
|
||
|
if (!dateString || typeof dateString !== 'string' || dateString.length < 12) return null;
|
||
|
dateString = dateString.trim();
|
||
|
try {
|
||
|
const Y = parseInt(dateString.substring(0, 4), 10);
|
||
|
const M = parseInt(dateString.substring(4, 6), 10) - 1;
|
||
|
const D = parseInt(dateString.substring(6, 8), 10);
|
||
|
const h = parseInt(dateString.substring(8, 10), 10);
|
||
|
const m = parseInt(dateString.substring(10, 12), 10);
|
||
|
const s = parseInt(dateString.substring(12, 14), 10) || 0;
|
||
|
|
||
|
if ([Y, M, D, h, m, s].some(isNaN)) return null;
|
||
|
|
||
|
let date;
|
||
|
const isoBase = `${String(Y).padStart(4, '0')}-${String(M + 1).padStart(2, '0')}-${String(D).padStart(2, '0')}T${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||
|
|
||
|
if (dateString.length > 14) {
|
||
|
const offsetPart = dateString.substring(14).trim();
|
||
|
if (offsetPart.toUpperCase() === 'Z') {
|
||
|
date = new Date(`${isoBase}Z`);
|
||
|
} else {
|
||
|
const offsetMatch = offsetPart.match(/^([+-])(\d{2})(\d{2})?$/);
|
||
|
if (offsetMatch) {
|
||
|
const offsetMinutes = parseInt(offsetMatch[3] || '00', 10);
|
||
|
date = new Date(`${isoBase}${offsetMatch[1]}${offsetMatch[2]}:${String(offsetMinutes).padStart(2,'0')}`);
|
||
|
} else {
|
||
|
date = new Date(Date.UTC(Y, M, D, h, m, s));
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
date = new Date(Y, M, D, h, m, s);
|
||
|
}
|
||
|
if (isNaN(date.getTime())) return null;
|
||
|
return date;
|
||
|
} catch (e) { console.warn("Error parsing EPG date string:", dateString, e); return null; }
|
||
|
}
|
||
|
|
||
|
function formatEPGTime(date) {
|
||
|
if (!(date instanceof Date) || isNaN(date.getTime())) { return '??:??'; }
|
||
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
|
}
|
||
|
|
||
|
function populateEPGChannelsModal() {
|
||
|
const channelsList = $('#epgChannelsList').empty();
|
||
|
if (!orderedEpgChannelIds || orderedEpgChannelIds.length === 0) {
|
||
|
channelsList.append('<p class="p-3 text-secondary text-center">No hay canales EPG disponibles.</p>'); return;
|
||
|
}
|
||
|
|
||
|
const fragment = document.createDocumentFragment();
|
||
|
orderedEpgChannelIds.forEach(id => {
|
||
|
const m3uChannelForEpgId = channels.find(chM3U => chM3U.effectiveEpgId === id);
|
||
|
|
||
|
let name = epgChannelDisplayNames[id];
|
||
|
if (!name && m3uChannelForEpgId) name = m3uChannelForEpgId.name;
|
||
|
if (!name) name = id;
|
||
|
|
||
|
let icon = getEpgChannelIcon(id);
|
||
|
if (!icon && m3uChannelForEpgId) icon = m3uChannelForEpgId['tvg-logo'];
|
||
|
|
||
|
const item = document.createElement('div');
|
||
|
item.className = 'epg-channel-item'; item.dataset.channelId = id;
|
||
|
|
||
|
const richTitle = `${escapeHtml(name)}\n${m3uChannelForEpgId ? 'Click para reproducir' : 'ID EPG: '+id}`;
|
||
|
item.title = richTitle;
|
||
|
|
||
|
let iconHtml = '';
|
||
|
if (icon) { iconHtml = `<img src="${escapeHtml(icon)}" alt="" onerror="this.style.display='none'; if(this.parentElement && this.parentElement.querySelector('.epg-icon-placeholder')) this.parentElement.querySelector('.epg-icon-placeholder').style.display=''">`; }
|
||
|
const placeholderHtml = `<span class="epg-icon-placeholder" ${icon ? 'style="display:none;"' : ''}></span>`;
|
||
|
const playButtonHtml = m3uChannelForEpgId ? '<button class="play-channel-epg-btn" title="Reproducir"></button>' : '';
|
||
|
item.innerHTML = `${iconHtml}${placeholderHtml}<span style="flex-grow: 1; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(name)}</span>${playButtonHtml}`;
|
||
|
const playButton = item.querySelector('.play-channel-epg-btn');
|
||
|
if (playButton) {
|
||
|
playButton.addEventListener('click', (e) => {
|
||
|
e.stopPropagation();
|
||
|
if (typeof createPlayerWindow === 'function') {
|
||
|
createPlayerWindow(m3uChannelForEpgId);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
item.addEventListener('click', () => {
|
||
|
const programsContainer = $('#epgProgramsContainer')[0];
|
||
|
if (programsContainer) {
|
||
|
const channelIndex = orderedEpgChannelIds.indexOf(id);
|
||
|
const itemHeight = $('.epg-channel-item').first().outerHeight() || 60;
|
||
|
const programsContainerHeight = $(programsContainer).height() || 500;
|
||
|
const scrollTo = (channelIndex * itemHeight) - (programsContainerHeight / 2) + (itemHeight / 2);
|
||
|
$(programsContainer).animate({ scrollTop: Math.max(0, scrollTo) }, 300);
|
||
|
}
|
||
|
});
|
||
|
fragment.appendChild(item);
|
||
|
});
|
||
|
channelsList.append(fragment);
|
||
|
}
|
||
|
|
||
|
function getEffectiveEpgTimeRange() {
|
||
|
let finalMinMs = Infinity;
|
||
|
let finalMaxMs = -Infinity;
|
||
|
|
||
|
if (epgMinTimeMs > 0 && epgMinTimeMs !== Infinity) {
|
||
|
finalMinMs = epgMinTimeMs;
|
||
|
finalMaxMs = epgMaxTimeMs;
|
||
|
}
|
||
|
|
||
|
if (userSettings.useMovistarVodAsEpg) {
|
||
|
let vodMin = Infinity, vodMax = -Infinity;
|
||
|
Object.values(epgDataByChannelId).forEach(programsArr => {
|
||
|
if (programsArr && programsArr.length > 0 && programsArr.some(p => p.isMovistarVod)) {
|
||
|
programsArr.forEach(p => {
|
||
|
if (p.isMovistarVod) {
|
||
|
vodMin = Math.min(vodMin, p.startDt.getTime());
|
||
|
vodMax = Math.max(vodMax, p.stopDt.getTime());
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
if (vodMin !== Infinity) finalMinMs = Math.min(finalMinMs, vodMin);
|
||
|
if (vodMax !== -Infinity) finalMaxMs = Math.max(finalMaxMs, vodMax);
|
||
|
}
|
||
|
|
||
|
if (finalMinMs === Infinity || finalMaxMs === -Infinity) {
|
||
|
finalMinMs = new Date().setHours(0,0,0,0);
|
||
|
finalMaxMs = new Date().setHours(23,59,59,999);
|
||
|
}
|
||
|
|
||
|
return { min: finalMinMs, max: finalMaxMs };
|
||
|
}
|
||
|
|
||
|
function renderEPGTimeBar() {
|
||
|
const timeBar = $('#epgTimeBar').empty();
|
||
|
const programsContainerEl = $('#epgProgramsContainer')[0];
|
||
|
if (!programsContainerEl) return;
|
||
|
|
||
|
const epgPixelsPerHour = userSettings.epgDensity || 200;
|
||
|
|
||
|
const timeRange = getEffectiveEpgTimeRange();
|
||
|
const overallMinTimeMs = timeRange.min;
|
||
|
const overallMaxTimeMs = timeRange.max;
|
||
|
|
||
|
if(overallMaxTimeMs <= overallMinTimeMs) {
|
||
|
timeBar.html('<p class="p-2 text-secondary">Rango de tiempo EPG no disponible.</p>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const totalTimebarWidth = (overallMaxTimeMs - overallMinTimeMs) / (3600 * 1000) * epgPixelsPerHour;
|
||
|
|
||
|
const fragment = document.createDocumentFragment();
|
||
|
let currentTime = new Date(overallMinTimeMs);
|
||
|
currentTime.setMinutes(0, 0, 0);
|
||
|
|
||
|
while (currentTime.getTime() < overallMaxTimeMs) {
|
||
|
const slotEl = document.createElement('div');
|
||
|
slotEl.className = 'epg-time-slot';
|
||
|
slotEl.style.minWidth = `${epgPixelsPerHour}px`;
|
||
|
slotEl.style.width = `${epgPixelsPerHour}px`;
|
||
|
slotEl.textContent = formatEPGTime(currentTime);
|
||
|
fragment.appendChild(slotEl);
|
||
|
currentTime.setTime(currentTime.getTime() + 3600 * 1000);
|
||
|
}
|
||
|
timeBar.append(fragment).css('width', `${totalTimebarWidth}px`);
|
||
|
$('#epgTimeBar').parent()[0].scrollLeft = programsContainerEl.scrollLeft || 0;
|
||
|
}
|
||
|
|
||
|
function renderEPGProgramsLazy() {
|
||
|
const programsEl = $('#epgPrograms').empty()[0];
|
||
|
const scrollContainer = $('#epgProgramsContainer')[0];
|
||
|
if (!scrollContainer || !programsEl) return;
|
||
|
|
||
|
if (orderedEpgChannelIds.length === 0) {
|
||
|
$(programsEl).append('<p class="p-3 text-secondary">No hay datos EPG para mostrar.</p>').css('min-height', '200px');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const epgPixelsPerHour = userSettings.epgDensity || 200;
|
||
|
const timeRange = getEffectiveEpgTimeRange();
|
||
|
const overallMinTimeMs = timeRange.min;
|
||
|
const overallMaxTimeMs = timeRange.max;
|
||
|
|
||
|
if(overallMaxTimeMs <= overallMinTimeMs) {
|
||
|
$(programsEl).append('<p class="p-3 text-secondary">Rango de tiempo EPG no disponible para programas.</p>').css('min-height', '200px');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const totalEpgDurationMs = overallMaxTimeMs - overallMinTimeMs;
|
||
|
const totalEPGWidth = totalEpgDurationMs / (3600 * 1000) * epgPixelsPerHour;
|
||
|
|
||
|
$(programsEl).css('width', `${totalEPGWidth}px`).css('--pixelsPerHour', `${epgPixelsPerHour}px`);
|
||
|
|
||
|
const rowFragment = document.createDocumentFragment();
|
||
|
orderedEpgChannelIds.forEach(channelId => {
|
||
|
const row = document.createElement('div');
|
||
|
row.className = 'epg-program-row'; row.dataset.channelId = channelId;
|
||
|
rowFragment.appendChild(row);
|
||
|
});
|
||
|
$(programsEl).append(rowFragment);
|
||
|
|
||
|
const observer = new IntersectionObserver((entries, obs) => {
|
||
|
entries.forEach(entry => {
|
||
|
if (entry.isIntersecting) {
|
||
|
const row = entry.target; const channelId = row.dataset.channelId;
|
||
|
if (row.dataset.rendered) return; row.dataset.rendered = true;
|
||
|
const channelPrograms = epgDataByChannelId[channelId];
|
||
|
if (channelPrograms && channelPrograms.length > 0) {
|
||
|
const progsFrag = document.createDocumentFragment();
|
||
|
const now = new Date();
|
||
|
channelPrograms.forEach(p => {
|
||
|
const startMs = p.startDt.getTime();
|
||
|
const stopMs = p.stopDt.getTime();
|
||
|
const durationMs = stopMs - startMs;
|
||
|
if (durationMs <= 0) return;
|
||
|
|
||
|
const offsetMsFromEPGStart = startMs - overallMinTimeMs;
|
||
|
const scaledPixelsPerMs = epgPixelsPerHour / 3600000;
|
||
|
const offsetPx = offsetMsFromEPGStart * scaledPixelsPerMs;
|
||
|
const widthPx = Math.max(2, durationMs * scaledPixelsPerMs);
|
||
|
|
||
|
const isCurrent = now >= p.startDt && now < p.stopDt;
|
||
|
const progEl = document.createElement('div');
|
||
|
progEl.className = `epg-program-item ${isCurrent ? 'current' : ''}`;
|
||
|
progEl.style.cssText = `left:${offsetPx}px; width:${widthPx}px;`;
|
||
|
|
||
|
let richTitle = `${escapeHtml(p.title)}\n${formatEPGTime(p.startDt)} - ${formatEPGTime(p.stopDt)}`;
|
||
|
if (p.desc) richTitle += `\n\n${escapeHtml(p.desc.substring(0,200))}${p.desc.length > 200 ? '...' : ''}`;
|
||
|
progEl.title = richTitle;
|
||
|
|
||
|
progEl.textContent = escapeHtml(p.title);
|
||
|
progEl.addEventListener('click', () => showProgramDetails(p));
|
||
|
progsFrag.appendChild(progEl);
|
||
|
});
|
||
|
requestAnimationFrame(() => { if (row.parentElement) { $(row).append(progsFrag); } });
|
||
|
}
|
||
|
obs.unobserve(row);
|
||
|
}
|
||
|
});
|
||
|
}, { root: scrollContainer, rootMargin: '300px 0px', threshold: 0.01 });
|
||
|
|
||
|
$(programsEl).children('.epg-program-row').each((_, el) => observer.observe(el));
|
||
|
$(programsEl).css('min-height', `${Math.max(200, orderedEpgChannelIds.length * 60)}px`);
|
||
|
|
||
|
requestAnimationFrame(() => {
|
||
|
const now = new Date().getTime();
|
||
|
const offsetNowFromEPGStartMs = now - overallMinTimeMs;
|
||
|
const scaledPixelsPerMs = epgPixelsPerHour / 3600000;
|
||
|
const nowPositionPx = offsetNowFromEPGStartMs * scaledPixelsPerMs;
|
||
|
const containerWidth = scrollContainer.clientWidth;
|
||
|
const targetScrollLeft = nowPositionPx - (containerWidth / 3);
|
||
|
scrollContainer.scrollLeft = Math.max(0, targetScrollLeft);
|
||
|
$('#epgTimeBar').parent()[0].scrollLeft = scrollContainer.scrollLeft;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function startCurrentTimeLineUpdater() {
|
||
|
if (epgCurrentTimeLineInterval) clearInterval(epgCurrentTimeLineInterval);
|
||
|
const currentTimeLine = $('#epgCurrentTimeLine');
|
||
|
|
||
|
const timeRange = getEffectiveEpgTimeRange();
|
||
|
const overallMinTimeMs = timeRange.min;
|
||
|
const overallMaxTimeMs = timeRange.max;
|
||
|
|
||
|
if (!currentTimeLine.length || overallMaxTimeMs <= overallMinTimeMs) {
|
||
|
currentTimeLine.hide();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const update = () => {
|
||
|
const now = new Date().getTime();
|
||
|
if (now < overallMinTimeMs || now > overallMaxTimeMs) {
|
||
|
currentTimeLine.hide();
|
||
|
return;
|
||
|
}
|
||
|
const epgPixelsPerHour = userSettings.epgDensity || 200;
|
||
|
const offsetNowFromEPGStartMs = now - overallMinTimeMs;
|
||
|
const scaledPixelsPerMs = epgPixelsPerHour / 3600000;
|
||
|
const linePositionPx = offsetNowFromEPGStartMs * scaledPixelsPerMs;
|
||
|
currentTimeLine.css('transform', `translateX(${linePositionPx}px)`).show();
|
||
|
};
|
||
|
update();
|
||
|
epgCurrentTimeLineInterval = setInterval(update, 60000);
|
||
|
$('#epgModal').one('hidden.bs.modal', () => {
|
||
|
if(epgCurrentTimeLineInterval) clearInterval(epgCurrentTimeLineInterval);
|
||
|
epgCurrentTimeLineInterval = null;
|
||
|
currentTimeLine.hide();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function showProgramDetails(program) {
|
||
|
const details = $('#epgProgramDetails').empty();
|
||
|
const m3uChannelForEpgId = channels.find(chM3U => chM3U.effectiveEpgId === program.channel);
|
||
|
const playButton = $('#playEpgProgramBtn').off('click');
|
||
|
const now = new Date();
|
||
|
const isPastProgram = program.stopDt < now;
|
||
|
|
||
|
let isCatchupChannel = false;
|
||
|
if (program.isMovistarVod && m3uChannelForEpgId) {
|
||
|
isCatchupChannel = true;
|
||
|
} else if (m3uChannelForEpgId && m3uChannelForEpgId.url &&
|
||
|
(m3uChannelForEpgId.url.includes('.cdn.telefonica.com/') || m3uChannelForEpgId.url.includes('.movistarplus.es/'))) {
|
||
|
isCatchupChannel = true;
|
||
|
}
|
||
|
|
||
|
if (m3uChannelForEpgId) {
|
||
|
playButton.show();
|
||
|
if (isPastProgram && isCatchupChannel) {
|
||
|
playButton.html('<span class="icon-placeholder" style="font-family:sans-serif;">⏪</span> Ver Programa (Catchup/VOD)');
|
||
|
playButton.on('click', () => {
|
||
|
handlePlayCatchup(m3uChannelForEpgId, program);
|
||
|
$('#epgProgramModal').modal('hide');
|
||
|
});
|
||
|
} else {
|
||
|
playButton.html('<span class="icon-placeholder"></span> Reproducir Canal (Directo)');
|
||
|
playButton.on('click', () => {
|
||
|
if (typeof createPlayerWindow === 'function') {
|
||
|
createPlayerWindow(m3uChannelForEpgId);
|
||
|
}
|
||
|
$('#epgProgramModal').modal('hide');
|
||
|
});
|
||
|
}
|
||
|
} else {
|
||
|
playButton.hide();
|
||
|
}
|
||
|
|
||
|
if (program.icon) { details.append($(`<img src="${escapeHtml(program.icon)}" alt="${escapeHtml(program.title)}" onerror="this.style.display='none';">`)); }
|
||
|
details.append(
|
||
|
$('<h5>').text(escapeHtml(program.title)),
|
||
|
$('<p>').html(`<strong>Canal:</strong> ${escapeHtml(m3uChannelForEpgId ? m3uChannelForEpgId.name : (epgChannelDisplayNames[program.channel] || program.channel))}`),
|
||
|
$('<p>').html(`<strong>Horario:</strong> ${formatEPGTime(program.startDt)} - ${formatEPGTime(program.stopDt)}`),
|
||
|
program.desc ? $('<p>').html(`<strong>Descripción:</strong> ${escapeHtml(program.desc)}`) : null,
|
||
|
program.category ? $('<p>').html(`<strong>Categoría:</strong> ${escapeHtml(program.category)}`) : null,
|
||
|
program.date ? $('<p>').html(`<strong>Año:</strong> ${escapeHtml(program.date)}`) : null
|
||
|
).append('<div style="clear: both;"></div>');
|
||
|
|
||
|
$('#epgProgramModalLabel').text('Detalles del Programa');
|
||
|
$('#epgProgramModal').modal('show');
|
||
|
}
|
||
|
|
||
|
function updatePlayerInfobar(channel, infobarElement) {
|
||
|
if (!channel || !infobarElement) return;
|
||
|
|
||
|
const logoEl = $(infobarElement).find('.infobar-logo');
|
||
|
const nameEl = $(infobarElement).find('.infobar-channel-name');
|
||
|
const currentEl = $(infobarElement).find('.infobar-epg-current');
|
||
|
const nextEl = $(infobarElement).find('.infobar-epg-next');
|
||
|
const progressContainerEl = $(infobarElement).find('.infobar-epg-progress-container');
|
||
|
const progressEl = $(infobarElement).find('.infobar-epg-progress');
|
||
|
const timeEl = $(infobarElement).find('.infobar-time');
|
||
|
|
||
|
const logoUrl = channel['tvg-logo'] || '';
|
||
|
logoEl.attr('src', logoUrl).toggle(!!logoUrl);
|
||
|
nameEl.text(channel.name || 'Canal Desconocido');
|
||
|
timeEl.text(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||
|
|
||
|
let hasEpg = false;
|
||
|
if (channel.effectiveEpgId && epgDataByChannelId[channel.effectiveEpgId]) {
|
||
|
const programs = epgDataByChannelId[channel.effectiveEpgId];
|
||
|
const now = new Date();
|
||
|
const currentProgram = programs.find(p => now >= p.startDt && now < p.stopDt);
|
||
|
|
||
|
if (currentProgram) {
|
||
|
hasEpg = true;
|
||
|
const programDuration = currentProgram.stopDt.getTime() - currentProgram.startDt.getTime();
|
||
|
const elapsed = now.getTime() - currentProgram.startDt.getTime();
|
||
|
const progressPercent = programDuration > 0 ? Math.min(100, (elapsed / programDuration) * 100) : 0;
|
||
|
|
||
|
currentEl.text(`Ahora: ${currentProgram.title}`).show();
|
||
|
progressEl.css('width', `${progressPercent}%`);
|
||
|
progressContainerEl.show();
|
||
|
|
||
|
const nextProgramIndex = programs.indexOf(currentProgram) + 1;
|
||
|
if (nextProgramIndex < programs.length) {
|
||
|
const nextProgram = programs[nextProgramIndex];
|
||
|
nextEl.text(`Siguiente: ${nextProgram.title} (${formatEPGTime(nextProgram.startDt)})`).show();
|
||
|
} else {
|
||
|
nextEl.hide();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!hasEpg) {
|
||
|
currentEl.hide();
|
||
|
nextEl.hide();
|
||
|
progressContainerEl.hide();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function startPlayerInfobarUpdate(windowId) {
|
||
|
const instance = playerInstances[windowId];
|
||
|
if (!instance) return;
|
||
|
|
||
|
if (instance.infobarInterval) clearInterval(instance.infobarInterval);
|
||
|
|
||
|
const infobarElement = instance.container.querySelector('.player-infobar');
|
||
|
updatePlayerInfobar(instance.channel, infobarElement);
|
||
|
|
||
|
instance.infobarInterval = setInterval(() => {
|
||
|
if (playerInstances[windowId] && $(instance.container).is(':visible')) {
|
||
|
updatePlayerInfobar(instance.channel, infobarElement);
|
||
|
}
|
||
|
}, 30000);
|
||
|
}
|
||
|
|
||
|
async function handlePlayCatchup(originalM3UChannelFromEpg, rawOrTransformedProgramData) {
|
||
|
let normalizedProgramData;
|
||
|
|
||
|
if (rawOrTransformedProgramData.startDt instanceof Date && rawOrTransformedProgramData.stopDt instanceof Date) {
|
||
|
normalizedProgramData = {
|
||
|
startDt: rawOrTransformedProgramData.startDt,
|
||
|
stopDt: rawOrTransformedProgramData.stopDt,
|
||
|
title: rawOrTransformedProgramData.title,
|
||
|
serviceUid2: rawOrTransformedProgramData.CanalServiceUid2,
|
||
|
channelName: originalM3UChannelFromEpg?.name
|
||
|
};
|
||
|
} else {
|
||
|
normalizedProgramData = {
|
||
|
startDt: new Date(parseInt(rawOrTransformedProgramData.FechaHoraInicio)),
|
||
|
stopDt: new Date(parseInt(rawOrTransformedProgramData.FechaHoraFin)),
|
||
|
title: rawOrTransformedProgramData.Titulo,
|
||
|
serviceUid2: rawOrTransformedProgramData.CanalServiceUid2,
|
||
|
channelName: rawOrTransformedProgramData.CanalNombre
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let effectiveOriginalM3UChannel = originalM3UChannelFromEpg;
|
||
|
if (!effectiveOriginalM3UChannel && normalizedProgramData.serviceUid2) {
|
||
|
effectiveOriginalM3UChannel = channels.find(ch => {
|
||
|
if (ch.url && (ch.url.includes(`/${normalizedProgramData.serviceUid2}/`) || ch.url.includes(`/CVXCH${normalizedProgramData.serviceUid2}/`))) return true;
|
||
|
const tvgIdServiceUid = ch['tvg-id'] ? ch['tvg-id'].split('.').pop() : null;
|
||
|
if (tvgIdServiceUid === normalizedProgramData.serviceUid2) return true;
|
||
|
if (ch.attributes && ch.attributes['ch-number'] && ch.attributes['ch-number'] === normalizedProgramData.serviceUid2) return true;
|
||
|
return false;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!effectiveOriginalM3UChannel) {
|
||
|
showNotification(`No se encontró canal M3U base (${normalizedProgramData.channelName || normalizedProgramData.serviceUid2}) para reproducir VOD.`, "warning");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!(normalizedProgramData.startDt instanceof Date) || !(normalizedProgramData.stopDt instanceof Date) || isNaN(normalizedProgramData.startDt) || isNaN(normalizedProgramData.stopDt)) {
|
||
|
showNotification("Faltan datos o fechas inválidas para reproducir el programa en catchup/VOD.", "error");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (normalizedProgramData.stopDt.getTime() <= normalizedProgramData.startDt.getTime()) {
|
||
|
showNotification("El programa seleccionado tiene una duración inválida y no se puede reproducir.", "warning");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const catchupUrl = buildMovistarCatchupUrl(effectiveOriginalM3UChannel, normalizedProgramData.startDt, normalizedProgramData.stopDt);
|
||
|
if (!catchupUrl) {
|
||
|
showNotification("No se pudo generar la URL de catchup/VOD para este programa/canal.", "error");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const catchupChannelObject = JSON.parse(JSON.stringify(effectiveOriginalM3UChannel));
|
||
|
catchupChannelObject.url = catchupUrl;
|
||
|
catchupChannelObject.name = `${effectiveOriginalM3UChannel.name} (Catchup: ${normalizedProgramData.title.substring(0, 20)}...)`;
|
||
|
if (!catchupChannelObject.kodiProps) catchupChannelObject.kodiProps = {};
|
||
|
catchupChannelObject.kodiProps['inputstream.adaptive.play_timeshift_buffer'] = 'true';
|
||
|
|
||
|
const existingTelefonicaWindow = Object.values(playerInstances).find(inst => inst.channel && (inst.channel.url.toLowerCase().includes('telefonica.com') || inst.channel.url.toLowerCase().includes('movistarplus.es')));
|
||
|
|
||
|
if (existingTelefonicaWindow) {
|
||
|
const existingId = Object.keys(playerInstances).find(key => playerInstances[key] === existingTelefonicaWindow);
|
||
|
if (existingId) {
|
||
|
showNotification("Reutilizando la ventana de Movistar+ para el programa VOD.", "info");
|
||
|
playChannelInShaka(catchupChannelObject, existingId);
|
||
|
const instance = playerInstances[existingId];
|
||
|
if(instance.container) {
|
||
|
instance.container.querySelector('.player-window-title').textContent = catchupChannelObject.name;
|
||
|
instance.container.querySelector('.player-window-title').title = catchupChannelObject.name;
|
||
|
}
|
||
|
setActivePlayer(existingId);
|
||
|
} else {
|
||
|
|
||
|
if (typeof createPlayerWindow === 'function') createPlayerWindow(catchupChannelObject);
|
||
|
}
|
||
|
} else {
|
||
|
if (typeof createPlayerWindow === 'function') {
|
||
|
createPlayerWindow(catchupChannelObject);
|
||
|
}
|
||
|
}
|
||
|
}
|