1458 lines
68 KiB
JavaScript

let channels = [];
let favorites = [];
let appHistory = [];
let currentFilter = 'all';
let currentPage = 1;
let currentM3UContent = null;
let currentM3UName = null;
let notificationTimeout = null;
let currentGroupOrder = [];
let selectedMovistarLongTokenIdForSettings = null;
let currentView = { type: 'main' };
let navigationHistory = [];
let playerInstances = {};
let activePlayerId = null;
let highestZIndex = 1950;
let hoverPlayTimeout = null;
const HOVER_PLAY_DELAY = 700;
let activeCardPreviewPlayer = null;
let activeCardPreviewElement = null;
let currentTranslations = {};
async function loadLanguage(lang) {
try {
const response = await fetch(chrome.runtime.getURL(`_locales/${lang}/messages.json`));
if (!response.ok) throw new Error(`Could not load ${lang}.json`);
const messages = await response.json();
currentTranslations = {};
for (const key in messages) {
if (Object.hasOwnProperty.call(messages, key)) {
currentTranslations[key] = messages[key].message;
}
}
document.documentElement.lang = lang;
} catch (error) {
console.error("Error loading language file:", error);
if (lang !== 'es') {
await loadLanguage('es');
}
}
}
function applyTranslations() {
document.querySelectorAll('[data-lang-key]').forEach(element => {
const key = element.getAttribute('data-lang-key');
const message = currentTranslations[key];
if (message) {
let finalMessage = message;
if (element.hasAttribute('data-lang-vars')) {
try {
const varsAttr = element.getAttribute('data-lang-vars');
const vars = JSON.parse(varsAttr);
for (const varKey in vars) {
const selector = vars[varKey];
const varElement = document.querySelector(selector);
if (varElement) {
finalMessage = finalMessage.replace(`{${varKey}}`, varElement.innerHTML);
}
}
} catch(e) { console.error(`Error parsing data-lang-vars for key ${key}:`, e)}
}
const attr = element.getAttribute('data-lang-attr');
if (attr) {
element.setAttribute(attr, finalMessage);
} else {
element.innerHTML = finalMessage;
}
}
});
}
async function showLoadFromDBModal() {
if (typeof dbPromise === 'undefined' || !dbPromise) {
showLoading(true, currentTranslations['loading'] || 'Iniciando base de datos local...');
try { if (typeof openDB === 'function') await openDB(); } catch (error) { showNotification(`Error DB: ${error.message}`, 'error'); showLoading(false); return; }
finally { showLoading(false); }
}
showLoading(true, currentTranslations['loadingLists'] || 'Cargando listas guardadas...');
try {
const files = typeof getAllFilesFromDB === 'function' ? await getAllFilesFromDB() : [];
const $list = $('#dbFilesList').empty();
if (!files || files.length === 0) {
$list.append(`<li class="list-group-item text-secondary text-center">${currentTranslations['noFileLoaded'] || "No hay listas guardadas."}</li>`);
} else {
files.sort((a,b) => new Date(b.timestamp) - new Date(a.timestamp));
files.forEach(file => {
const date = file.timestamp ? new Date(file.timestamp).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : 'Fecha desconocida';
const time = file.timestamp ? new Date(file.timestamp).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit'}) : '';
const count = typeof file.channelCount === 'number' ? file.channelCount : (typeof countChannels === 'function' ? countChannels(file.content) : 0);
$list.append(`
<li class="list-group-item d-flex justify-content-between align-items-center">
<div style="flex-grow: 1; margin-right: 1rem; overflow: hidden;">
<strong title="${escapeHtml(file.name)}" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;">${escapeHtml(file.name)}</strong>
<small class="text-secondary">${count} canales | ${date} ${time}</small>
</div>
<div>
<button class="btn-control btn-sm load-file-btn me-2" data-name="${escapeHtml(file.name)}">${currentTranslations['loadButton'] || "Cargar"}</button>
<button class="btn-control btn-sm delete-file-btn" data-name="${escapeHtml(file.name)}"></button>
</div>
</li>`);
});
$list.off('click', '.load-file-btn').on('click', '.load-file-btn', function () { loadFileToPlayer($(this).data('name')); $('#loadFromDBModal').modal('hide'); });
$list.off('click', '.delete-file-btn').on('click', '.delete-file-btn', function () { handleDeleteFromDB($(this).data('name')); });
}
$('#loadFromDBModal').modal('show');
} catch (error) {
showNotification(`Error cargando listas guardadas: ${error.message}`, 'error');
$('#dbFilesList').empty().append('<li class="list-group-item text-danger text-center">Error al cargar listas.</li>');
}
finally { showLoading(false); }
}
async function loadFileToPlayer(name) {
showLoading(true, `Cargando "${escapeHtml(name)}" desde BD...`);
currentGroupOrder = [];
try {
const file = typeof getFileFromDB === 'function' ? await getFileFromDB(name) : null;
if (!file || !file.content) throw new Error('Lista no encontrada en la base de datos.');
processM3UContent(file.content, file.name, true);
if (userSettings.autoSaveM3U) {
if (file.content.length < 4 * 1024 * 1024) {
await saveAppConfigValue('lastM3UFileContent', file.content);
await saveAppConfigValue('lastM3UFileName', file.name);
await deleteAppConfigValue('lastM3UUrl');
await deleteAppConfigValue('currentXtreamServerInfo');
} else {
await deleteAppConfigValue('lastM3UFileContent');
await deleteAppConfigValue('lastM3UFileName');
await deleteAppConfigValue('lastM3UUrl');
await deleteAppConfigValue('currentXtreamServerInfo');
showNotification('Lista cargada pero demasiado grande para guardado automático futuro.', 'info');
}
}
showNotification(`Lista "${escapeHtml(name)}" cargada (${channels.length} canales).`, 'success');
} catch (error) {
showNotification(`Error cargando "${escapeHtml(name)}": ${error.message}`, 'error');
channels = []; currentM3UContent = null; currentM3UName = null; currentGroupOrder = [];
filterAndRenderChannels();
} finally { showLoading(false); }
}
async function handleDeleteFromDB(name) {
const confirmed = await showConfirmationModal(`¿Estás seguro de eliminar la lista "${escapeHtml(name)}" de forma permanente?`, "Confirmar Eliminación", "Sí, Eliminar", "btn-danger");
if (!confirmed) return;
showLoading(true, `Eliminando "${escapeHtml(name)}"...`);
try {
if (typeof deleteFileFromDB === 'function') await deleteFileFromDB(name); else throw new Error("deleteFileFromDB no definido");
showNotification(`Lista "${escapeHtml(name)}" eliminada.`, 'success');
$(`#dbFilesList li button[data-name="${escapeHtml(name)}"]`).closest('li').fadeOut(300, function() {
$(this).remove();
if ($('#dbFilesList li').length === 0) {
$('#dbFilesList').append(`<li class="list-group-item text-secondary text-center">${currentTranslations['noFileLoaded'] || "No hay listas guardadas."}</li>`);
}
});
const lastM3UFileName = await getAppConfigValue('lastM3UFileName');
if (lastM3UFileName === name) {
channels = []; currentM3UContent = null; currentM3UName = null; currentGroupOrder = [];
filterAndRenderChannels();
await deleteAppConfigValue('lastM3UFileContent');
await deleteAppConfigValue('lastM3UFileName');
showNotification('La lista actualmente cargada fue eliminada.', 'info');
}
} catch (error) {
showNotification(`Error al eliminar "${escapeHtml(name)}": ${error.message}`, 'error');
}
finally { showLoading(false); }
}
$(document).ready(async function () {
shaka.polyfill.installAll();
if (typeof loadUserSettings === 'function') {
await loadUserSettings();
}
if (typeof applyUISettings === 'function') {
await applyUISettings();
}
makeWindowsDraggableAndResizable();
bindEvents();
if (typeof bindEpgEvents === 'function') {
bindEpgEvents();
}
if (typeof MovistarTokenHandler !== 'undefined' && typeof MovistarTokenHandler.setLogCallback === 'function') {
MovistarTokenHandler.setLogCallback(logToMovistarSettingsUI);
loadAndDisplayInitialMovistarStatus();
}
if (typeof initXCodecPanelManagement === 'function') {
initXCodecPanelManagement();
}
if (typeof phpGenerator !== 'undefined' && typeof phpGenerator.init === 'function') {
phpGenerator.init();
}
if (userSettings.persistFilters && userSettings.lastSelectedFilterTab) {
currentFilter = userSettings.lastSelectedFilterTab;
}
updateActiveFilterButton();
checkIfChannelsExist();
const urlParams = new URLSearchParams(window.location.search);
const channelNameFromUrl = urlParams.get('name');
const channelStreamUrl = urlParams.get('url');
const autoPlayFromUrl = (channelNameFromUrl && channelStreamUrl);
if (autoPlayFromUrl) {
showNotification(`Cargando ${escapeHtml(channelNameFromUrl)} desde editor...`, 'info');
const channelDataForPlayer = {
name: channelNameFromUrl,
url: channelStreamUrl,
'tvg-logo': urlParams.get('logo') || '',
'tvg-id': urlParams.get('tvgid') || '',
'group-title': urlParams.get('group') || 'Externo',
attributes: {
'tvg-id': urlParams.get('tvgid') || '',
'tvg-logo': urlParams.get('logo') || '',
'group-title': urlParams.get('group') || 'Externo',
'ch-number': urlParams.get('chnumber') || '',
...(urlParams.has('player-buffer') && { 'player-buffer': urlParams.get('player-buffer') })
},
kodiProps: {}, vlcOptions: {}, extHttp: {}
};
const licenseType = urlParams.get('licenseType');
const licenseKey = urlParams.get('licenseKey');
const serverCertBase64 = urlParams.get('serverCert');
if (licenseType) channelDataForPlayer.kodiProps['inputstream.adaptive.license_type'] = licenseType;
if (licenseKey) channelDataForPlayer.kodiProps['inputstream.adaptive.license_key'] = licenseKey;
if (serverCertBase64) channelDataForPlayer.kodiProps['inputstream.adaptive.server_certificate'] = serverCertBase64;
const streamHeaders = urlParams.get('streamHeaders');
if (streamHeaders) channelDataForPlayer.kodiProps['inputstream.adaptive.stream_headers'] = streamHeaders;
const userAgent = urlParams.get('userAgent');
const referrer = urlParams.get('referrer');
const origin = urlParams.get('origin');
if (userAgent) channelDataForPlayer.vlcOptions['http-user-agent'] = userAgent;
if (referrer) channelDataForPlayer.vlcOptions['http-referrer'] = referrer;
if (origin) channelDataForPlayer.vlcOptions['http-origin'] = origin;
const extHttpJson = urlParams.get('extHttp');
if (extHttpJson) {
try { channelDataForPlayer.extHttp = JSON.parse(extHttpJson); } catch (e) { }
}
if (typeof createPlayerWindow === 'function') {
createPlayerWindow(channelDataForPlayer);
}
} else {
await loadLastM3U();
if (userSettings.useMovistarVodAsEpg && typeof updateEpgWithMovistarVodData === 'function') {
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 (typeof startDynamicEpgUpdaters === 'function') {
startDynamicEpgUpdaters();
}
setTimeout(() => {
$(window).one('scroll mousemove touchstart', initParticles);
setTimeout(initParticles, 5000);
}, 100);
}
});
function launchEditor() {
if (!channels || channels.length === 0) {
showNotification("No hay ninguna lista M3U cargada para editar.", "warning");
return;
}
if (typeof editorHandler === 'undefined' || typeof editorHandler.init !== 'function') {
showNotification("El módulo del editor no está disponible.", "error");
return;
}
editorHandler.init(channels, currentM3UName);
const editorModal = new bootstrap.Modal(document.getElementById('editorModal'));
editorModal.show();
}
async function handleChannelCardClick(event) {
if ($(event.target).closest('.favorite-btn').length && userSettings.cardShowFavButton) {
return;
}
clearTimeout(hoverPlayTimeout);
if (typeof destroyActiveCardPreviewPlayer === 'function' && typeof activeCardPreviewPlayer !== 'undefined' && activeCardPreviewPlayer) {
await destroyActiveCardPreviewPlayer();
}
const card = $(this);
let channelUrl, seriesChannel, seasonData, episodeData;
try {
const seasonDataAttr = card.data('season-data');
if (seasonDataAttr) {
seasonData = (typeof seasonDataAttr === 'string') ? JSON.parse(seasonDataAttr) : seasonDataAttr;
const episodes = await loadXtreamSeasonEpisodes(seasonData.series_id, seasonData.season_number);
if (episodes && episodes.length > 0) {
pushNavigationState();
currentView = { type: 'episode_list', data: episodes, title: `${seasonData['group-title']} - ${seasonData.name}` };
renderCurrentView();
} else {
showNotification('No se encontraron episodios para esta temporada.', 'info');
}
return;
}
const episodeDataAttr = card.data('episode-data');
if (episodeDataAttr) {
episodeData = (typeof episodeDataAttr === 'string') ? JSON.parse(episodeDataAttr) : episodeDataAttr;
createPlayerWindow(episodeData);
return;
}
channelUrl = card.data('url');
seriesChannel = channels.find(c => c.url === channelUrl);
if (seriesChannel && seriesChannel.attributes && seriesChannel.attributes['xtream-type'] === 'series') {
const seriesId = seriesChannel.attributes['xtream-series-id'];
const seasons = await loadXtreamSeasons(seriesId, seriesChannel.name);
if (seasons && seasons.length > 0) {
pushNavigationState();
currentView = { type: 'season_list', data: seasons, title: seriesChannel.name };
renderCurrentView();
} else {
showNotification('No se encontraron temporadas para esta serie.', 'info');
}
return;
}
if (seriesChannel) {
const isMovistarStream = seriesChannel.url && (seriesChannel.url.toLowerCase().includes('telefonica.com') || seriesChannel.url.toLowerCase().includes('movistarplus.es'));
const existingMovistarWindow = isMovistarStream
? Object.values(playerInstances).find(inst =>
inst.channel &&
(inst.channel.url.toLowerCase().includes('telefonica.com') || inst.channel.url.toLowerCase().includes('movistarplus.es'))
)
: null;
if (existingMovistarWindow) {
const existingId = Object.keys(playerInstances).find(key => playerInstances[key] === existingMovistarWindow);
if (existingId) {
showNotification("Reutilizando la ventana de Movistar+ para el nuevo canal.", "info");
playChannelInShaka(seriesChannel, existingId);
const instance = playerInstances[existingId];
instance.container.querySelector('.player-window-title').textContent = seriesChannel.name;
setActivePlayer(existingId);
}
} else {
createPlayerWindow(seriesChannel);
}
} else {
showNotification('Error: Canal no encontrado para reproducir.', 'error');
}
} catch (e) {
showNotification('Error al procesar la acción: ' + e.message, 'error');
}
}
function handleGlobalKeyPress(e) {
if (!activePlayerId || !playerInstances[activePlayerId]) return;
const instance = playerInstances[activePlayerId];
if (instance.container.style.display === 'none') return;
const player = instance.player;
const ui = instance.ui;
const video = instance.videoElement;
if (!video) return;
if ($(e.target).is('input, textarea, [contenteditable="true"], .shaka-text-input')) return;
let currentChannelIndex = -1;
const currentFilteredChannels = getFilteredChannels();
if (instance.channel && instance.channel.url) {
currentChannelIndex = currentFilteredChannels.findIndex(ch => ch.url === instance.channel.url);
}
switch (e.key.toLowerCase()) {
case ' ':
e.preventDefault();
video.paused ? video.play() : video.pause();
break;
case 'f':
e.preventDefault();
if (ui) ui.toggleFullScreen();
break;
case 'i':
e.preventDefault();
showPlayerInfobar(instance.channel, instance.container.querySelector('.player-infobar'));
break;
case 'm':
e.preventDefault();
video.muted = !video.muted;
break;
case 'arrowleft':
e.preventDefault();
if (video.duration && video.currentTime > 0) {
video.currentTime = Math.max(0, video.currentTime - (e.shiftKey ? 15 : 5));
}
break;
case 'arrowright':
e.preventDefault();
if (video.duration && video.currentTime < video.duration) {
video.currentTime = Math.min(video.duration, video.currentTime + (e.shiftKey ? 15 : 5));
}
break;
case 'arrowup':
e.preventDefault();
video.volume = Math.min(1, video.volume + 0.05);
break;
case 'arrowdown':
e.preventDefault();
video.volume = Math.max(0, video.volume - 0.05);
break;
case 'pageup':
e.preventDefault();
if (currentChannelIndex > 0 && currentFilteredChannels.length > 0) {
const prevChannel = currentFilteredChannels[currentChannelIndex - 1];
playChannelInShaka(prevChannel, activePlayerId);
}
break;
case 'pagedown':
e.preventDefault();
if (currentChannelIndex !== -1 && currentChannelIndex < currentFilteredChannels.length - 1) {
const nextChannel = currentFilteredChannels[currentChannelIndex + 1];
playChannelInShaka(nextChannel, activePlayerId);
}
break;
case 'escape':
if (ui && ui.isFullScreen()) {
e.preventDefault();
ui.toggleFullScreen();
}
break;
}
}
function bindEvents() {
$('#sidebarToggleBtn').on('click', async () => {
const sidebar = $('#sidebar');
const appContainer = $('#app-container');
sidebar.toggleClass('collapsed expanded');
appContainer.toggleClass('sidebar-collapsed');
userSettings.sidebarCollapsed = sidebar.hasClass('collapsed');
if(userSettings.persistFilters) await saveAppConfigValue('userSettings', userSettings);
});
$('#loadUrl').on('click', () => {
const url = $('#urlInput').val().trim();
if (url) {
if (typeof isXtreamUrl === 'function' && isXtreamUrl(url)) {
if (typeof handleXtreamUrl === 'function') handleXtreamUrl(url);
} else {
loadUrl(url);
}
} else {
showNotification('Introduce una URL válida.', 'info');
}
});
$('#fileInput').on('change', loadFile);
$('#loadFromDBBtnHeader').on('click', showLoadFromDBModal);
$('#saveToDBBtnHeader').on('click', () => {
if (!currentM3UContent) {
showNotification('No hay lista cargada para guardar.', 'info');
return;
}
let defaultName = currentM3UName || 'mi_lista';
defaultName = defaultName.replace(/\.(m3u8?|txt|pls|m3uplus)$/i, '').replace(/^\/|\/$/g, '');
if(defaultName.includes('/')) { defaultName = defaultName.substring(defaultName.lastIndexOf('/') + 1); }
defaultName = defaultName.replace(/[^\w\s._-]/g, '_').replace(/\s+/g, '_');
if (!defaultName || defaultName === '_') defaultName = 'lista_guardada';
$('#saveM3UNameInput').val(defaultName);
const saveModal = new bootstrap.Modal(document.getElementById('saveM3UModal'));
saveModal.show();
});
$('#confirmSaveM3UBtn').on('click', handleSaveToDB);
$('#openEditorBtn').on('click', launchEditor);
$('#applyEditorChangesBtn').on('click', () => {
if (typeof editorHandler !== 'undefined' && typeof editorHandler.getFinalData === 'function') {
const editorResult = editorHandler.getFinalData();
channels = editorResult.channels;
currentGroupOrder = editorResult.groupOrder;
regenerateCurrentM3UContentFromString();
filterAndRenderChannels();
showNotification("Cambios del editor aplicados y guardados.", "success");
const editorModalInstance = bootstrap.Modal.getInstance(document.getElementById('editorModal'));
if (editorModalInstance) {
editorModalInstance.hide();
}
} else {
showNotification("Error: No se pudieron aplicar los cambios del editor.", "error");
}
});
$('#downloadM3UBtnHeader').on('click', downloadCurrentM3U);
$('#loadOrangeTvBtnHeader').on('click', async () => {
if (typeof generateM3uOrangeTv === 'function') {
const orangeTvSourceName = "OrangeTV";
if (typeof removeChannelsBySourceOrigin === 'function') {
removeChannelsBySourceOrigin(orangeTvSourceName);
}
const m3uString = await generateM3uOrangeTv();
if (m3uString && !m3uString.includes("Error general en el proceso") && !m3uString.includes("No se pudieron obtener canales")) {
if (typeof appendM3UContent === 'function') {
appendM3UContent(m3uString, orangeTvSourceName);
}
} else {
showNotification('No se generaron canales de OrangeTV o hubo un error durante el proceso.', 'warning');
if (channels.length === 0) {
if (typeof filterAndRenderChannels === 'function') filterAndRenderChannels();
}
}
} else {
showNotification("Función para cargar OrangeTV no encontrada.", "error");
}
});
$('#loadAtresplayerBtnHeader').on('click', async () => {
if (typeof generateM3UAtresplayer === 'function') {
await generateM3UAtresplayer();
} else {
if (typeof showNotification === 'function') showNotification("Funcionalidad Atresplayer no cargada.", "error");
}
});
$('#loadBarTvBtnHeader').on('click', async () => {
if (typeof generateM3uBarTv === 'function') {
await generateM3uBarTv();
} else {
if (typeof showNotification === 'function') showNotification("Funcionalidad BarTV no cargada.", "error");
}
});
$('#searchInput').on('input', debounce(filterAndRenderChannels, 300));
$('#groupFilterSidebar').on('change', async function() {
const selectedGroup = $(this).val();
currentPage = 1;
filterAndRenderChannels();
if (userSettings.persistFilters) {
userSettings.lastSelectedGroup = selectedGroup;
await saveAppConfigValue('userSettings', userSettings);
}
});
$('#sidebarGroupList').on('click', '.list-group-item', function () {
const groupName = $(this).data('group-name');
$('#groupFilterSidebar').val(groupName).trigger('change');
});
$('#showAllChannels').on('click', () => switchFilter('all'));
$('#showFavorites').on('click', () => switchFilter('favorites'));
$('#showHistory').on('click', () => switchFilter('history'));
$('#openEpgModalBtn').on('click', () => $('#epgModal').modal('show'));
$('#openMovistarVODModalBtn').on('click', openMovistarVODModal);
$('#xtreamBackButton').on('click', popNavigationState);
$('#updateDaznBtn').on('click', async () => {
if (typeof orchestrateDaznUpdate === 'function') {
if (!channels || channels.length === 0) {
showNotification('Carga una lista M3U que contenga canales de DAZN primero.', 'info');
return;
}
let daznM3uUserAgent = null;
const daznChannelInM3U = channels.find(ch =>
(ch.sourceOrigin && ch.sourceOrigin.toLowerCase() === 'dazn') ||
(ch.url && ch.url.toLowerCase().includes('dazn')) ||
(ch['tvg-id'] && ch['tvg-id'].toLowerCase().includes('dazn'))
);
if (daznChannelInM3U && daznChannelInM3U.vlcOptions && daznChannelInM3U.vlcOptions['http-user-agent']) {
daznM3uUserAgent = daznChannelInM3U.vlcOptions['http-user-agent'];
}
await orchestrateDaznUpdate(daznM3uUserAgent);
} else {
showNotification('Error: Funcionalidad DAZN no cargada.', 'error');
}
});
$('#openXtreamModalBtn').on('click', () => {
if (typeof showXtreamConnectionModal === 'function') showXtreamConnectionModal();
});
$('#openManageXCodecPanelsModalBtn').on('click', () => {
if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal !== 'undefined') {
const xcodecModalEl = document.getElementById('manageXCodecPanelsModal');
if (xcodecModalEl) {
const xcodecModalInstance = bootstrap.Modal.getOrCreateInstance(xcodecModalEl);
xcodecModalInstance.show();
if (typeof loadSavedXCodecPanels === 'function') loadSavedXCodecPanels();
} else {
showNotification("Error: Modal XCodec no encontrado.", "error");
}
} else {
showNotification("Error: Bootstrap no cargado, no se puede abrir modal XCodec.", "error");
}
});
$('#openSettingsModalBtn').on('click', () => {
$('#settingsModal').modal('show');
if (typeof updateMovistarVodCacheStatsUI === 'function') {
updateMovistarVodCacheStatsUI();
}
});
$(document).on('keydown', handleGlobalKeyPress);
$('#player-taskbar').on('click', '.taskbar-item', function() {
const windowId = $(this).data('windowId');
if (windowId) {
setActivePlayer(windowId);
}
});
$('#prevPage').on('click', () => changePage(currentPage - 1));
$('#nextPage').on('click', () => changePage(currentPage + 1));
$('#channelGrid').on('click', '.channel-card', handleChannelCardClick);
$('#channelGrid').on('mouseenter', '.channel-card', function() {
if (!userSettings.enableHoverPreview) return;
const card = $(this);
if (Object.keys(playerInstances).length > 0) return;
if (activeCardPreviewPlayer && activeCardPreviewElement && activeCardPreviewElement[0] !== card[0]) {
if (typeof destroyActiveCardPreviewPlayer === 'function') destroyActiveCardPreviewPlayer();
}
clearTimeout(hoverPlayTimeout);
hoverPlayTimeout = setTimeout(async () => {
const channelUrl = card.data('url');
if (!channelUrl) return;
const channel = channels.find(c => c.url === channelUrl);
if (channel) {
if (channel.attributes && channel.attributes['xtream-type'] === 'series') {
return;
}
if (typeof playChannelInCardPreview === 'function') {
activeCardPreviewElement = card;
card.addClass('is-playing-preview');
await playChannelInCardPreview(channel, card.find('.card-video-preview-container')[0]);
}
}
}, HOVER_PLAY_DELAY);
});
$('#channelGrid').on('mouseleave', '.channel-card', function() {
clearTimeout(hoverPlayTimeout);
if (activeCardPreviewElement && activeCardPreviewElement[0] === $(this)[0]) {
if (typeof destroyActiveCardPreviewPlayer === 'function') destroyActiveCardPreviewPlayer();
}
});
$('#channelGrid').on('click', '.favorite-btn', handleFavoriteButtonClick);
$('#channelGrid').on('error', '.channel-logo', function () {
this.classList.add('error');
this.style.display = 'none';
const placeholder = $(this).siblings('.epg-icon-placeholder');
if (placeholder.length) { placeholder.show(); }
else { $(this).parent().addClass('no-logo-fallback'); }
});
$('#saveSettingsBtn').on('click', () => { if(typeof saveUserSettings === 'function') saveUserSettings(); });
const rangeInputsSelector = '#epgNameMatchThreshold, #playerBufferInput, #channelCardSizeInput, #abrDefaultBandwidthEstimateInput, #manifestRetryMaxAttemptsInput, #manifestRetryTimeoutInput, #segmentRetryMaxAttemptsInput, #segmentRetryTimeoutInput, #epgDensityInput, #channelsPerPageInput, #particleOpacityInput, #shakaDefaultPresentationDelayInput, #shakaAudioVideoSyncThresholdInput, #playerWindowOpacityInput';
$(rangeInputsSelector).on('input', function () {
const id = this.id;
const value = $(this).val();
if(id === 'epgNameMatchThreshold') $('#epgNameMatchThresholdValue').text(value + '%');
if(id === 'playerBufferInput') $('#playerBufferValue').text(value + 's');
if(id === 'channelCardSizeInput') {
const size = value + 'px';
$('#channelCardSizeValue').text(size);
document.documentElement.style.setProperty('--m3u-grid-minmax-size', size);
}
if(id === 'channelsPerPageInput') $('#channelsPerPageValue').text(value);
if(id === 'abrDefaultBandwidthEstimateInput') $('#abrDefaultBandwidthEstimateValue').text(value + ' Kbps');
if(id === 'manifestRetryMaxAttemptsInput') $('#manifestRetryMaxAttemptsValue').text(value);
if(id === 'manifestRetryTimeoutInput') $('#manifestRetryTimeoutValue').text(value);
if(id === 'segmentRetryMaxAttemptsInput') $('#segmentRetryMaxAttemptsValue').text(value);
if(id === 'segmentRetryTimeoutInput') $('#segmentRetryTimeoutValue').text(value);
if(id === 'epgDensityInput') $('#epgDensityValue').text(value + 'px/h');
if(id === 'particleOpacityInput') $('#particleOpacityValue').text(value + '%');
if(id === 'shakaDefaultPresentationDelayInput') $('#shakaDefaultPresentationDelayValue').text(parseFloat(value).toFixed(parseFloat(value) % 1 === 0 ? 0 : 1) + 's');
if(id === 'shakaAudioVideoSyncThresholdInput') $('#shakaAudioVideoSyncThresholdValue').text(parseFloat(value).toFixed(parseFloat(value) % 1 === 0 ? 0 : 2) + 's');
if(id === 'playerWindowOpacityInput') {
$('#playerWindowOpacityValue').text(Math.round(value * 100) + '%');
Object.values(playerInstances).forEach(instance => {
if (instance.container) {
instance.container.style.setProperty('--player-window-opacity', value);
}
});
}
});
$('#exportSettingsBtn').on('click', () => { if(typeof exportSettings === 'function') exportSettings(); });
$('#importSettingsInput').on('change', (event) => { if(typeof importSettings === 'function') importSettings(event); });
$('#clearCacheBtn').on('click', clearCacheAndReload);
$('#connectXtreamServerBtn').on('click', () => {
if (typeof handleConnectXtreamServer === 'function') handleConnectXtreamServer();
});
$('#xtreamConfirmGroupSelectionBtn').on('click', () => {
if (typeof handleXtreamGroupSelection === 'function') handleXtreamGroupSelection();
});
$('#saveXtreamServerBtn').on('click', () => {
if (typeof handleSaveXtreamServer === 'function') handleSaveXtreamServer();
});
$('#savedXtreamServersList').on('click', '.load-xtream-server-btn', function() {
const serverId = parseInt($(this).data('id'), 10);
if (typeof loadXtreamServerToForm === 'function') loadXtreamServerToForm(serverId);
});
$('#savedXtreamServersList').on('click', '.delete-xtream-server-btn', function() {
const serverId = parseInt($(this).data('id'), 10);
if (typeof handleDeleteXtreamServer === 'function') handleDeleteXtreamServer(serverId);
});
$('#sendM3UToServerBtn').on('click', () => {
const urlFromInput = $('#m3uUploadServerUrlInput').val()?.trim();
if (typeof sendM3UToServer === 'function') {
sendM3UToServer(urlFromInput);
} else {
showNotification("Error: Función para enviar M3U no encontrada.", "error");
}
});
$('#movistarLoginBtnSettings').on('click', handleMovistarLogin);
$('#movistarValidateAllBtnSettings').on('click', handleMovistarValidateAllTokens);
$('#movistarDeleteExpiredBtnSettings').on('click', handleMovistarDeleteExpiredTokens);
$('#movistarAddManualTokenBtnSettings').on('click', handleMovistarAddManualToken);
$('#movistarLongTokensTableBodySettings').on('click', '.delete-long-token-btn-settings', function() {
const tokenId = $(this).closest('tr').data('tokenid');
if (tokenId) handleMovistarDeleteSingleLongToken(tokenId);
});
$('#movistarLongTokensTableBodySettings').on('click', '.validate-long-token-btn-settings', function() {
const tokenId = $(this).closest('tr').data('tokenid');
if (tokenId) handleMovistarValidateSingleLongToken(tokenId);
});
$('#movistarLongTokensTableBodySettings').on('click', 'tr', function(event) {
const tokenId = $(this).data('tokenid');
if (tokenId && !$(event.target).closest('button').length) {
selectedMovistarLongTokenIdForSettings = tokenId;
$('#movistarLongTokensTableBodySettings tr').removeClass('table-active');
$(this).addClass('table-active');
$('#selectedLongTokenIdDisplaySettings').text(`...${tokenId.slice(-12)}`);
$('#movistarDeviceManagementSectionSettings').show();
$('#movistarLoadDevicesForSettingsBtn').prop('disabled', false);
$('#movistarDevicesListForSettings').html('<div class="list-group-item text-muted text-center">Carga los dispositivos para el token seleccionado arriba.</div>');
$('#movistarAssociateDeviceForSettingsBtn').prop('disabled', true);
$('#movistarRegisterNewDeviceForSettingsBtn').prop('disabled', false);
}
});
$('#movistarLoadDevicesForSettingsBtn').on('click', handleMovistarLoadDevicesForSettings);
$('#movistarAssociateDeviceForSettingsBtn').on('click', handleMovistarAssociateDeviceForSettings);
$('#movistarRegisterNewDeviceForSettingsBtn').on('click', handleMovistarRegisterNewDeviceForSettings);
$('#movistarRefreshCdnBtnSettings').on('click', handleMovistarRefreshCdnToken);
$('#movistarCopyCdnBtnSettings').on('click', handleMovistarCopyCdnToken);
$('#movistarApplyCdnToChannelsBtnSettings').on('click', handleMovistarApplyCdnToChannels);
$('#clearMovistarVodCacheBtnSettings').on('click', handleClearMovistarVodCache);
$('#loadMovistarVODBtn').on('click', loadMovistarVODData);
$('#movistarVODDateInput').on('change', function() {
movistarVodSelectedDate = new Date($(this).val() + 'T00:00:00');
loadMovistarVODData();
});
$('#movistarVODModal-channel-filter, #movistarVODModal-genre-filter').on('change', renderMovistarVODPrograms);
$('#movistarVODModal-search-input').on('input', debounce(renderMovistarVODPrograms, 300));
$('#movistarVODModal-programs').on('click', '.movistar-vod-card', function() {
const programArrayIndex = parseInt($(this).data('program-array-index'), 10);
if (!isNaN(programArrayIndex) && movistarVodFilteredPrograms[programArrayIndex]) {
const program = movistarVodFilteredPrograms[programArrayIndex];
if (program && typeof handleMovistarVODProgramClick === 'function') {
handleMovistarVODProgramClick(program);
}
} else {
showNotification("Error al seleccionar el programa VOD.", "error");
}
});
$('#movistarVODModal-prev-page').on('click', function() {
if (movistarVodCurrentPage > 1) {
movistarVodCurrentPage--;
displayCurrentMovistarVODPage();
updateMovistarVODPaginationControls();
}
});
$('#movistarVODModal-next-page').on('click', function() {
const totalPages = Math.ceil(movistarVodFilteredPrograms.length / MOVISTAR_VOD_ITEMS_PER_PAGE);
if (movistarVodCurrentPage < totalPages) {
movistarVodCurrentPage++;
displayCurrentMovistarVODPage();
updateMovistarVODPaginationControls();
}
});
}
async function applyUISettings() {
await loadLanguage(userSettings.language);
applyTranslations();
if (typeof populateUserSettingsForm === 'function') populateUserSettingsForm();
if (typeof applyThemeAndFont === 'function') applyThemeAndFont();
const sidebar = $('#sidebar');
const appContainer = $('#app-container');
if (userSettings.sidebarCollapsed && window.innerWidth >= 992) {
sidebar.removeClass('expanded').addClass('collapsed');
appContainer.addClass('sidebar-collapsed');
} else if(window.innerWidth >= 992) {
sidebar.removeClass('collapsed').addClass('expanded');
appContainer.removeClass('sidebar-collapsed');
} else {
sidebar.removeClass('expanded').addClass('collapsed');
appContainer.addClass('sidebar-collapsed');
}
document.documentElement.style.setProperty('--m3u-grid-minmax-size', userSettings.channelCardSize + 'px');
document.documentElement.style.setProperty('--card-logo-aspect-ratio', userSettings.cardLogoAspectRatio === 'auto' ? '16/9' : userSettings.cardLogoAspectRatio);
Object.values(playerInstances).forEach(instance => {
if (typeof updatePlayerConfigFromSettings === 'function') {
updatePlayerConfigFromSettings(instance.player);
}
if (instance.container) {
instance.container.style.setProperty('--player-window-opacity', userSettings.playerWindowOpacity);
}
});
if (typeof initParticles === 'function') initParticles();
if (userSettings.persistFilters) {
if(userSettings.lastSelectedFilterTab) currentFilter = userSettings.lastSelectedFilterTab;
}
if (channels.length > 0) filterAndRenderChannels();
}
function initParticles() {
if (typeof particlesJS === 'function' && document.getElementById('particles-js') && !document.getElementById('particles-js').dataset.initialized && userSettings.particlesEnabled) {
document.getElementById('particles-js').dataset.initialized = 'true';
const particleColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-secondary').trim();
const particleLineColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-primary').trim();
document.documentElement.style.setProperty('--particle-opacity', userSettings.particleOpacity);
particlesJS('particles-js', {
"particles": {
"number": { "value": 30, "density": { "enable": true, "value_area": 1200 } },
"color": { "value": particleColor },
"shape": { "type": "circle" },
"opacity": { "value": 1, "random": true, "anim": { "enable": true, "speed": 0.15, "opacity_min": 0.3, "sync": false } },
"size": { "value": 1.5, "random": true },
"line_linked": { "enable": true, "distance": 160, "color": particleLineColor, "opacity": 0.5, "width": 1 },
"move": { "enable": true, "speed": 0.8, "direction": "none", "random": true, "straight": false, "out_mode": "out" }
},
"interactivity": { "detect_on": "canvas", "events": { "onhover": { "enable": false }, "onclick": { "enable": false }, "resize": true } },
"retina_detect": true
});
const particlesCanvas = document.querySelector('#particles-js canvas');
if(particlesCanvas && particlesCanvas.style) {
particlesCanvas.style.setProperty('opacity', '1', 'important');
}
$('#particles-js').removeClass('disabled');
} else if (!userSettings.particlesEnabled && document.getElementById('particles-js')) {
$('#particles-js').addClass('disabled');
if(typeof pJSDom !== 'undefined' && pJSDom.length > 0 && pJSDom[0].pJS) {
pJSDom[0].pJS.fn.vendors.destroypJS();
pJSDom = [];
if (document.getElementById('particles-js')) document.getElementById('particles-js').dataset.initialized = 'false';
}
}
}
window.updateM3UWithDaznData = function(daznChannelDetailsList) {
if (!channels || channels.length === 0) {
if(typeof showNotification === 'function') showNotification('DAZN: No hay lista M3U cargada para actualizar.', 'info');
return;
}
if (!daznChannelDetailsList || daznChannelDetailsList.length === 0) {
if(typeof showNotification === 'function') showNotification('DAZN: No se recibieron datos de canales para la actualización.', 'info');
return;
}
let updatedCount = 0;
const daznDataMapByLinearId = new Map();
daznChannelDetailsList.forEach(daznChannel => {
if (daznChannel.daznLinearId) {
daznDataMapByLinearId.set(daznChannel.daznLinearId, daznChannel);
}
});
channels.forEach(m3uChannel => {
let currentChannelLinearId = null;
if (m3uChannel.url) {
const urlMatch = m3uChannel.url.match(/dazn-linear-(\d+)/);
if (urlMatch && urlMatch[1]) {
currentChannelLinearId = urlMatch[1];
}
}
if (!currentChannelLinearId && m3uChannel['tvg-id']) {
const tvgIdMatch = String(m3uChannel['tvg-id']).match(/dazn-linear-(\d+)/i);
if (tvgIdMatch && tvgIdMatch[1]) {
currentChannelLinearId = tvgIdMatch[1];
}
}
if (currentChannelLinearId && daznDataMapByLinearId.has(currentChannelLinearId)) {
const daznUpdate = daznDataMapByLinearId.get(currentChannelLinearId);
m3uChannel.url = daznUpdate.baseUrl;
if (!m3uChannel.kodiProps) {
m3uChannel.kodiProps = {};
}
if (!m3uChannel.vlcOptions) {
m3uChannel.vlcOptions = {};
}
if (daznUpdate.cdnTokenName && daznUpdate.cdnTokenValue) {
m3uChannel.kodiProps['inputstream.adaptive.stream_headers'] = `${daznUpdate.cdnTokenName}=${daznUpdate.cdnTokenValue}`;
} else {
delete m3uChannel.kodiProps['inputstream.adaptive.stream_headers'];
}
if (daznUpdate.streamUserAgent) {
m3uChannel.vlcOptions['http-user-agent'] = daznUpdate.streamUserAgent;
}
m3uChannel.sourceOrigin = "DAZN";
updatedCount++;
}
});
if (updatedCount > 0) {
if(typeof showNotification === 'function') showNotification(`DAZN: ${updatedCount} canales actualizados en tu lista M3U.`, 'success');
regenerateCurrentM3UContentFromString();
filterAndRenderChannels();
} else {
if(typeof showNotification === 'function') showNotification('DAZN: No se encontraron canales en tu M3U que coincidieran con los datos de DAZN para actualizar.', 'info');
}
};
function logToMovistarSettingsUI(message, type = 'info') {
const logArea = $('#movistarLogAreaSettings');
if (logArea.length) {
const timestamp = new Date().toLocaleTimeString();
const existingLog = logArea.val();
const newLog = `[${timestamp}] ${message}\n`;
logArea.val(existingLog + newLog);
logArea.scrollTop(logArea[0].scrollHeight);
}
}
async function loadAndDisplayInitialMovistarStatus() {
if (!window.MovistarTokenHandler) return;
logToMovistarSettingsUI("Cargando estado inicial de Movistar+...", "info");
try {
const status = await window.MovistarTokenHandler.getShortTokenStatus();
updateMovistarCdnTokenUI(status.token, status.expiry);
await loadAndRenderLongTokensListSettings();
} catch (error) {
logToMovistarSettingsUI(`Error cargando estado inicial: ${error.message}`, "error");
}
}
function updateMovistarCdnTokenUI(token, expiryTimestamp) {
$('#movistarCdnTokenDisplaySettings').val(token || "");
const expiryDate = expiryTimestamp ? new Date(expiryTimestamp * 1000) : null;
if (expiryDate && expiryTimestamp > Math.floor(Date.now() / 1000)) {
$('#movistarCdnTokenExpirySettings').text(`${chrome.i18n.getMessage("movistarExpiresHeader") || "Expira"}: ${expiryDate.toLocaleString()}`);
$('#movistarCdnTokenExpirySettings').removeClass('text-danger').addClass('text-success');
} else if (expiryDate) {
$('#movistarCdnTokenExpirySettings').text(`Expirado: ${expiryDate.toLocaleString()}`);
$('#movistarCdnTokenExpirySettings').removeClass('text-success').addClass('text-danger');
} else {
$('#movistarCdnTokenExpirySettings').text(`${chrome.i18n.getMessage("movistarExpiresHeader") || "Expira"}: -`);
$('#movistarCdnTokenExpirySettings').removeClass('text-success text-danger');
}
$('#movistarCopyCdnBtnSettings').prop('disabled', !token);
$('#movistarApplyCdnToChannelsBtnSettings').prop('disabled', !token || (expiryTimestamp <= Math.floor(Date.now()/1000)));
}
async function loadAndRenderLongTokensListSettings() {
if (!window.MovistarTokenHandler) return;
const tbody = $('#movistarLongTokensTableBodySettings');
tbody.html(`<tr><td colspan="6" class="text-center p-3"><i class="fas fa-spinner fa-spin"></i> ${chrome.i18n.getMessage("movistarLoading") || "Cargando..."}</td></tr>`);
selectedMovistarLongTokenIdForSettings = null;
$('#movistarDeviceManagementSectionSettings').hide();
$('#movistarLoadDevicesForSettingsBtn').prop('disabled', true);
try {
const tokens = await window.MovistarTokenHandler.getAllLongTokens();
if (tokens.length === 0) {
tbody.html(`<tr><td colspan="6" class="text-center p-3 text-muted">${chrome.i18n.getMessage("xtreamNoSavedServers") || "No hay tokens largos guardados."}</td></tr>`);
return;
}
tokens.sort((a, b) => (b.expiry_tstamp || 0) - (a.expiry_tstamp || 0));
tbody.empty();
const nowSeconds = Math.floor(Date.now() / 1000);
tokens.forEach(token => {
const isExpired = (token.expiry_tstamp || 0) < nowSeconds;
const expiryDate = token.expiry_tstamp ? new Date(token.expiry_tstamp * 1000) : null;
const expiryString = expiryDate ? expiryDate.toLocaleDateString() : 'N/D';
let statusText = isExpired ? 'Expirado' : (token.device_id ? 'Válido' : 'Sin DeviceID');
let statusClass = isExpired ? 'text-danger' : (token.device_id ? 'text-success' : 'text-warning');
const tr = $('<tr>').data('tokenid', token.id).css('cursor', 'pointer');
tr.append($('<td>').text(`...${token.id.slice(-12)}`).attr('title', token.id));
tr.append($('<td>').text(token.account_nbr || 'N/A'));
tr.append($('<td>').text(token.device_id ? `...${token.device_id.slice(-6)}` : 'NULO').attr('title', token.device_id || 'Sin Device ID'));
tr.append($('<td>').text(expiryString));
tr.append($('<td>').addClass(statusClass).text(statusText));
tr.append($('<td>')
.append($('<button>').addClass('btn btn-outline-danger btn-sm delete-long-token-btn-settings me-1').attr('title', 'Eliminar').html('<i class="fas fa-trash"></i>'))
.append($('<button>').addClass('btn btn-outline-info btn-sm validate-long-token-btn-settings').attr('title', 'Validar').html('<i class="fas fa-check"></i>'))
);
tbody.append(tr);
});
} catch (error) {
logToMovistarSettingsUI(`Error cargando lista de tokens largos: ${error.message}`, "error");
tbody.html(`<tr><td colspan="6" class="text-center p-3 text-danger">Error: ${escapeHtml(error.message)}</td></tr>`);
}
}
async function handleMovistarLogin() {
const username = $('#movistarUsernameSettingsInput').val();
const password = $('#movistarPasswordSettingsInput').val();
logToMovistarSettingsUI("Iniciando login...", "info");
showLoading(true, "Iniciando sesión Movistar+...");
const result = await window.MovistarTokenHandler.loginAndGetTokens(username, password);
showLoading(false);
logToMovistarSettingsUI(result.message, result.success ? "success" : "error");
showNotification(result.message, result.success ? "success" : "error");
if (result.success) {
updateMovistarCdnTokenUI(result.shortToken, result.shortTokenExpiry);
await loadAndRenderLongTokensListSettings();
$('#movistarUsernameSettingsInput').val('');
$('#movistarPasswordSettingsInput').val('');
}
}
async function handleMovistarValidateAllTokens() {
logToMovistarSettingsUI("Validando todos los tokens largos...", "info");
showLoading(true, "Validando tokens...");
const result = await window.MovistarTokenHandler.validateAllLongTokens();
showLoading(false);
let summary = `Validación completa: ${result.validated} validados, ${result.functional} funcionales, ${result.expired} expirados, ${result.noDeviceId} sin Device ID.`;
if(result.refreshed > 0 || result.refreshErrors > 0) {
summary += ` Refrescos: ${result.refreshed} éxitos, ${result.refreshErrors} fallos.`;
}
logToMovistarSettingsUI(summary, "info");
showNotification(summary, "info", 6000);
await loadAndRenderLongTokensListSettings();
}
async function handleMovistarDeleteExpiredTokens() {
logToMovistarSettingsUI("Eliminando tokens largos expirados...", "info");
const userConfirmed = await showConfirmationModal(
"¿Seguro que quieres eliminar todos los tokens largos expirados?",
"Confirmar Eliminación", "Sí, Eliminar Expirados", "btn-warning"
);
if (!userConfirmed) {
logToMovistarSettingsUI("Eliminación de expirados cancelada.", "info");
return;
}
showLoading(true, "Eliminando tokens expirados...");
const deletedCount = await window.MovistarTokenHandler.deleteExpiredLongTokens();
showLoading(false);
logToMovistarSettingsUI(`${deletedCount} tokens expirados eliminados.`, "info");
showNotification(`${deletedCount} tokens expirados eliminados.`, "success");
await loadAndRenderLongTokensListSettings();
}
async function handleMovistarAddManualToken() {
const jwt = $('#movistarAddManualTokenJwtInputSettings').val().trim();
const deviceId = $('#movistarAddManualTokenDeviceIdInputSettings').val().trim() || null;
if (!jwt) {
showNotification("Por favor, pega el token JWT largo.", "warning");
return;
}
logToMovistarSettingsUI(`Intentando añadir token manual: ${jwt.substring(0,20)}...`, "info");
showLoading(true, "Añadiendo token...");
try {
await window.MovistarTokenHandler.addLongTokenManually(jwt, deviceId);
logToMovistarSettingsUI("Token manual añadido con éxito.", "success");
showNotification("Token manual añadido con éxito.", "success");
$('#movistarAddManualTokenJwtInputSettings').val('');
$('#movistarAddManualTokenDeviceIdInputSettings').val('');
await loadAndRenderLongTokensListSettings();
} catch (error) {
logToMovistarSettingsUI(`Error añadiendo token manual: ${error.message}`, "error");
showNotification(`Error añadiendo token: ${error.message}`, "error");
} finally {
showLoading(false);
}
}
async function handleMovistarDeleteSingleLongToken(tokenId) {
logToMovistarSettingsUI(`Intentando eliminar token ${tokenId.slice(-12)}...`, "info");
const userConfirmed = await showConfirmationModal(
`¿Seguro que quieres eliminar el token largo con ID ...${tokenId.slice(-12)}?`,
"Confirmar Eliminación", "Sí, Eliminar Token", "btn-danger"
);
if (!userConfirmed) {
logToMovistarSettingsUI("Eliminación cancelada.", "info");
return;
}
showLoading(true, "Eliminando token...");
try {
await window.MovistarTokenHandler.deleteLongToken(tokenId);
logToMovistarSettingsUI(`Token ${tokenId.slice(-12)} eliminado.`, "success");
showNotification(`Token ${tokenId.slice(-12)} eliminado.`, "success");
await loadAndRenderLongTokensListSettings();
if (selectedMovistarLongTokenIdForSettings === tokenId) {
selectedMovistarLongTokenIdForSettings = null;
$('#movistarDeviceManagementSectionSettings').hide();
$('#movistarLoadDevicesForSettingsBtn').prop('disabled', true);
}
} catch (error) {
logToMovistarSettingsUI(`Error eliminando token: ${error.message}`, "error");
showNotification(`Error eliminando token: ${error.message}`, "error");
} finally {
showLoading(false);
}
}
async function handleMovistarValidateSingleLongToken(tokenId) {
logToMovistarSettingsUI(`Validando token ${tokenId.slice(-12)}...`, "info");
showLoading(true, `Validando token ...${tokenId.slice(-12)}`);
const tokens = await MovistarTokenHandler.getAllLongTokens();
const token = tokens.find(t => t.id === tokenId);
showLoading(false);
if (token) {
const nowSeconds = Math.floor(Date.now() / 1000);
const isExpired = (token.expiry_tstamp || 0) < nowSeconds;
let msg = `Token ...${tokenId.slice(-12)}: `;
if (isExpired) msg += "Expirado.";
else if (!token.device_id) msg += "Válido pero SIN Device ID.";
else msg += "Válido y funcional.";
logToMovistarSettingsUI(msg, "info");
showNotification(msg, "info");
} else {
logToMovistarSettingsUI(`Token ...${tokenId.slice(-12)} no encontrado para validar.`, "warning");
showNotification(`Token ...${tokenId.slice(-12)} no encontrado.`, "warning");
}
await loadAndRenderLongTokensListSettings();
}
async function handleMovistarLoadDevicesForSettings() {
if (!selectedMovistarLongTokenIdForSettings) {
showNotification("Selecciona un token largo de la lista primero.", "warning");
return;
}
logToMovistarSettingsUI(`Cargando dispositivos para token ${selectedMovistarLongTokenIdForSettings.slice(-12)}...`, "info");
showLoading(true, "Cargando dispositivos...");
$('#movistarDevicesListForSettings').html('<div class="list-group-item text-muted text-center"><i class="fas fa-spinner fa-spin"></i> Cargando...</div>');
$('#movistarAssociateDeviceForSettingsBtn').prop('disabled', true);
try {
const devices = await window.MovistarTokenHandler.getDevicesForToken(selectedMovistarLongTokenIdForSettings);
$('#movistarDevicesListForSettings').empty();
if (devices.length === 0) {
$('#movistarDevicesListForSettings').html('<div class="list-group-item text-muted text-center">No se encontraron dispositivos para esta cuenta.</div>');
} else {
devices.forEach(dev => {
const item = $(`
<label class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<input class="form-check-input me-2" type="radio" name="movistarDeviceForSettings" value="${escapeHtml(dev.id)}" id="devRadioSettings_${dev.id.replace(/[^a-zA-Z0-9]/g, '')}">
<strong>${escapeHtml(dev.name)}</strong> <small class="text-muted">(...${escapeHtml(dev.id.slice(-6))}, ${escapeHtml(dev.type)})</small>
</div>
${dev.is_associated ? '<span class="badge bg-success rounded-pill">Asociado</span>' : ''}
</label>
`);
item.find('input').on('change', function() {
$('#movistarAssociateDeviceForSettingsBtn').prop('disabled', !this.checked);
});
$('#movistarDevicesListForSettings').append(item);
});
}
} catch (error) {
logToMovistarSettingsUI(`Error cargando dispositivos: ${error.message}`, "error");
showNotification(`Error cargando dispositivos: ${error.message}`, "error");
$('#movistarDevicesListForSettings').html(`<div class="list-group-item text-danger text-center">Error: ${escapeHtml(error.message)}</div>`);
} finally {
showLoading(false);
}
}
async function handleMovistarAssociateDeviceForSettings() {
const selectedDeviceId = $('input[name="movistarDeviceForSettings"]:checked').val();
if (!selectedMovistarLongTokenIdForSettings || !selectedDeviceId) {
showNotification("Selecciona un token largo y un dispositivo para asociar.", "warning");
return;
}
logToMovistarSettingsUI(`Asociando dispositivo ${selectedDeviceId.slice(-6)} a token ${selectedMovistarLongTokenIdForSettings.slice(-12)}...`, "info");
showLoading(true, "Asociando dispositivo...");
try {
await window.MovistarTokenHandler.associateDeviceToToken(selectedMovistarLongTokenIdForSettings, selectedDeviceId);
logToMovistarSettingsUI("Dispositivo asociado con éxito.", "success");
showNotification("Dispositivo asociado con éxito.", "success");
await loadAndRenderLongTokensListSettings();
await handleMovistarLoadDevicesForSettings();
} catch (error) {
logToMovistarSettingsUI(`Error asociando dispositivo: ${error.message}`, "error");
showNotification(`Error asociando dispositivo: ${error.message}`, "error");
} finally {
showLoading(false);
}
}
async function handleMovistarRegisterNewDeviceForSettings() {
if (!selectedMovistarLongTokenIdForSettings) {
showNotification("Selecciona un token largo de la lista primero.", "warning");
return;
}
logToMovistarSettingsUI(`Registrando nuevo dispositivo para token ${selectedMovistarLongTokenIdForSettings.slice(-12)}...`, "info");
const userConfirmed = await showConfirmationModal(
"Esto intentará registrar un NUEVO dispositivo en tu cuenta Movistar+ y asociarlo a este token largo. Puede fallar si has alcanzado el límite de dispositivos. ¿Continuar?",
"Confirmar Registro de Nuevo Dispositivo", "Sí, Registrar Nuevo", "btn-warning"
);
if (!userConfirmed) {
logToMovistarSettingsUI("Registro de nuevo dispositivo cancelado.", "info");
return;
}
showLoading(true, "Registrando nuevo dispositivo...");
try {
await window.MovistarTokenHandler.registerAndAssociateNewDeviceToToken(selectedMovistarLongTokenIdForSettings);
logToMovistarSettingsUI("Nuevo dispositivo registrado y asociado con éxito.", "success");
showNotification("Nuevo dispositivo registrado y asociado.", "success");
await loadAndRenderLongTokensListSettings();
await handleMovistarLoadDevicesForSettings();
} catch (error) {
logToMovistarSettingsUI(`Error registrando nuevo dispositivo: ${error.message}`, "error");
showNotification(`Error registrando dispositivo: ${error.message}`, "error");
} finally {
showLoading(false);
}
}
async function handleMovistarRefreshCdnToken() {
logToMovistarSettingsUI("Refrescando token CDN...", "info");
showLoading(true, "Refrescando Token CDN...");
const result = await window.MovistarTokenHandler.refreshCdnToken(true);
showLoading(false);
logToMovistarSettingsUI(result.message, result.success ? "success" : "error");
showNotification(result.message, result.success ? "success" : "error");
if (result.success) {
updateMovistarCdnTokenUI(result.shortToken, result.shortTokenExpiry);
}
}
async function handleMovistarCopyCdnToken() {
const tokenToCopy = $('#movistarCdnTokenDisplaySettings').val();
if (!tokenToCopy) {
showNotification("No hay token CDN para copiar.", "warning");
return;
}
try {
await navigator.clipboard.writeText(tokenToCopy);
showNotification("Token CDN copiado al portapapeles.", "success");
logToMovistarSettingsUI("Token CDN copiado.", "info");
} catch (err) {
showNotification("Error al copiar. Revisa la consola.", "error");
logToMovistarSettingsUI(`Error al copiar token: ${err.message}`, "error");
}
}
async function handleMovistarApplyCdnToChannels() {
logToMovistarSettingsUI("Aplicando token CDN a canales Movistar+...", "info");
const status = await window.MovistarTokenHandler.getShortTokenStatus();
if (!status.token || status.expiry <= Math.floor(Date.now()/1000)) {
showNotification("El token CDN actual no es válido o ha expirado. Refréscalo primero.", "warning");
logToMovistarSettingsUI("Aplicación cancelada: token CDN no válido/expirado.", "warning");
return;
}
if (!channels || channels.length === 0) {
showNotification("No hay lista M3U cargada para aplicar el token.", "info");
logToMovistarSettingsUI("Aplicación cancelada: no hay canales cargados.", "info");
return;
}
showLoading(true, "Aplicando token a canales...");
let updatedCount = 0;
const tokenHeaderString = `X-TCDN-Token=${status.token}`;
channels.forEach(channel => {
if (channel && channel.url && (channel.url.toLowerCase().includes('telefonica.com') || channel.url.toLowerCase().includes('movistarplus.es')) ) {
channel.kodiProps = channel.kodiProps || {};
let headers = channel.kodiProps['inputstream.adaptive.stream_headers'] || '';
let headerParts = headers.split('&').filter(part => part && !part.toLowerCase().startsWith('x-tcdn-token='));
headerParts.push(tokenHeaderString);
channel.kodiProps['inputstream.adaptive.stream_headers'] = headerParts.join('&');
channel.sourceOrigin = "Movistar+";
updatedCount++;
}
});
if (updatedCount > 0) {
showNotification(`Token CDN aplicado a ${updatedCount} canales de Movistar+.`, "success");
logToMovistarSettingsUI(`Token aplicado a ${updatedCount} canales.`, "success");
regenerateCurrentM3UContentFromString();
filterAndRenderChannels();
} else {
showNotification("No se encontraron canales de Movistar+ para aplicar el token.", "info");
logToMovistarSettingsUI("No se encontraron canales Movistar+.", "info");
}
showLoading(false);
}
async function handleClearMovistarVodCache() {
logToMovistarSettingsUI("Limpiando caché VOD Movistar+...", "info");
const userConfirmed = await showConfirmationModal(
"¿Seguro que quieres eliminar TODOS los datos de caché de Movistar VOD guardados localmente?",
"Confirmar Limpieza de Caché VOD", "Sí, Limpiar Caché", "btn-danger"
);
if (!userConfirmed) {
logToMovistarSettingsUI("Limpieza de caché VOD cancelada.", "info");
return;
}
showLoading(true, "Limpiando caché VOD...");
try {
if (typeof clearMovistarVodCacheFromDB === 'function') {
await clearMovistarVodCacheFromDB();
logToMovistarSettingsUI("Caché VOD Movistar+ limpiada con éxito.", "success");
showNotification("Caché VOD Movistar+ limpiada.", "success");
if (typeof updateMovistarVodCacheStatsUI === 'function') {
updateMovistarVodCacheStatsUI();
}
} else {
throw new Error("Función de limpieza de caché no encontrada.");
}
} catch (error) {
logToMovistarSettingsUI(`Error limpiando caché VOD: ${error.message}`, "error");
showNotification(`Error limpiando caché VOD: ${error.message}`, "error");
} finally {
showLoading(false);
}
}
function renderCurrentView() {
const mainContentEl = $('#main-content');
if (mainContentEl.length) mainContentEl.scrollTop(0);
$('#xtreamBackButton').toggle(navigationHistory.length > 0);
if (currentView.type === 'main') {
filterAndRenderChannels();
} else if (currentView.type === 'season_list' || currentView.type === 'episode_list') {
renderXtreamContent(currentView.data, currentView.title);
}
}
function pushNavigationState() {
navigationHistory.push(JSON.parse(JSON.stringify(currentView)));
}
function popNavigationState() {
if (navigationHistory.length > 0) {
currentView = navigationHistory.pop();
renderCurrentView();
}
}
function displayXtreamInfoBar(data) {
const infoBar = $('#xtream-info-bar');
if (!data || !data.user_info || !data.server_info) {
infoBar.hide();
return;
}
const userInfo = data.user_info;
const serverInfo = data.server_info;
let expDate = 'Permanente';
if (userInfo.exp_date && userInfo.exp_date !== 'null') {
expDate = new Date(parseInt(userInfo.exp_date, 10) * 1000).toLocaleDateString();
}
const html = `
<span title="Usuario"><i class="fas fa-user"></i> ${escapeHtml(userInfo.username)}</span>
<span title="Estado"><i class="fas fa-check-circle"></i> ${escapeHtml(userInfo.status)}</span>
<span title="Expira"><i class="fas fa-calendar-alt"></i> ${escapeHtml(expDate)}</span>
<span title="Conexiones Activas/Máximas"><i class="fas fa-network-wired"></i> ${escapeHtml(userInfo.active_cons)} / ${escapeHtml(userInfo.max_connections)}</span>
<span title="Servidor"><i class="fas fa-server"></i> ${escapeHtml(serverInfo.url)}:${escapeHtml(serverInfo.port)}</span>
`;
infoBar.html(html).show();
}
function hideXtreamInfoBar() {
$('#xtream-info-bar').hide().empty();
}
function setActivePlayer(id) {
if (activePlayerId === id) {
const instanceToToggle = playerInstances[id];
if (instanceToToggle && instanceToToggle.container.style.display === 'none') {
instanceToToggle.container.style.display = 'flex';
highestZIndex++;
instanceToToggle.container.style.zIndex = highestZIndex;
}
return;
}
activePlayerId = id;
Object.keys(playerInstances).forEach(instanceId => {
const instance = playerInstances[instanceId];
if (!instance) return;
const isNowActive = instanceId === activePlayerId;
const taskbarItem = document.getElementById(`taskbar-item-${instanceId}`);
if (isNowActive) {
highestZIndex++;
instance.container.style.zIndex = highestZIndex;
instance.container.classList.add('active');
if (instance.container.style.display === 'none') {
instance.container.style.display = 'flex';
}
} else {
instance.container.classList.remove('active');
}
if (instance.videoElement) {
instance.videoElement.muted = !isNowActive;
}
if (taskbarItem) {
taskbarItem.classList.toggle('active', isNowActive);
}
});
}