const PRESET_XCODEC_PANELS = [ { name: "Orange", serverUrl: "http://213.220.3.165/", apiToken: "iM4iIpjCWwNiOoL4EPEZV1xD" }, ]; let xcodecUi = { manageModal: null, panelNameInput: null, panelServerUrlInput: null, panelApiTokenInput: null, editingPanelIdInput: null, savePanelBtn: null, clearFormBtn: null, processPanelBtn: null, processAllPanelsBtn: null, importPresetBtn: null, savedPanelsList: null, status: null, progressContainer: null, progressBar: null, previewModal: null, previewModalLabel: null, previewStats: null, previewGroupList: null, previewChannelList: null, previewSelectAllGroupsBtn: null, previewSelectAllChannelsInGroupBtn: null, addSelectedBtn: null, addAllValidBtn: null }; let xcodecTotalApiCallsExpected = 0; let xcodecApiCallsCompleted = 0; let currentXCodecPanelDataForPreview = null; let xcodecProcessedStreamsForPreview = []; function initXCodecPanelManagement() { xcodecUi.manageModal = document.getElementById('manageXCodecPanelsModal'); xcodecUi.panelNameInput = document.getElementById('xcodecPanelNameInput'); xcodecUi.panelServerUrlInput = document.getElementById('xcodecPanelServerUrlInput'); xcodecUi.panelApiTokenInput = document.getElementById('xcodecPanelApiTokenInput'); xcodecUi.editingPanelIdInput = document.getElementById('xcodecEditingPanelIdInput'); xcodecUi.savePanelBtn = document.getElementById('xcodecSavePanelBtn'); xcodecUi.clearFormBtn = document.getElementById('xcodecClearFormBtn'); xcodecUi.processPanelBtn = document.getElementById('xcodecProcessPanelBtn'); xcodecUi.processAllPanelsBtn = document.getElementById('xcodecProcessAllPanelsBtn'); xcodecUi.importPresetBtn = document.getElementById('xcodecImportPresetPanelsBtn'); xcodecUi.savedPanelsList = document.getElementById('savedXCodecPanelsList'); xcodecUi.status = document.getElementById('xcodecStatus'); xcodecUi.progressContainer = document.getElementById('xcodecProgressContainer'); xcodecUi.progressBar = document.getElementById('xcodecProgressBar'); xcodecUi.previewModal = document.getElementById('xcodecPreviewModal'); xcodecUi.previewModalLabel = document.getElementById('xcodecPreviewModalLabel'); xcodecUi.previewStats = document.getElementById('xcodecPreviewStats'); xcodecUi.previewGroupList = document.getElementById('xcodecPreviewGroupList'); xcodecUi.previewChannelList = document.getElementById('xcodecPreviewChannelList'); xcodecUi.previewSelectAllGroupsBtn = document.getElementById('xcodecPreviewSelectAllGroupsBtn'); xcodecUi.previewSelectAllChannelsInGroupBtn = document.getElementById('xcodecPreviewSelectAllChannelsInGroupBtn'); xcodecUi.addSelectedBtn = document.getElementById('xcodecAddSelectedBtn'); xcodecUi.addAllValidBtn = document.getElementById('xcodecAddAllValidBtn'); if (xcodecUi.savePanelBtn) xcodecUi.savePanelBtn.addEventListener('click', handleSaveXCodecPanel); if (xcodecUi.clearFormBtn) xcodecUi.clearFormBtn.addEventListener('click', clearXCodecPanelForm); if (xcodecUi.processPanelBtn) xcodecUi.processPanelBtn.addEventListener('click', () => processPanelFromForm(false)); if (xcodecUi.processAllPanelsBtn) xcodecUi.processAllPanelsBtn.addEventListener('click', processAllSavedXCodecPanels); if (xcodecUi.importPresetBtn) xcodecUi.importPresetBtn.addEventListener('click', importPresetXCodecPanels); if (xcodecUi.previewSelectAllGroupsBtn) xcodecUi.previewSelectAllGroupsBtn.addEventListener('click', toggleAllGroupsInPreview); if (xcodecUi.previewSelectAllChannelsInGroupBtn) xcodecUi.previewSelectAllChannelsInGroupBtn.addEventListener('click', toggleAllChannelsInCurrentPreviewGroup); if (xcodecUi.addSelectedBtn) xcodecUi.addSelectedBtn.addEventListener('click', addSelectedXCodecStreamsToM3U); if (xcodecUi.addAllValidBtn) xcodecUi.addAllValidBtn.addEventListener('click', addAllValidXCodecStreamsToM3U); if (xcodecUi.savedPanelsList) { xcodecUi.savedPanelsList.addEventListener('click', (event) => { const target = event.target.closest('button'); if (!target) return; const panelId = parseInt(target.dataset.id, 10); if (target.classList.contains('load-xcodec-panel-btn')) { loadXCodecPanelToForm(panelId); } else if (target.classList.contains('delete-xcodec-panel-btn')) { handleDeleteXCodecPanel(panelId); } else if (target.classList.contains('process-xcodec-panel-direct-btn')) { loadXCodecPanelToForm(panelId).then(() => processPanelFromForm(true)); } }); } if (xcodecUi.previewGroupList) { xcodecUi.previewGroupList.addEventListener('click', (event) => { const groupItem = event.target.closest('.list-group-item'); if (groupItem && groupItem.dataset.groupName) { const groupName = groupItem.dataset.groupName; renderXCodecPreviewChannels(groupName); xcodecUi.previewGroupList.querySelectorAll('.list-group-item').forEach(item => item.classList.remove('active')); groupItem.classList.add('active'); xcodecUi.previewSelectAllChannelsInGroupBtn.disabled = false; } }); } loadSavedXCodecPanels(); } function xcodecUpdateStatus(message, type = 'info', modal = 'manage') { const statusEl = modal === 'manage' ? xcodecUi.status : xcodecUi.previewStats; if (!statusEl) return; statusEl.textContent = message; statusEl.className = 'alert mt-2'; statusEl.style.display = 'block'; if (type) statusEl.classList.add(`alert-${type}`); } function xcodecResetProgress(expectedCalls = 0) { if (!xcodecUi) return; xcodecApiCallsCompleted = 0; xcodecTotalApiCallsExpected = expectedCalls; xcodecUi.progressBar.style.width = '0%'; xcodecUi.progressBar.textContent = '0%'; xcodecUi.progressContainer.style.display = expectedCalls > 0 ? 'block' : 'none'; } function xcodecUpdateProgress() { if (!xcodecUi || xcodecTotalApiCallsExpected === 0) return; xcodecApiCallsCompleted++; const percentage = Math.min(100, Math.max(0, (xcodecApiCallsCompleted / xcodecTotalApiCallsExpected) * 100)); xcodecUi.progressBar.style.width = percentage + '%'; xcodecUi.progressBar.textContent = Math.round(percentage) + '%'; if (percentage >= 100 && xcodecUi.progressContainer) { setTimeout(() => { if (xcodecUi.progressContainer) xcodecUi.progressContainer.style.display = 'none'; }, 1500); } } function xcodecSetControlsDisabled(disabled, modal = 'manage') { if (!xcodecUi) return; if (modal === 'manage') { xcodecUi.processPanelBtn.disabled = disabled; if (xcodecUi.processAllPanelsBtn) xcodecUi.processAllPanelsBtn.disabled = disabled; xcodecUi.panelServerUrlInput.disabled = disabled; xcodecUi.panelApiTokenInput.disabled = disabled; xcodecUi.savePanelBtn.disabled = disabled; xcodecUi.clearFormBtn.disabled = disabled; xcodecUi.importPresetBtn.disabled = disabled; const processBtnIcon = xcodecUi.processPanelBtn.querySelector('i'); if (processBtnIcon) processBtnIcon.className = disabled ? 'fas fa-spinner fa-spin me-1' : 'fas fa-cogs me-1'; const processAllBtnIcon = xcodecUi.processAllPanelsBtn ? xcodecUi.processAllPanelsBtn.querySelector('i') : null; if (processAllBtnIcon) processAllBtnIcon.className = disabled ? 'fas fa-spinner fa-spin me-1' : 'fas fa-tasks me-1'; } else if (modal === 'preview') { xcodecUi.addSelectedBtn.disabled = disabled; xcodecUi.addAllValidBtn.disabled = disabled; } } function xcodecCleanUrl(url) { try { const urlObj = new URL(url); urlObj.searchParams.delete('decryption_key'); return urlObj.toString(); } catch (e) { return url.replace(/[?&]decryption_key=[^&]+/gi, ''); } } function getXCodecProxiedApiEndpoint(targetServerBaseUrl, apiPath) { let base = targetServerBaseUrl.trim(); if (!base.endsWith('/')) base += '/'; let path = apiPath.trim(); if (path.startsWith('/')) path = path.substring(1); const proxy = userSettings.xcodecCorsProxyUrl ? userSettings.xcodecCorsProxyUrl.trim() : ''; if (proxy) { return proxy + base + path; } return base + path; } async function fetchXCodecWithTimeout(resource, options = {}, timeout) { const effectiveTimeout = timeout || userSettings.xcodecDefaultTimeout || 8000; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), effectiveTimeout); const response = await fetch(resource, { ...options, signal: controller.signal }); clearTimeout(id); return response; } async function getXCodecStreamStats(targetServerUrl, apiToken) { const apiUrl = getXCodecProxiedApiEndpoint(targetServerUrl, 'api/stream/stats'); xcodecUpdateProgress(); const headers = {}; if (apiToken) headers['Authorization'] = `Token ${apiToken}`; try { const response = await fetchXCodecWithTimeout(apiUrl, { headers }); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); const stats = await response.json(); if (!Array.isArray(stats)) throw new Error("La respuesta de estadísticas no es un array."); return stats; } catch (error) { throw error; } } async function processXCodecStreamConfig(targetServerUrl, apiToken, streamId, streamNameFallback, serverHostForGroupTitle) { const apiUrl = getXCodecProxiedApiEndpoint(targetServerUrl, `api/stream/${streamId}/config`); const headers = {}; if (apiToken) headers['Authorization'] = `Token ${apiToken}`; const DEFAULT_KID_FOR_JSON_SINGLE_KEY = "00000000000000000000000000000000"; try { const response = await fetchXCodecWithTimeout(apiUrl, { headers }); if (!response.ok) throw new Error(`HTTP ${response.status} para config ${streamId}`); const config = await response.json(); const streamName = config?.name || streamNameFallback || `Stream ${streamId}`; if (!config?.input_urls?.length) { return { error: `Stream ${streamId} (${streamName}) sin input_urls.` }; } let kodiProps = { 'inputstreamaddon': 'inputstream.adaptive', 'inputstream.adaptive.manifest_type': 'mpd' }; let vlcOpts = {}; const urlWithKey = config.input_urls.find(u => /[?&]decryption_key=([^&]+)/i.test(u)); if (urlWithKey) { const keyMatch = urlWithKey.match(/[?&]decryption_key=([^&]+)/i); if (keyMatch && keyMatch[1]) { const allKeyEntriesString = keyMatch[1]; const keyEntriesArray = allKeyEntriesString.split(','); let licenseKeyStringForKodi = ''; if (keyEntriesArray.length === 1) { const singleEntry = keyEntriesArray[0].trim(); if (singleEntry.indexOf(':') === -1 && singleEntry.length === 32 && /^[0-9a-fA-F]{32}$/.test(singleEntry)) { licenseKeyStringForKodi = singleEntry; } } if (!licenseKeyStringForKodi) { const licenseKeysObject = {}; let foundValidKeysForJson = false; for (const entryStr of keyEntriesArray) { const trimmedEntry = entryStr.trim(); if (!trimmedEntry) continue; const parts = trimmedEntry.split(':'); if (parts.length === 2 && parts[0].trim() && parts[1].trim()) { const kid = parts[0].trim(); const key = parts[1].trim(); if (kid.length === 32 && key.length === 32 && /^[0-9a-fA-F]+$/.test(kid) && /^[0-9a-fA-F]+$/.test(key)) { licenseKeysObject[kid] = key; foundValidKeysForJson = true; } } else if (parts.length === 1 && parts[0].trim()) { const potentialKey = parts[0].trim(); if (potentialKey.length === 32 && /^[0-9a-fA-F]{32}$/.test(potentialKey)) { licenseKeysObject[DEFAULT_KID_FOR_JSON_SINGLE_KEY] = potentialKey; foundValidKeysForJson = true; } } } if (foundValidKeysForJson) { licenseKeyStringForKodi = JSON.stringify(licenseKeysObject); } } if (licenseKeyStringForKodi) { kodiProps['inputstream.adaptive.license_type'] = 'clearkey'; kodiProps['inputstream.adaptive.license_key'] = licenseKeyStringForKodi; } } } if (config.headers) { try { const formattedHeaders = config.headers.split('&').map(p => { const eq = p.indexOf('='); return eq > -1 ? `${p.substring(0, eq).trim()}=${encodeURIComponent(p.substring(eq + 1).trim())}` : p.trim(); }).join('&'); kodiProps['inputstream.adaptive.stream_headers'] = formattedHeaders; } catch (e) { } } return { name: streamName, url: xcodecCleanUrl(config.input_urls[0]), 'tvg-id': config.epg_id || `xcodec.${streamId}`, 'tvg-logo': config.logo || '', 'group-title': config.category_name || serverHostForGroupTitle || 'XCodec Streams', attributes: { duration: -1 }, kodiProps: kodiProps, vlcOptions: vlcOpts, sourceOrigin: `XCodec: ${serverHostForGroupTitle}` }; } catch (error) { return { error: `Fallo config Stream ${streamId} de ${targetServerUrl}: ${error.message}` }; } } async function processSingleXCodecPanelLogic(panelData, directAdd, isPartOfBatchOperation) { let serverHostForGroupTitle; try { const urlObj = new URL(panelData.serverUrl); serverHostForGroupTitle = panelData.name || urlObj.hostname; } catch(e) { serverHostForGroupTitle = panelData.name || panelData.serverUrl; } const serverBaseUrl = panelData.serverUrl.endsWith('/') ? panelData.serverUrl : panelData.serverUrl + '/'; if (!isPartOfBatchOperation) { xcodecUpdateStatus(`Iniciando panel: ${escapeHtml(panelData.name || panelData.serverUrl)}...`, 'info', 'manage'); } xcodecResetProgress(1); let streamStats; try { streamStats = await getXCodecStreamStats(serverBaseUrl, panelData.apiToken); } catch (error) { const errorMsg = `Error obteniendo estadísticas de ${serverHostForGroupTitle}: ${error.message}`; if (!isPartOfBatchOperation) { xcodecUpdateStatus(errorMsg, 'danger', 'manage'); xcodecSetControlsDisabled(false, 'manage'); xcodecResetProgress(); } return { success: false, name: serverHostForGroupTitle, error: errorMsg, added: 0, errors: 1 }; } if (!streamStats) { if (!isPartOfBatchOperation) { xcodecUpdateStatus(`No se obtuvieron estadísticas de ${serverHostForGroupTitle}.`, 'warning', 'manage'); xcodecSetControlsDisabled(false, 'manage'); xcodecResetProgress(); } return { success: false, name: serverHostForGroupTitle, error: "No stats returned", added: 0, errors: 1 }; } if (streamStats.length === 0) { if (!isPartOfBatchOperation) { xcodecUpdateStatus(`No se encontraron streams activos en ${serverHostForGroupTitle}.`, 'info', 'manage'); xcodecSetControlsDisabled(false, 'manage'); xcodecResetProgress(); } return { success: true, name: serverHostForGroupTitle, added: 0, errors: 0, message: "No active streams" }; } if (directAdd && userSettings.xcodecIgnorePanelsOverStreams > 0 && streamStats.length > userSettings.xcodecIgnorePanelsOverStreams) { const ignoreMsg = `Panel ${serverHostForGroupTitle} ignorado: ${streamStats.length} streams (límite ${userSettings.xcodecIgnorePanelsOverStreams}).`; if (!isPartOfBatchOperation) { xcodecUpdateStatus(ignoreMsg, 'warning', 'manage'); xcodecSetControlsDisabled(false, 'manage'); xcodecResetProgress(); } return { success: true, name: serverHostForGroupTitle, added: 0, errors: 0, message: ignoreMsg }; } xcodecTotalApiCallsExpected = 1 + streamStats.length; if (!isPartOfBatchOperation) { xcodecUi.progressBar.textContent = Math.round((1 / xcodecTotalApiCallsExpected) * 100) + '%'; } if (!isPartOfBatchOperation) { xcodecUpdateStatus(`Procesando ${streamStats.length} streams de ${serverHostForGroupTitle}...`, 'info', 'manage'); } if (streamStats.length > 0) xcodecUi.progressContainer.style.display = 'block'; const batchSize = userSettings.xcodecDefaultBatchSize || 15; let processedStreams = []; let streamsWithErrors = 0; for (let j = 0; j < streamStats.length; j += batchSize) { const batch = streamStats.slice(j, j + batchSize); const configPromises = batch.map(s => processXCodecStreamConfig(serverBaseUrl, panelData.apiToken, s.id, s.name, serverHostForGroupTitle) .finally(() => xcodecUpdateProgress()) ); const batchResults = await Promise.allSettled(configPromises); batchResults.forEach(r => { if (r.status === 'fulfilled' && r.value && !r.value.error) { processedStreams.push(r.value); } else { streamsWithErrors++; } }); } currentXCodecPanelDataForPreview = panelData; xcodecProcessedStreamsForPreview = processedStreams; if (directAdd) { if (processedStreams.length > 0) { const m3uString = streamsToM3U(processedStreams, serverHostForGroupTitle); const sourceName = `XCodec: ${serverHostForGroupTitle}`; if (typeof removeChannelsBySourceOrigin === 'function') removeChannelsBySourceOrigin(sourceName); appendM3UContent(m3uString, sourceName); if (!isPartOfBatchOperation) { showNotification(`${processedStreams.length} canales de "${escapeHtml(serverHostForGroupTitle)}" añadidos/actualizados.`, 'success'); } } if (!isPartOfBatchOperation) { xcodecUpdateStatus(`Proceso completado. Streams OK: ${processedStreams.length}. Errores: ${streamsWithErrors}.`, 'success', 'manage'); const manageModalInstance = bootstrap.Modal.getInstance(xcodecUi.manageModal); if (manageModalInstance) manageModalInstance.hide(); } } else { if (!isPartOfBatchOperation) { openXCodecPreviewModal(serverHostForGroupTitle, processedStreams.length, streamsWithErrors); } } if (!isPartOfBatchOperation) { xcodecSetControlsDisabled(false, 'manage'); } return { success: true, name: serverHostForGroupTitle, added: processedStreams.length, errors: streamsWithErrors }; } async function processPanelFromForm(directAdd = false) { if (!xcodecUi) return; const panelData = { id: xcodecUi.editingPanelIdInput.value ? parseInt(xcodecUi.editingPanelIdInput.value, 10) : null, name: xcodecUi.panelNameInput.value.trim(), serverUrl: xcodecUi.panelServerUrlInput.value.trim(), apiToken: xcodecUi.panelApiTokenInput.value.trim() }; if (!panelData.serverUrl) { xcodecUpdateStatus('Por favor, introduce la URL del servidor X-UI/XC.', 'warning', 'manage'); return; } try { new URL(panelData.serverUrl); } catch(e){ xcodecUpdateStatus('La URL del servidor no es válida.', 'warning', 'manage'); return; } if (!panelData.name) panelData.name = new URL(panelData.serverUrl).hostname; xcodecSetControlsDisabled(true, 'manage'); try { await processSingleXCodecPanelLogic(panelData, directAdd, false); } catch (error) { xcodecUpdateStatus(`Error procesando el panel ${escapeHtml(panelData.name)}: ${error.message}`, 'danger', 'manage'); xcodecSetControlsDisabled(false, 'manage'); xcodecResetProgress(); } } async function processAllSavedXCodecPanels() { if (!xcodecUi) return; const userConfirmed = await showConfirmationModal( "Esto procesará TODOS los paneles XCodec guardados y añadirá sus streams directamente a la lista M3U actual. Esta operación puede tardar y añadir muchos canales. ¿Continuar?", "Confirmar Procesamiento Masivo de Paneles", "Sí, Procesar Todos", "btn-primary" ); if (!userConfirmed) { xcodecUpdateStatus('Procesamiento masivo cancelado por el usuario.', 'info', 'manage'); return; } xcodecSetControlsDisabled(true, 'manage'); xcodecUpdateStatus('Iniciando procesamiento de todos los paneles guardados...', 'info', 'manage'); let savedPanels; try { savedPanels = await getAllXCodecPanelsFromDB(); } catch (error) { xcodecUpdateStatus(`Error al obtener paneles guardados: ${error.message}`, 'danger', 'manage'); xcodecSetControlsDisabled(false, 'manage'); return; } if (!savedPanels || savedPanels.length === 0) { xcodecUpdateStatus('No hay paneles guardados para procesar.', 'info', 'manage'); xcodecSetControlsDisabled(false, 'manage'); return; } let totalPanels = savedPanels.length; let panelsProcessedCount = 0; let totalStreamsAdded = 0; let totalErrorsAcrossPanels = 0; let panelsWithErrorsCount = 0; xcodecUi.progressContainer.style.display = 'block'; for (const panel of savedPanels) { panelsProcessedCount++; const panelDisplayName = panel.name || panel.serverUrl; const overallPercentage = (panelsProcessedCount / totalPanels) * 100; xcodecUi.progressBar.style.width = overallPercentage + '%'; xcodecUi.progressBar.textContent = `Panel ${panelsProcessedCount}/${totalPanels}`; xcodecUpdateStatus(`Procesando panel ${panelsProcessedCount} de ${totalPanels}: "${escapeHtml(panelDisplayName)}"`, 'info', 'manage'); try { const result = await processSingleXCodecPanelLogic(panel, true, true); if (result) { totalStreamsAdded += result.added || 0; if (!result.success || (result.errors || 0) > 0) { panelsWithErrorsCount++; totalErrorsAcrossPanels += result.errors || 0; } } } catch (error) { xcodecUpdateStatus(`Error crítico procesando panel "${escapeHtml(panelDisplayName)}": ${error.message}. Saltando al siguiente.`, 'warning', 'manage'); panelsWithErrorsCount++; totalErrorsAcrossPanels++; } } xcodecUi.progressBar.style.width = '100%'; xcodecUi.progressBar.textContent = `Completado ${totalPanels}/${totalPanels}`; setTimeout(() => { if (xcodecUi.progressContainer) xcodecUi.progressContainer.style.display = 'none'; xcodecUi.progressBar.style.width = '0%'; xcodecUi.progressBar.textContent = '0%'; }, 3000); const summaryMessage = `Procesamiento masivo completado. ${panelsProcessedCount} paneles procesados. Total streams añadidos: ${totalStreamsAdded}. Paneles con errores: ${panelsWithErrorsCount}. Total errores individuales: ${totalErrorsAcrossPanels}.`; xcodecUpdateStatus(summaryMessage, 'success', 'manage'); showNotification(summaryMessage, 'success', 10000); xcodecSetControlsDisabled(false, 'manage'); } function streamsToM3U(streamsArray, panelName) { let m3u = '#EXTM3U\n'; m3u += `# ----- Inicio Panel: ${panelName} -----\n\n`; streamsArray.forEach(ch => { m3u += `#EXTINF:-1 tvg-id="${ch['tvg-id']}" tvg-logo="${ch['tvg-logo']}" group-title="${ch['group-title']}",${ch.name}\n`; if (ch.kodiProps) { for (const key in ch.kodiProps) { m3u += `#KODIPROP:${key}=${ch.kodiProps[key]}\n`; } } m3u += `${ch.url}\n\n`; }); m3u += `# ----- Fin Panel: ${panelName} -----\n\n`; return m3u; } function openXCodecPreviewModal(panelName, validCount, errorCount) { xcodecUi.previewModalLabel.textContent = `Previsualización Panel: ${escapeHtml(panelName)}`; xcodecUpdateStatus(`Streams válidos: ${validCount}. Con errores: ${errorCount}.`, 'info', 'preview'); const groups = {}; xcodecProcessedStreamsForPreview.forEach(stream => { const group = stream['group-title'] || 'Sin Grupo'; if (!groups[group]) groups[group] = []; groups[group].push(stream); }); xcodecUi.previewGroupList.innerHTML = ''; const sortedGroupNames = Object.keys(groups).sort((a, b) => a.localeCompare(b)); sortedGroupNames.forEach(groupName => { const groupItem = document.createElement('li'); groupItem.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center'; groupItem.dataset.groupName = groupName; groupItem.style.cursor = 'pointer'; groupItem.innerHTML = `
${groups[groupName].length} `; xcodecUi.previewGroupList.appendChild(groupItem); }); xcodecUi.previewChannelList.innerHTML = '
  • Selecciona un grupo para ver los canales.
  • '; xcodecUi.addSelectedBtn.disabled = false; xcodecUi.addAllValidBtn.disabled = validCount === 0; xcodecUi.previewSelectAllChannelsInGroupBtn.disabled = true; const previewModalInstance = bootstrap.Modal.getOrCreateInstance(xcodecUi.previewModal); previewModalInstance.show(); const manageModalInstance = bootstrap.Modal.getInstance(xcodecUi.manageModal); if (manageModalInstance) manageModalInstance.hide(); } function renderXCodecPreviewChannels(groupName) { xcodecUi.previewChannelList.innerHTML = ''; const streamsInGroup = xcodecProcessedStreamsForPreview.filter(s => (s['group-title'] || 'Sin Grupo') === groupName); if (streamsInGroup.length === 0) { xcodecUi.previewChannelList.innerHTML = '
  • No hay canales en este grupo.
  • '; return; } streamsInGroup.forEach(stream => { const channelItem = document.createElement('li'); channelItem.className = 'list-group-item'; channelItem.innerHTML = `
    `; xcodecUi.previewChannelList.appendChild(channelItem); }); } function toggleAllGroupsInPreview() { const firstCheckbox = xcodecUi.previewGroupList.querySelector('.xcodec-group-checkbox'); if (!firstCheckbox) return; const currentlyChecked = firstCheckbox.checked; xcodecUi.previewGroupList.querySelectorAll('.xcodec-group-checkbox').forEach(cb => cb.checked = !currentlyChecked); xcodecUi.previewChannelList.querySelectorAll('.xcodec-channel-checkbox').forEach(cb => cb.checked = !currentlyChecked); } function toggleAllChannelsInCurrentPreviewGroup() { const activeGroupItem = xcodecUi.previewGroupList.querySelector('.list-group-item.active'); if (!activeGroupItem) return; const groupName = activeGroupItem.dataset.groupName; const firstChannelCheckboxInGroup = xcodecUi.previewChannelList.querySelector(`.xcodec-channel-checkbox[data-group="${escapeHtml(groupName)}"]`); if (!firstChannelCheckboxInGroup) return; const currentlyChecked = firstChannelCheckboxInGroup.checked; xcodecUi.previewChannelList.querySelectorAll(`.xcodec-channel-checkbox[data-group="${escapeHtml(groupName)}"]`).forEach(cb => { cb.checked = !currentlyChecked; }); } function addSelectedXCodecStreamsToM3U() { const selectedStreams = []; const selectedGroupCheckboxes = xcodecUi.previewGroupList.querySelectorAll('.xcodec-group-checkbox:checked'); const selectedGroups = Array.from(selectedGroupCheckboxes).map(cb => cb.value); xcodecUi.previewChannelList.querySelectorAll('.xcodec-channel-checkbox:checked').forEach(cb => { const streamUrl = cb.value; const streamGroup = cb.dataset.group; if (selectedGroups.includes(streamGroup)) { const streamData = xcodecProcessedStreamsForPreview.find(s => s.url === streamUrl && (s['group-title'] || 'Sin Grupo') === streamGroup); if (streamData) selectedStreams.push(streamData); } }); if (selectedStreams.length > 0) { const panelName = currentXCodecPanelDataForPreview.name || new URL(currentXCodecPanelDataForPreview.serverUrl).hostname; const m3uString = streamsToM3U(selectedStreams, panelName); const sourceName = `XCodec: ${panelName}`; if (typeof removeChannelsBySourceOrigin === 'function') removeChannelsBySourceOrigin(sourceName); appendM3UContent(m3uString, sourceName); showNotification(`${selectedStreams.length} canales de "${escapeHtml(panelName)}" seleccionados y añadidos.`, 'success'); } else { showNotification('No se seleccionaron canales para añadir.', 'info'); } const previewModalInstance = bootstrap.Modal.getInstance(xcodecUi.previewModal); if (previewModalInstance) previewModalInstance.hide(); } function addAllValidXCodecStreamsToM3U() { if (xcodecProcessedStreamsForPreview.length > 0) { const panelName = currentXCodecPanelDataForPreview.name || new URL(currentXCodecPanelDataForPreview.serverUrl).hostname; const m3uString = streamsToM3U(xcodecProcessedStreamsForPreview, panelName); const sourceName = `XCodec: ${panelName}`; if (typeof removeChannelsBySourceOrigin === 'function') removeChannelsBySourceOrigin(sourceName); appendM3UContent(m3uString, sourceName); showNotification(`${xcodecProcessedStreamsForPreview.length} canales válidos de "${escapeHtml(panelName)}" añadidos.`, 'success'); } else { showNotification('No hay canales válidos para añadir.', 'info'); } const previewModalInstance = bootstrap.Modal.getInstance(xcodecUi.previewModal); if (previewModalInstance) previewModalInstance.hide(); } async function loadSavedXCodecPanels() { if (typeof showLoading === 'function') showLoading(true, 'Cargando paneles XCodec...'); try { const panels = typeof getAllXCodecPanelsFromDB === 'function' ? await getAllXCodecPanelsFromDB() : []; xcodecUi.savedPanelsList.innerHTML = ''; if (!panels || panels.length === 0) { xcodecUi.savedPanelsList.innerHTML = '
  • No hay paneles guardados.
  • '; } else { panels.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); panels.forEach(panel => { const panelDisplayName = panel.name || panel.serverUrl; const item = document.createElement('li'); item.className = 'list-group-item d-flex justify-content-between align-items-center'; item.innerHTML = `
    ${escapeHtml(panelDisplayName)} ${escapeHtml(panel.serverUrl)}
    `; xcodecUi.savedPanelsList.appendChild(item); }); } } catch (error) { showNotification(`Error cargando paneles XCodec: ${error.message}`, 'error'); xcodecUi.savedPanelsList.innerHTML = '
  • Error al cargar paneles.
  • '; } finally { if (typeof showLoading === 'function') showLoading(false); } } async function handleSaveXCodecPanel() { const panelData = { id: xcodecUi.editingPanelIdInput.value ? parseInt(xcodecUi.editingPanelIdInput.value, 10) : null, name: xcodecUi.panelNameInput.value.trim(), serverUrl: xcodecUi.panelServerUrlInput.value.trim(), apiToken: xcodecUi.panelApiTokenInput.value.trim() }; if (!panelData.serverUrl) { showNotification('La URL del Servidor es obligatoria.', 'warning'); return; } try { new URL(panelData.serverUrl); } catch(e){ showNotification('La URL del servidor no es válida.', 'warning'); return; } if (!panelData.name) panelData.name = new URL(panelData.serverUrl).hostname; if (typeof showLoading === 'function') showLoading(true, `Guardando panel XCodec: ${escapeHtml(panelData.name)}...`); try { await saveXCodecPanelToDB(panelData); showNotification(`Panel XCodec "${escapeHtml(panelData.name)}" guardado.`, 'success'); loadSavedXCodecPanels(); clearXCodecPanelForm(); } catch (error) { showNotification(`Error al guardar panel: ${error.message}`, 'error'); } finally { if (typeof showLoading === 'function') showLoading(false); } } function clearXCodecPanelForm() { xcodecUi.editingPanelIdInput.value = ''; xcodecUi.panelNameInput.value = ''; xcodecUi.panelServerUrlInput.value = ''; xcodecUi.panelApiTokenInput.value = ''; xcodecUi.panelNameInput.focus(); } async function loadXCodecPanelToForm(id) { if (typeof showLoading === 'function') showLoading(true, "Cargando datos del panel..."); try { const panel = await getXCodecPanelFromDB(id); if (panel) { xcodecUi.editingPanelIdInput.value = panel.id; xcodecUi.panelNameInput.value = panel.name || ''; xcodecUi.panelServerUrlInput.value = panel.serverUrl || ''; xcodecUi.panelApiTokenInput.value = panel.apiToken || ''; showNotification(`Datos del panel "${escapeHtml(panel.name || panel.serverUrl)}" cargados.`, 'info'); } else { showNotification('Panel no encontrado.', 'error'); } } catch (error) { showNotification(`Error al cargar panel: ${error.message}`, 'error'); } finally { if (typeof showLoading === 'function') showLoading(false); } } async function handleDeleteXCodecPanel(id) { const panelToDelete = await getXCodecPanelFromDB(id); const panelName = panelToDelete ? (panelToDelete.name || panelToDelete.serverUrl) : 'este panel'; if (!confirm(`¿Seguro que quieres eliminar el panel XCodec "${escapeHtml(panelName)}"?`)) return; if (typeof showLoading === 'function') showLoading(true, `Eliminando panel "${escapeHtml(panelName)}"...`); try { await deleteXCodecPanelFromDB(id); showNotification(`Panel XCodec "${escapeHtml(panelName)}" eliminado.`, 'success'); loadSavedXCodecPanels(); if (xcodecUi.editingPanelIdInput.value === String(id)) { clearXCodecPanelForm(); } } catch (error) { showNotification(`Error al eliminar panel: ${error.message}`, 'error'); } finally { if (typeof showLoading === 'function') showLoading(false); } } async function importPresetXCodecPanels() { if (!confirm(`¿Quieres importar ${PRESET_XCODEC_PANELS.length} paneles predefinidos a tu lista? Esto no sobrescribirá los existentes con la misma URL.`)) return; if (typeof showLoading === 'function') showLoading(true, "Importando paneles predefinidos..."); let importedCount = 0; let skippedCount = 0; try { const existingPanels = await getAllXCodecPanelsFromDB(); const existingUrls = new Set(existingPanels.map(p => p.serverUrl)); for (const preset of PRESET_XCODEC_PANELS) { if (!existingUrls.has(preset.serverUrl)) { await saveXCodecPanelToDB(preset); importedCount++; } else { skippedCount++; } } showNotification(`Importación completada: ${importedCount} paneles añadidos, ${skippedCount} omitidos (ya existían).`, 'success'); loadSavedXCodecPanels(); } catch (error) { showNotification(`Error importando paneles: ${error.message}`, 'error'); } finally { if (typeof showLoading === 'function') showLoading(false); } }