Advanced_DRM_Player/m3u_utils.js

286 lines
13 KiB
JavaScript

function parseM3U(content, sourceOrigin = null) {
const lines = content.split(/\r\n?|\n/).map(line => line.trim()).filter(Boolean);
const parsedChannels = [];
let currentChannel = null;
const seenGroups = new Set();
const orderedGroups = [];
if (lines.length > 0 && !lines[0].startsWith('#EXTM3U')) {
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#EXTINF:')) {
if (currentChannel && !currentChannel.url) {
}
currentChannel = {
name: `Canal ${parsedChannels.length + 1}`,
url: null,
attributes: {},
kodiProps: {},
vlcOptions: {},
extHttp: {},
effectiveEpgId: null,
sourceOrigin: sourceOrigin
};
try {
const extinfMatch = line.match(/^#EXTINF:(-?\d*(?:\.\d+)?)([^,]*),(.*)$/);
if (extinfMatch) {
currentChannel.attributes.duration = extinfMatch[1];
const attrString = extinfMatch[2].trim();
const channelName = extinfMatch[3].trim();
currentChannel.name = channelName || `Canal ${parsedChannels.length + 1}`;
const attributeMatchRegex = /([a-zA-Z0-9_-]+)=("([^"]*)"|'([^']*)'|([^"\s',]+))/g;
let attrMatch;
while ((attrMatch = attributeMatchRegex.exec(attrString)) !== null) {
const attrName = attrMatch[1].toLowerCase();
const attrValue = attrMatch[3] || attrMatch[4] || attrMatch[5] || '';
currentChannel.attributes[attrName] = attrValue.trim();
}
currentChannel['tvg-id'] = currentChannel.attributes['tvg-id'] || '';
currentChannel['tvg-name'] = currentChannel.attributes['tvg-name'] || '';
currentChannel['tvg-logo'] = currentChannel.attributes['tvg-logo'] || '';
currentChannel['group-title'] = currentChannel.attributes['group-title'] || '';
currentChannel.attributes['ch-number'] = currentChannel.attributes['ch-number'] || currentChannel.attributes['tvg-chno'] || '';
if (currentChannel.attributes['source-origin']) {
currentChannel.sourceOrigin = currentChannel.attributes['source-origin'];
}
const groupTitle = currentChannel['group-title'];
if (groupTitle && groupTitle.trim() !== '' && !seenGroups.has(groupTitle)) {
seenGroups.add(groupTitle); orderedGroups.push(groupTitle);
}
} else {
const commaIndex = line.indexOf(',');
if (commaIndex !== -1) {
currentChannel.name = line.substring(commaIndex + 1).trim() || `Canal ${parsedChannels.length + 1}`;
currentChannel.attributes.duration = line.substring("#EXTINF:".length, commaIndex).trim();
} else { currentChannel = null; }
}
} catch (e) { console.warn("Error parsing #EXTINF line:", line, e); currentChannel = null; }
} else if (currentChannel && line.startsWith('#KODIPROP:')) {
const propMatch = line.match(/^#KODIPROP:([^=]+)=(.*)$/);
if (propMatch && propMatch[1] && typeof propMatch[2] === 'string') {
currentChannel.kodiProps[propMatch[1].trim()] = propMatch[2].trim();
}
} else if (currentChannel && line.startsWith('#EXTVLCOPT:')) {
const propMatch = line.match(/^#EXTVLCOPT:([^=]+)=(.*)$/);
if (propMatch && propMatch[1] && typeof propMatch[2] === 'string') {
const key = propMatch[1].trim();
let value = propMatch[2].trim();
if (key === 'http-user-agent' && value.includes('&Referer=')) {
const parts = value.split('&Referer=');
currentChannel.vlcOptions['http-user-agent'] = parts[0];
if (parts.length > 1 && parts[1]) {
currentChannel.vlcOptions['http-referrer'] = parts[1];
}
} else if (key === 'http-user-agent' && value.includes('&referer=')) {
const parts = value.split('&referer=');
currentChannel.vlcOptions['http-user-agent'] = parts[0];
if (parts.length > 1 && parts[1]) {
currentChannel.vlcOptions['http-referrer'] = parts[1];
}
}
else {
currentChannel.vlcOptions[key] = value;
}
}
} else if (currentChannel && line.startsWith('#EXTHTTP:')) {
try {
const httpJson = line.substring('#EXTHTTP:'.length).trim();
if (httpJson) { currentChannel.extHttp = JSON.parse(httpJson); }
} catch (e) { console.warn("Error parsing #EXTHTTP JSON:", line, e); }
} else if (currentChannel && line.startsWith('#EXTGRP:')) {
const groupName = line.substring('#EXTGRP:'.length).trim();
if (!currentChannel['group-title'] && groupName) {
currentChannel['group-title'] = groupName;
if (!seenGroups.has(groupName)) { seenGroups.add(groupName); orderedGroups.push(groupName); }
} else if (groupName && groupName.trim() !== '' && !seenGroups.has(groupName)) {
seenGroups.add(groupName); orderedGroups.push(groupName);
}
} else if (!line.startsWith('#') && currentChannel && !currentChannel.url) {
const url = line.trim();
if (url) {
currentChannel.url = url;
if (!currentChannel.attributes['source-origin']) {
if (url.includes('atres-live.atresmedia.com')) {
currentChannel.sourceOrigin = 'Atresplayer';
} else if (url.includes('orangetv.orange.es')) {
currentChannel.sourceOrigin = 'OrangeTV';
} else if (url.toLowerCase().includes('dazn')) {
currentChannel.sourceOrigin = 'DAZN';
} else if (url.toLowerCase().includes('telefonica.com') || url.toLowerCase().includes('movistarplus.es')) {
currentChannel.sourceOrigin = 'Movistar+';
}
}
if (currentChannel.attributes['source-origin']) {
currentChannel.sourceOrigin = currentChannel.attributes['source-origin'];
}
parsedChannels.push(currentChannel);
currentChannel = null;
} else {
currentChannel = null;
}
} else if (!line.startsWith('#') && !currentChannel) {
}
}
if (currentChannel && !currentChannel.url) {
}
const finalOrderedGroups = Array.from(new Set(orderedGroups));
return { channels: parsedChannels, groupOrder: finalOrderedGroups };
}
function normalizeStringForComparison(str) {
if (typeof str !== 'string') return '';
return str.toLowerCase()
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[hd|sd|fhd|uhd|4k|8k|(\(\d+p\))|[,.:;\-_\s()\[\]&+'!¡¿?]/g, '')
.replace(/\s+/g, '');
}
function getStringSimilarity(str1, str2) {
const s1 = normalizeStringForComparison(str1);
const s2 = normalizeStringForComparison(str2);
if (s1 === s2) return 1.0;
if (s1.length < 2 || s2.length < 2) return 0.0;
const profile1 = {};
for (let i = 0; i < s1.length - 1; i++) {
const bigram = s1.substring(i, i + 2);
profile1[bigram] = (profile1[bigram] || 0) + 1;
}
const profile2 = {};
for (let i = 0; i < s2.length - 1; i++) {
const bigram = s2.substring(i, i + 2);
profile2[bigram] = (profile2[bigram] || 0) + 1;
}
const union = new Set([...Object.keys(profile1), ...Object.keys(profile2)]);
let intersectionSize = 0;
for (const bigram of union) {
if (profile1[bigram] && profile2[bigram]) {
intersectionSize += Math.min(profile1[bigram], profile2[bigram]);
}
}
return (2.0 * intersectionSize) / (s1.length - 1 + s2.length - 1);
}
function base64ToHex(base64) {
try {
const b64Str = String(base64 || '');
const binary = atob(b64Str.replace(/-/g, '+').replace(/_/g, '/'));
let hex = '';
for (let i = 0; i < binary.length; i++) {
const byte = binary.charCodeAt(i).toString(16).padStart(2, '0');
hex += byte;
}
return hex.toLowerCase();
} catch (e) {
return null;
}
}
function parseClearKey(keyString) {
if (!keyString || typeof keyString !== 'string') {
return null;
}
keyString = keyString.trim();
const clearKeys = {};
try {
if (keyString.startsWith('{') && keyString.endsWith('}')) {
try {
const parsed = JSON.parse(keyString);
if (parsed.keys && Array.isArray(parsed.keys)) {
for (const keyObj of parsed.keys) {
if (keyObj.kty !== 'oct') { continue; }
if (!keyObj.k || !keyObj.kid) { continue; }
const kidHex = base64ToHex(keyObj.kid);
const keyHex = base64ToHex(keyObj.k);
if (kidHex && keyHex && /^[0-9a-f]{32}$/.test(kidHex) && /^[0-9a-f]{32}$/.test(keyHex)) {
clearKeys[kidHex] = keyHex;
}
}
} else {
for (const kid_orig in parsed) {
if (Object.prototype.hasOwnProperty.call(parsed, kid_orig)) {
const key_orig = parsed[kid_orig];
if (typeof kid_orig !== 'string' || typeof key_orig !== 'string') {
continue;
}
let kidHexStr, keyHexStr;
if (!/^[0-9a-fA-F]{32}$/.test(kid_orig)) {
const converted = base64ToHex(kid_orig);
kidHexStr = converted ? converted : '';
} else {
kidHexStr = kid_orig.toLowerCase();
}
if (!/^[0-9a-fA-F]{32}$/.test(key_orig)) {
const converted = base64ToHex(key_orig);
keyHexStr = converted ? converted : '';
} else {
keyHexStr = key_orig.toLowerCase();
}
if (/^[0-9a-f]{32}$/.test(kidHexStr) && /^[0-9a-f]{32}$/.test(keyHexStr)) {
clearKeys[kidHexStr] = keyHexStr;
}
}
}
}
} catch (jsonParseError) {
const compactObjectMatch = keyString.match(/^\{([0-9a-fA-F]{32}):([0-9a-fA-F]{32})\}$/);
if (compactObjectMatch) {
clearKeys[compactObjectMatch[1].toLowerCase()] = compactObjectMatch[2].toLowerCase();
}
}
}
if (Object.keys(clearKeys).length === 0) {
const simpleHexMatch = keyString.match(/^([0-9a-fA-F]{32}):([0-9a-fA-F]{32})$/);
if (simpleHexMatch) {
clearKeys[simpleHexMatch[1].toLowerCase()] = simpleHexMatch[2].toLowerCase();
return clearKeys;
}
const simpleBase64Match = keyString.match(/^([A-Za-z0-9+/_-]+={0,2}):([A-Za-z0-9+/_-]+={0,2})$/);
if (simpleBase64Match) {
const kidHex = base64ToHex(simpleBase64Match[1]);
const keyHex = base64ToHex(simpleBase64Match[2]);
if (kidHex && keyHex && /^[0-9a-f]{32}$/.test(kidHex) && /^[0-9a-f]{32}$/.test(keyHex)) {
clearKeys[kidHex] = keyHex;
return clearKeys;
}
}
}
if (Object.keys(clearKeys).length === 0) {
return null;
}
return clearKeys;
} catch (e) {
console.error("Error parsing clearkey string:", e, keyString);
return null;
}
}
function safeParseInt(value, defaultValue = 0) {
const parsed = parseInt(value, 10);
return isNaN(parsed) ? defaultValue : parsed;
}
function detectMimeType(url) {
if (typeof url !== 'string') return '';
const u = url.toLowerCase();
const urlWithoutQuery = u.split('?')[0];
if (urlWithoutQuery.endsWith('.m3u8')) return 'application/x-mpegURL';
if (urlWithoutQuery.endsWith('.mpd')) return 'application/dash+xml';
return '';
}