Advanced_DRM_Player/movistar_handler.js

648 lines
34 KiB
JavaScript

const MOVISTAR_API_BASE = 'https://auth.dof6.com';
const MOVISTAR_API_CLIENTSERVICES = 'https://clientservices.dof6.com';
const MOVISTAR_API_IDSERVER = 'https://idserver.dof6.com';
const MOVISTAR_UI_VERSION = '2.45.20';
const MOVISTAR_API_DEVICES_ENDPOINT = `${MOVISTAR_API_CLIENTSERVICES}/movistarplus/accounts/{ACCOUNTNUMBER}/devices?qspVersion=ssp`;
const MOVISTAR_API_REGISTER_DEVICE_ENDPOINT = `${MOVISTAR_API_BASE}/movistarplus/android.tv/accounts/{ACCOUNTNUMBER}/devices/?qspVersion=ssp`;
const M_SHORT_TOKEN_KEY = 'movistar_shortToken';
const M_SHORT_TOKEN_EXPIRY_KEY = 'movistar_shortTokenExpiry';
const M_LONG_TOKEN_PREFIX = 'movistar_longToken_';
const M_LAST_USED_TOKEN_ID_KEY = 'movistar_lastUsedLongTokenId';
const M_LAST_ROTATION_DATE_KEY = 'movistar_lastRotationDate';
const M_REFRESH_LONG_TOKEN_WITHIN_DAYS = 2;
let movistarLogCallback = (message, type = 'info') => { console.log(`[MovistarHandler Log|${type}]: ${message}`); };
function setMovistarLogCallback(callback) {
if (typeof callback === 'function') {
movistarLogCallback = callback;
}
}
function _log(message, type = 'info') {
movistarLogCallback(message, type);
}
function _parseJwtPayload(token) {
if (!token || typeof token !== 'string') return null;
try {
const base64Url = token.split('.')[1];
if (!base64Url) return null;
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const paddedBase64 = base64 + '==='.slice((base64.length + 3) % 4);
const jsonPayload = decodeURIComponent(atob(paddedBase64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
return JSON.parse(jsonPayload);
} catch (e) {
_log(`Error decodificando JWT: ${e.message}`, 'error');
return null;
}
}
async function _getAllLongTokensFromDB() {
_log('Obteniendo todos los tokens largos de la DB...');
if (typeof getAllAppConfigValues !== 'function') {
_log('Función getAllAppConfigValues no disponible en db_manager.js. No se pueden listar tokens largos.', 'error');
return [];
}
try {
const allConfig = await getAllAppConfigValues();
const longTokens = [];
for (const key in allConfig) {
if (key.startsWith(M_LONG_TOKEN_PREFIX) && allConfig[key] && typeof allConfig[key] === 'object') {
longTokens.push(allConfig[key]);
}
}
_log(`Se encontraron ${longTokens.length} tokens largos en la DB.`);
return longTokens;
} catch (error) {
_log(`Error cargando todos los tokens largos: ${error.message}`, 'error');
return [];
}
}
async function _saveLongTokenToDB(tokenData) {
if (!tokenData || !tokenData.id || !tokenData.id.startsWith(M_LONG_TOKEN_PREFIX)) {
_log(`Intento de guardar token largo con ID inválido o faltante: ${JSON.stringify(tokenData)}`, 'error');
throw new Error("ID de token largo inválido o faltante.");
}
_log(`Guardando token largo ID: ${tokenData.id.slice(-12)}`);
return saveAppConfigValue(tokenData.id, tokenData);
}
async function _deleteLongTokenFromDB(tokenId) {
_log(`Eliminando token largo ID: ${tokenId.slice(-12)}`);
return deleteAppConfigValue(tokenId);
}
async function _getOrCreateFunctionalDeviceId(longTokenData) {
_log("Buscando/Creando Device ID funcional...", 'info');
if (!longTokenData || !longTokenData.login_token || !longTokenData.account_nbr) {
throw new Error("Datos de token insuficientes para buscar/crear Device ID.");
}
const url = MOVISTAR_API_DEVICES_ENDPOINT.replace('{ACCOUNTNUMBER}', longTokenData.account_nbr);
const headers = {
'Authorization': `Bearer ${longTokenData.login_token}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01', 'x-movistarplus-ui': MOVISTAR_UI_VERSION,
'x-movistarplus-os': 'Windows10', 'Origin': 'https://ver.movistarplus.es', 'Referer': 'https://ver.movistarplus.es/'
};
try {
_log("Consultando /devices...", 'info');
const response = await fetch(url, { method: 'GET', headers: headers });
const responseText = await response.text();
_log(`Respuesta /devices: ${response.status}`, 'debug');
if (!response.ok) { throw new Error(`Fallo consulta /devices: ${response.status} ${responseText.substring(0,100)}`); }
const devices = JSON.parse(responseText);
if (!Array.isArray(devices)) throw new Error("Respuesta /devices no es array.");
const validDevices = devices.filter(d => d && d.Id && d.Id !== '-');
_log(`Encontrados ${validDevices.length} dispositivos válidos.`, 'info');
const preferredTypes = ["WP_DASH", "ANTV"];
for (const type of preferredTypes) {
const device = validDevices.find(d => d.DeviceTypeCode === type);
if (device) { _log(`Reutilizando device tipo ${type}: ...${device.Id.slice(-6)}`, 'info'); return device.Id; }
}
if (validDevices.length > 0) { _log(`Reutilizando primer device válido (tipo ${validDevices[0].DeviceTypeCode}): ...${validDevices[0].Id.slice(-6)}`, 'info'); return validDevices[0].Id; }
_log("No hay devices válidos, registrando nuevo...", 'info');
const registerUrl = MOVISTAR_API_REGISTER_DEVICE_ENDPOINT.replace('{ACCOUNTNUMBER}', longTokenData.account_nbr);
const registerHeaders = { ...headers, 'Content-Type': 'application/json' };
delete registerHeaders.Origin; delete registerHeaders.Referer;
const registerResponse = await fetch(registerUrl, { method: 'POST', headers: registerHeaders });
const newDeviceIdText = await registerResponse.text();
const newDeviceId = newDeviceIdText.trim().replace(/^"|"$/g, '');
_log(`Respuesta registro: ${registerResponse.status}`, 'debug');
if (!registerResponse.ok || !newDeviceId || newDeviceId.length < 10) {
let errorMsg = `Fallo registro: ${registerResponse.status}`;
if (newDeviceIdText.length < 200 && !newDeviceIdText.includes('<')) errorMsg += ` - ${newDeviceIdText}`;
if (registerResponse.status === 403 || newDeviceIdText.toLowerCase().includes('limit')) errorMsg = "Límite de dispositivos alcanzado.";
throw new Error(errorMsg);
}
_log(`Nuevo device registrado: ...${newDeviceId.slice(-6)}`, 'success');
return newDeviceId;
} catch (error) {
_log(`Error en flujo Device ID: ${error.message}`, 'error'); throw error;
}
}
async function _refreshMovistarLongToken(currentTokenData) {
_log(`Intentando renovar token largo ID: ${currentTokenData?.id?.slice(-12)}`, 'info');
if (!currentTokenData?.login_token || !currentTokenData?.account_nbr || !currentTokenData?.device_id) {
_log("Datos insuficientes para renovación.", 'error'); return null;
}
const { login_token, account_nbr, device_id } = currentTokenData;
try {
const sdpUrl = `${MOVISTAR_API_CLIENTSERVICES}/movistarplus/android.tv/sdp/mediaPlayers/${device_id}/initData?qspVersion=ssp&version=8&status=login`;
const sdpHeaders = {
'x-movistarplus-ui': MOVISTAR_UI_VERSION, 'Authorization': `Bearer ${login_token}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 'Content-Type': 'application/json', 'Accept': 'application/json, text/javascript, */*; q=0.01',
'x-movistarplus-deviceid': device_id, 'x-movistarplus-os': 'Windows10'
};
const sdpPayload = { 'accountNumber': account_nbr, 'sessionUserProfile': 0, 'streamMiscellanea': 'HTTPS', 'deviceType': 'WP_OTT', 'deviceManufacturerProduct': 'Chrome', 'streamDRM': 'Widevine', 'streamFormat': 'DASH' };
_log("Solicitando initData para refrescar token largo...", 'info');
const sdpResponse = await fetch(sdpUrl, { method: 'POST', headers: sdpHeaders, body: JSON.stringify(sdpPayload) });
const sdpRespJson = await sdpResponse.json();
if (!sdpResponse.ok || !sdpRespJson.accessToken) { throw new Error(`Fallo SDP (refresh long): ${sdpRespJson.message || sdpResponse.status}`); }
const refreshed_login_token = sdpRespJson.accessToken;
const newJwtPayload = _parseJwtPayload(refreshed_login_token);
if (!newJwtPayload || !newJwtPayload.exp) { throw new Error("Token largo refrescado inválido."); }
_log("Token largo renovado con éxito.", 'success');
return { ...currentTokenData, login_token: refreshed_login_token, expiry_tstamp: newJwtPayload.exp };
} catch (error) {
_log(`Error renovando token largo: ${error.message}`, 'error'); return null;
}
}
async function _getValidLongTokenForCdnGeneration() {
_log("Buscando token largo válido para generar CDN...", 'info');
const now = Math.floor(Date.now() / 1000);
const currentDateStr = new Date().toISOString().slice(0, 10);
const allLongTokens = await _getAllLongTokensFromDB();
const validFunctionalTokens = allLongTokens.filter(t => t.expiry_tstamp > now && t.device_id);
if (validFunctionalTokens.length === 0) {
_log("No se encontraron tokens largos válidos CON Device ID.", 'error');
return null;
}
_log(`Encontrados ${validFunctionalTokens.length} tokens largos funcionales.`, 'info');
const lastUsedId = await getAppConfigValue(M_LAST_USED_TOKEN_ID_KEY);
const lastRotationDate = await getAppConfigValue(M_LAST_ROTATION_DATE_KEY);
let selectedToken = null;
let needsRotation = false;
if (!lastRotationDate || currentDateStr > lastRotationDate || !lastUsedId) {
needsRotation = true;
_log("Necesita rotación (fecha o último ID no encontrado).", 'info');
} else {
selectedToken = validFunctionalTokens.find(t => t.id === lastUsedId);
if (!selectedToken) {
needsRotation = true;
_log("Necesita rotación (último ID usado ya no es válido/funcional).", 'info');
}
}
if (needsRotation) {
let nextTokenIndex = 0;
if (lastUsedId) {
const lastOriginalIndex = allLongTokens.findIndex(t => t.id === lastUsedId);
if (lastOriginalIndex !== -1) {
let foundNextValid = false;
for (let i = 1; i <= allLongTokens.length; i++) {
const potentialNextOriginalIndex = (lastOriginalIndex + i) % allLongTokens.length;
const potentialTokenId = allLongTokens[potentialNextOriginalIndex]?.id;
if(potentialTokenId) {
const validIndex = validFunctionalTokens.findIndex(vt => vt.id === potentialTokenId);
if (validIndex !== -1) {
nextTokenIndex = validIndex;
foundNextValid = true;
break;
}
}
}
if (!foundNextValid) nextTokenIndex = 0;
}
}
selectedToken = validFunctionalTokens[nextTokenIndex % validFunctionalTokens.length];
_log(`Token rotado a: ${selectedToken.id.slice(-12)}`, 'info');
await saveAppConfigValue(M_LAST_USED_TOKEN_ID_KEY, selectedToken.id);
await saveAppConfigValue(M_LAST_ROTATION_DATE_KEY, currentDateStr);
} else {
_log(`Reutilizando último token largo usado: ${selectedToken.id.slice(-12)}`, 'info');
}
const refreshThreshold = now + (M_REFRESH_LONG_TOKEN_WITHIN_DAYS * 24 * 60 * 60);
if (selectedToken.expiry_tstamp < refreshThreshold) {
_log(`Token ${selectedToken.id.slice(-12)} cerca de expirar, intentando refresco...`, 'info');
try {
const refreshedData = await _refreshMovistarLongToken(selectedToken);
if (refreshedData) {
_log(`Refresco de token largo ${selectedToken.id.slice(-12)} exitoso.`, 'success');
await _saveLongTokenToDB(refreshedData);
selectedToken = refreshedData;
} else {
_log(`Refresco de token largo ${selectedToken.id.slice(-12)} fallido. Usando el actual.`, 'warning');
}
} catch (refreshError) {
_log(`Error durante el refresco oportunista: ${refreshError.message}`, 'error');
}
}
return selectedToken;
}
async function doMovistarLoginAndGetTokens(username, password) {
_log(`LOGIN: Iniciando para usuario ${username}...`, 'info');
let result = { success: false, message: "Error desconocido", shortToken: null, shortTokenExpiry: 0, longTokenData: null };
if (!username || !password) {
result.message = "Usuario o contraseña vacíos.";
_log(result.message, 'error');
return result;
}
try {
_log(`Realizando login para usuario: ${username}...`, 'info');
const loginUrl = `${MOVISTAR_API_BASE}/auth/oauth2/token?deviceClass=android.tv`;
const loginHeaders = {
'x-movistarplus-ui': MOVISTAR_UI_VERSION, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json, text/javascript, */*; q=0.01',
'x-movistarplus-os': 'Windows10'
};
const loginBody = new URLSearchParams({'grant_type': 'password', 'deviceClass': 'android.tv', 'username': username, 'password': password });
const response = await fetch(loginUrl, { method: 'POST', headers: loginHeaders, body: loginBody.toString() });
const respJson = await response.json();
if (!response.ok || !respJson.access_token) {
throw new Error(`Fallo en login: ${respJson.error_description || respJson.message || `Error ${response.status}`}`);
}
const new_login_token = respJson.access_token;
const jwtPayload = _parseJwtPayload(new_login_token);
if (!jwtPayload || !jwtPayload.accountNumber || !jwtPayload.exp) {
throw new Error('Token de login inválido o incompleto.');
}
const loggedInAccountNumber = jwtPayload.accountNumber;
const loggedInExpiry = jwtPayload.exp;
_log(`Login OK para cuenta: ${loggedInAccountNumber}`, 'success');
const functional_device_id = await _getOrCreateFunctionalDeviceId({ login_token: new_login_token, account_nbr: loggedInAccountNumber });
if (!functional_device_id) throw new Error("No se pudo obtener/registrar Device ID funcional.");
_log(`Device ID funcional: ...${functional_device_id.slice(-6)}`, 'info');
let existingTokenId = `${M_LONG_TOKEN_PREFIX}${Date.now()}_login_${Math.random().toString(16).slice(2,8)}`;
const allExistingTokens = await _getAllLongTokensFromDB();
const existingTokenForAccount = allExistingTokens.find(t => t.account_nbr === loggedInAccountNumber);
if (existingTokenForAccount) {
existingTokenId = existingTokenForAccount.id;
_log(`Token existente encontrado para ${loggedInAccountNumber} (ID: ...${existingTokenId.slice(-12)}). Se actualizará.`, 'info');
} else {
_log(`Creando nuevo token para ${loggedInAccountNumber} (ID: ...${existingTokenId.slice(-12)}).`, 'info');
}
result.longTokenData = {
id: existingTokenId, login_token: new_login_token, account_nbr: loggedInAccountNumber,
expiry_tstamp: loggedInExpiry, device_id: functional_device_id
};
await _saveLongTokenToDB(result.longTokenData);
await saveAppConfigValue(M_LAST_USED_TOKEN_ID_KEY, result.longTokenData.id);
await saveAppConfigValue(M_LAST_ROTATION_DATE_KEY, new Date().toISOString().slice(0, 10));
_log(`Token largo ${existingTokenForAccount ? 'actualizado' : 'guardado'} en DB.`, 'info');
_log('Generando token CDN...', 'info');
const sdpUrl = `${MOVISTAR_API_CLIENTSERVICES}/movistarplus/android.tv/sdp/mediaPlayers/${result.longTokenData.device_id}/initData?qspVersion=ssp&version=8&status=login`;
const sdpHeaders = {
'x-movistarplus-ui': MOVISTAR_UI_VERSION, 'Authorization': `Bearer ${result.longTokenData.login_token}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 'Content-Type': 'application/json', 'Accept': 'application/json, text/javascript, */*; q=0.01',
'x-movistarplus-deviceid': result.longTokenData.device_id, 'x-movistarplus-os': 'Windows10'
};
const sdpPayload = {
'accountNumber': result.longTokenData.account_nbr, 'sessionUserProfile': 0, 'streamMiscellanea': 'HTTPS', 'deviceType': 'WP_OTT',
'deviceManufacturerProduct': 'Chrome', 'streamDRM': 'Widevine', 'streamFormat': 'DASH'
};
const responseSDP = await fetch(sdpUrl, { method: 'POST', headers: sdpHeaders, body: JSON.stringify(sdpPayload) });
const respJsonSDP = await responseSDP.json();
if (!responseSDP.ok || !respJsonSDP.accessToken || !respJsonSDP.token) { throw new Error(`Fallo al obtener SDP init data: ${respJsonSDP.message || `Error ${responseSDP.status}`}.`); }
const sdp_access_token = respJsonSDP.accessToken;
const hzid_token = respJsonSDP.token;
const cdnTokenUrl = `${MOVISTAR_API_IDSERVER}/${result.longTokenData.account_nbr}/devices/android.tv/cdn/token/refresh`;
const cdnHeaders = {
'Authorization': `Bearer ${sdp_access_token}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Content-Type': 'application/json', 'Accept': 'application/vnd.miviewtv.v1+json', 'X-HZId': hzid_token
};
const responseCdn = await fetch(cdnTokenUrl, { method: 'POST', headers: cdnHeaders });
const responseCdnText = await responseCdn.text();
if (!responseCdn.ok) { throw new Error(`Fallo al obtener Token CDN: Error ${responseCdn.status} - ${responseCdnText.substring(0, 100)}`); }
let respJsonCdn;
try { respJsonCdn = JSON.parse(responseCdnText); } catch (e) { throw new Error("Respuesta CDN OK pero no JSON."); }
if (!respJsonCdn || !respJsonCdn.access_token) { throw new Error(`Fallo al obtener Token CDN: ${respJsonCdn?.message || 'No access_token'}`); }
result.shortToken = respJsonCdn.access_token;
const cdnPayload = _parseJwtPayload(result.shortToken);
result.shortTokenExpiry = (cdnPayload && cdnPayload.exp) ? cdnPayload.exp : 0;
await saveAppConfigValue(M_SHORT_TOKEN_KEY, result.shortToken);
await saveAppConfigValue(M_SHORT_TOKEN_EXPIRY_KEY, result.shortTokenExpiry);
_log(`Nuevo Token CDN obtenido (expira: ${new Date(result.shortTokenExpiry * 1000).toLocaleString()}) y guardado.`, 'success');
result.success = true;
result.message = "Login y obtención de tokens completados con éxito.";
} catch (error) {
result.message = error.message;
_log(`Error en Login Movistar: ${error.message}`, 'error');
}
return result;
}
async function refreshMovistarCdnToken(forceNew = false) {
_log("REFRESH CDN: Iniciando...", 'info');
let result = { success: false, message: "Error desconocido al refrescar CDN", shortToken: null, shortTokenExpiry: 0 };
const nowSeconds = Math.floor(Date.now() / 1000);
const bufferSeconds = 60; // 1 minuto de buffer
if (!forceNew) {
try {
const cachedToken = await getAppConfigValue(M_SHORT_TOKEN_KEY);
let cachedExpiry = await getAppConfigValue(M_SHORT_TOKEN_EXPIRY_KEY) || 0;
if (typeof cachedExpiry !== 'number') cachedExpiry = 0;
if (cachedToken && cachedExpiry > (nowSeconds + bufferSeconds)) {
_log(`Usando token CDN cacheado (expira: ${new Date(cachedExpiry * 1000).toLocaleString()})`, 'info');
result.shortToken = cachedToken;
result.shortTokenExpiry = cachedExpiry;
result.success = true;
result.message = "Token CDN obtenido de la caché.";
return result;
} else {
_log("Token CDN cacheado no válido o expirado. Procediendo a generar uno nuevo.", 'info');
await deleteAppConfigValue(M_SHORT_TOKEN_KEY);
await deleteAppConfigValue(M_SHORT_TOKEN_EXPIRY_KEY);
}
} catch (cacheError) {
_log(`Error leyendo caché de token CDN: ${cacheError.message}. Generando nuevo.`, 'warning');
}
} else {
_log("Forzando generación de nuevo token CDN.", 'info');
}
try {
const longTokenToUse = await _getValidLongTokenForCdnGeneration();
if (!longTokenToUse) {
throw new Error("No se encontró token largo válido y funcional para generar CDN.");
}
_log(`Usando Token Largo ID: ...${longTokenToUse.id.slice(-12)} (Exp: ${new Date(longTokenToUse.expiry_tstamp * 1000).toLocaleDateString()})`, 'info');
_log(`Con Device ID: ...${longTokenToUse.device_id.slice(-6)}`, 'info');
_log('Generando nuevo token CDN...', 'info');
const sdpUrl = `${MOVISTAR_API_CLIENTSERVICES}/movistarplus/android.tv/sdp/mediaPlayers/${longTokenToUse.device_id}/initData?qspVersion=ssp&version=8&status=login`;
const sdpHeaders = {
'x-movistarplus-ui': MOVISTAR_UI_VERSION, 'Authorization': `Bearer ${longTokenToUse.login_token}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 'Content-Type': 'application/json', 'Accept': 'application/json, text/javascript, */*; q=0.01',
'x-movistarplus-deviceid': longTokenToUse.device_id, 'x-movistarplus-os': 'Windows10'
};
const sdpPayload = {
'accountNumber': longTokenToUse.account_nbr, 'sessionUserProfile': 0, 'streamMiscellanea': 'HTTPS', 'deviceType': 'WP_OTT',
'deviceManufacturerProduct': 'Chrome', 'streamDRM': 'Widevine', 'streamFormat': 'DASH'
};
const responseSDP = await fetch(sdpUrl, { method: 'POST', headers: sdpHeaders, body: JSON.stringify(sdpPayload) });
const respJsonSDP = await responseSDP.json();
if (!responseSDP.ok || !respJsonSDP.accessToken || !respJsonSDP.token) { throw new Error(`Fallo al obtener SDP init data (refresh): ${respJsonSDP.message || `Error ${responseSDP.status}`}.`); }
const sdp_access_token = respJsonSDP.accessToken;
const hzid_token = respJsonSDP.token;
const cdnTokenUrl = `${MOVISTAR_API_IDSERVER}/${longTokenToUse.account_nbr}/devices/android.tv/cdn/token/refresh`;
const cdnHeaders = {
'Authorization': `Bearer ${sdp_access_token}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Content-Type': 'application/json', 'Accept': 'application/vnd.miviewtv.v1+json', 'X-HZId': hzid_token
};
const responseCdn = await fetch(cdnTokenUrl, { method: 'POST', headers: cdnHeaders });
const responseCdnText = await responseCdn.text();
if (!responseCdn.ok) { throw new Error(`Fallo al obtener Token CDN (refresh): Error ${responseCdn.status} - ${responseCdnText.substring(0, 100)}`); }
let respJsonCdn;
try { respJsonCdn = JSON.parse(responseCdnText); } catch (e) { throw new Error("Respuesta CDN OK pero no JSON (refresh)."); }
if (!respJsonCdn || !respJsonCdn.access_token) { throw new Error(`Fallo al obtener Token CDN (refresh): ${respJsonCdn?.message || 'No access_token'}`); }
result.shortToken = respJsonCdn.access_token;
const cdnPayload = _parseJwtPayload(result.shortToken);
result.shortTokenExpiry = (cdnPayload && cdnPayload.exp) ? cdnPayload.exp : 0;
if (result.shortTokenExpiry <= Math.floor(Date.now() / 1000)) {
throw new Error("Token CDN generado (refresh) ya ha expirado.");
}
await saveAppConfigValue(M_SHORT_TOKEN_KEY, result.shortToken);
await saveAppConfigValue(M_SHORT_TOKEN_EXPIRY_KEY, result.shortTokenExpiry);
_log(`Nuevo Token CDN obtenido vía refresh (expira: ${new Date(result.shortTokenExpiry * 1000).toLocaleString()}) y guardado.`, 'success');
result.success = true;
result.message = "Token CDN refrescado y guardado con éxito.";
} catch (error) {
result.message = error.message;
_log(`Error refrescando token CDN: ${error.message}`, 'error');
}
return result;
}
async function getAllLongTokens() {
return _getAllLongTokensFromDB();
}
async function deleteLongToken(tokenId) {
_log(`Eliminando token largo (handler): ${tokenId.slice(-12)}`, 'info');
await _deleteLongTokenFromDB(tokenId);
const lastUsedId = await getAppConfigValue(M_LAST_USED_TOKEN_ID_KEY);
if (lastUsedId === tokenId) {
await deleteAppConfigValue(M_LAST_USED_TOKEN_ID_KEY);
_log("Referencia a último token usado eliminada.", 'info');
}
}
async function validateAllLongTokens() {
_log("Validando todos los tokens largos...", 'info');
const nowSeconds = Math.floor(Date.now() / 1000);
const refreshThresholdSeconds = M_REFRESH_LONG_TOKEN_WITHIN_DAYS * 24 * 60 * 60;
let report = { validated: 0, functional: 0, expired: 0, refreshed: 0, refreshErrors: 0, noDeviceId: 0 };
const tokens = await _getAllLongTokensFromDB();
report.validated = tokens.length;
for (const token of tokens) {
if (!token || !token.expiry_tstamp) continue;
if (token.expiry_tstamp < nowSeconds) {
report.expired++;
} else {
if (!token.device_id) {
report.noDeviceId++;
} else {
report.functional++;
if (token.expiry_tstamp < (nowSeconds + refreshThresholdSeconds)) {
_log(`Token ${token.id.slice(-12)} cerca de expirar. Intentando refresco...`, 'info');
try {
const refreshedData = await _refreshMovistarLongToken(token);
if (refreshedData) {
await _saveLongTokenToDB(refreshedData);
report.refreshed++;
_log(`Token ${token.id.slice(-12)} refrescado.`, 'success');
} else {
report.refreshErrors++;
_log(`Fallo al refrescar token ${token.id.slice(-12)}.`, 'warning');
}
} catch (e) {
report.refreshErrors++;
_log(`Error crítico al refrescar ${token.id.slice(-12)}: ${e.message}`, 'error');
}
}
}
}
}
_log(`Validación completa: ${JSON.stringify(report)}`, 'info');
return report;
}
async function deleteExpiredLongTokens() {
_log("Eliminando tokens largos expirados...", 'info');
const tokens = await _getAllLongTokensFromDB();
const nowSeconds = Math.floor(Date.now() / 1000);
const expiredTokens = tokens.filter(t => !t || !t.expiry_tstamp || t.expiry_tstamp < nowSeconds);
let deletedCount = 0;
if (expiredTokens.length === 0) {
_log("No hay tokens expirados para eliminar.", 'info');
return 0;
}
for (const token of expiredTokens) {
if (token && token.id) {
try {
await _deleteLongTokenFromDB(token.id);
const lastUsedId = await getAppConfigValue(M_LAST_USED_TOKEN_ID_KEY);
if(lastUsedId === token.id) await deleteAppConfigValue(M_LAST_USED_TOKEN_ID_KEY);
deletedCount++;
} catch (e) {
_log(`Error eliminando token expirado ${token.id.slice(-12)}: ${e.message}`, 'error');
}
}
}
_log(`${deletedCount} tokens expirados eliminados.`, 'info');
return deletedCount;
}
async function addLongTokenManually(jwtTokenString, deviceId = null) {
_log(`Añadiendo token manualmente: ${jwtTokenString.substring(0,20)}...`, 'info');
const payload = _parseJwtPayload(jwtTokenString);
if (!payload || !payload.accountNumber || !payload.exp) {
throw new Error('Token JWT inválido o no contiene accountNumber/exp.');
}
let deviceIdToUse = deviceId;
if (!deviceIdToUse) {
_log("No se proveyó Device ID, intentando obtener/registrar uno...", 'info');
deviceIdToUse = await _getOrCreateFunctionalDeviceId({
login_token: jwtTokenString,
account_nbr: payload.accountNumber
});
if (!deviceIdToUse) throw new Error("Fallo al obtener/registrar Device ID automáticamente.");
_log(`Device ID obtenido/registrado: ...${deviceIdToUse.slice(-6)}`, 'info');
}
const newTokenData = {
id: `${M_LONG_TOKEN_PREFIX}${Date.now()}_manual_${Math.random().toString(16).slice(2)}`,
login_token: jwtTokenString,
account_nbr: payload.accountNumber,
expiry_tstamp: payload.exp,
device_id: deviceIdToUse
};
await _saveLongTokenToDB(newTokenData);
_log(`Token manual guardado con ID: ${newTokenData.id.slice(-12)}`, 'success');
return newTokenData;
}
async function getMovistarDevicesForToken(longTokenId) {
_log(`Obteniendo dispositivos para token ID ${longTokenId.slice(-12)}...`, 'info');
const tokenData = await getAppConfigValue(longTokenId);
if (!tokenData || !tokenData.login_token || !tokenData.account_nbr) {
throw new Error("Token largo no encontrado o inválido para obtener dispositivos.");
}
const url = MOVISTAR_API_DEVICES_ENDPOINT.replace('{ACCOUNTNUMBER}', tokenData.account_nbr);
const headers = {
'Authorization': `Bearer ${tokenData.login_token}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01', 'x-movistarplus-ui': MOVISTAR_UI_VERSION,
'x-movistarplus-os': 'Windows10', 'Origin': 'https://ver.movistarplus.es', 'Referer': 'https://ver.movistarplus.es/'
};
const response = await fetch(url, { method: 'GET', headers: headers });
const responseText = await response.text();
if (!response.ok) { throw new Error(`Fallo al obtener dispositivos: ${response.status} ${responseText.substring(0,100)}`); }
const devicesApi = JSON.parse(responseText);
if (!Array.isArray(devicesApi)) { throw new Error("Respuesta de API de dispositivos inesperada."); }
return devicesApi.filter(d => d && d.Id && d.Id !== '-').map(d => ({
id: d.Id,
name: d.Name || `Dispositivo ${d.DeviceTypeCode || '?'}`,
type: d.DeviceTypeCode || '?',
reg_date: d.RegistrationDate ? new Date(d.RegistrationDate).toLocaleDateString() : 'N/D',
is_associated: d.Id === tokenData.device_id
}));
}
async function associateDeviceToLongToken(longTokenId, deviceIdToAssociate) {
_log(`Asociando Device ID ${deviceIdToAssociate.slice(-6)} a Token ID ${longTokenId.slice(-12)}...`, 'info');
const tokenData = await getAppConfigValue(longTokenId);
if (!tokenData) throw new Error("Token largo no encontrado para asociar dispositivo.");
if (tokenData.device_id === deviceIdToAssociate) {
_log("El dispositivo ya está asociado a este token.", 'info');
return tokenData;
}
tokenData.device_id = deviceIdToAssociate;
await _saveLongTokenToDB(tokenData);
_log("Device ID asociado y token guardado.", 'success');
return tokenData;
}
async function registerAndAssociateNewDevice(longTokenId) {
_log(`Registrando nuevo dispositivo para Token ID ${longTokenId.slice(-12)}...`, 'info');
const tokenData = await getAppConfigValue(longTokenId);
if (!tokenData || !tokenData.login_token || !tokenData.account_nbr) {
throw new Error("Token largo no encontrado o inválido para registrar nuevo dispositivo.");
}
const url = MOVISTAR_API_REGISTER_DEVICE_ENDPOINT.replace('{ACCOUNTNUMBER}', tokenData.account_nbr);
const headers = {
'Authorization': `Bearer ${tokenData.login_token}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01', 'x-movistarplus-ui': MOVISTAR_UI_VERSION,
'x-movistarplus-os': 'Windows10', 'Content-Type': 'application/json'
};
const response = await fetch(url, { method: 'POST', headers: headers });
const newDeviceIdText = await response.text();
const newDeviceId = newDeviceIdText.trim().replace(/^"|"$/g, '');
if (!response.ok || !newDeviceId || newDeviceId.length < 10) {
let errorMsg = `Fallo registro: ${response.status}`;
if (newDeviceIdText.length < 200 && !newDeviceIdText.includes('<html')) errorMsg += ` - ${newDeviceIdText}`;
if (response.status === 403 || newDeviceIdText.toLowerCase().includes('limit')) errorMsg = "Límite de dispositivos alcanzado.";
throw new Error(errorMsg);
}
_log(`Nuevo Device ID registrado: ...${newDeviceId.slice(-6)}`, 'success');
tokenData.device_id = newDeviceId;
await _saveLongTokenToDB(tokenData);
_log("Nuevo Device ID asociado y token guardado.", 'success');
return tokenData;
}
async function getMovistarShortTokenStatus() {
const token = await getAppConfigValue(M_SHORT_TOKEN_KEY);
const expiry = await getAppConfigValue(M_SHORT_TOKEN_EXPIRY_KEY) || 0;
return { token, expiry: Number(expiry) || 0 };
}
window.MovistarTokenHandler = {
setLogCallback: setMovistarLogCallback,
loginAndGetTokens: doMovistarLoginAndGetTokens,
refreshCdnToken: refreshMovistarCdnToken,
getAllLongTokens: getAllLongTokens,
deleteLongToken: deleteLongToken,
validateAllLongTokens: validateAllLongTokens,
deleteExpiredLongTokens: deleteExpiredLongTokens,
addLongTokenManually: addLongTokenManually,
getDevicesForToken: getMovistarDevicesForToken,
associateDeviceToToken: associateDeviceToLongToken,
registerAndAssociateNewDeviceToToken: registerAndAssociateNewDevice,
getShortTokenStatus: getMovistarShortTokenStatus,
};
_log("Movistar Handler (para Extensión v1.0) inicializado.", 'info');