819 lines
38 KiB
JavaScript
819 lines
38 KiB
JavaScript
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 = `
|
|
<div class="form-check">
|
|
<input class="form-check-input xcodec-group-checkbox" type="checkbox" value="${escapeHtml(groupName)}" id="groupCheck_${escapeHtml(groupName.replace(/\s+/g, ''))}" checked>
|
|
<label class="form-check-label" for="groupCheck_${escapeHtml(groupName.replace(/\s+/g, ''))}">
|
|
${escapeHtml(groupName)}
|
|
</label>
|
|
</div>
|
|
<span class="badge bg-secondary rounded-pill">${groups[groupName].length}</span>
|
|
`;
|
|
xcodecUi.previewGroupList.appendChild(groupItem);
|
|
});
|
|
|
|
xcodecUi.previewChannelList.innerHTML = '<li class="list-group-item text-secondary text-center">Selecciona un grupo para ver los canales.</li>';
|
|
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 = '<li class="list-group-item text-secondary text-center">No hay canales en este grupo.</li>';
|
|
return;
|
|
}
|
|
streamsInGroup.forEach(stream => {
|
|
const channelItem = document.createElement('li');
|
|
channelItem.className = 'list-group-item';
|
|
channelItem.innerHTML = `
|
|
<div class="form-check">
|
|
<input class="form-check-input xcodec-channel-checkbox" type="checkbox" value="${escapeHtml(stream.url)}" id="channelCheck_${escapeHtml(stream['tvg-id'].replace(/[^a-zA-Z0-9]/g, ''))}" checked data-group="${escapeHtml(groupName)}">
|
|
<label class="form-check-label" for="channelCheck_${escapeHtml(stream['tvg-id'].replace(/[^a-zA-Z0-9]/g, ''))}" title="${escapeHtml(stream.name)} - ${escapeHtml(stream.url)}">
|
|
${escapeHtml(stream.name)}
|
|
</label>
|
|
</div>
|
|
`;
|
|
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 = '<li class="list-group-item text-secondary text-center">No hay paneles guardados.</li>';
|
|
} 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 = `
|
|
<div style="flex-grow: 1; margin-right: 0.5rem; overflow: hidden;">
|
|
<strong title="${escapeHtml(panelDisplayName)}">${escapeHtml(panelDisplayName)}</strong>
|
|
<small class="text-secondary d-block" style="font-size:0.75rem;">${escapeHtml(panel.serverUrl)}</small>
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-secondary load-xcodec-panel-btn" data-id="${panel.id}" title="Cargar en formulario"><i class="fas fa-edit"></i></button>
|
|
<button class="btn btn-outline-primary process-xcodec-panel-direct-btn" data-id="${panel.id}" title="Procesar y Añadir Todo Directamente"><i class="fas fa-bolt"></i></button>
|
|
<button class="btn btn-outline-danger delete-xcodec-panel-btn" data-id="${panel.id}" title="Eliminar panel"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
`;
|
|
xcodecUi.savedPanelsList.appendChild(item);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
showNotification(`Error cargando paneles XCodec: ${error.message}`, 'error');
|
|
xcodecUi.savedPanelsList.innerHTML = '<li class="list-group-item text-danger text-center">Error al cargar paneles.</li>';
|
|
} 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);
|
|
}
|
|
} |