// ── M3U PARSER ──────────────────────────────────────────────────────────────── function slugify(str) { return str.toLowerCase() .replace(/^\d+\s*/, '') // Strip any leading channel number. .replace(/[^a-z0-9]/g, ''); // Keep only alphanumeric characters. } async function loadM3U(url) { if (!url) { m3uStreams = {}; return; } try { let text; try { const r = await fetch(url); text = await r.text(); } catch(e) { const r = await fetch(CORS_PROXY + encodeURIComponent(url)); text = await r.text(); } parseM3U(text); } catch(e) { console.warn('M3U load failed:', e); } } function parseM3U(text) { m3uStreams = {}; const lines = text.split('\n'); let currentName = null; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXTINF')) { // Extract the display name from the last comma-separated segment. const comma = line.lastIndexOf(','); currentName = comma !== -1 ? line.slice(comma + 1).trim() : null; } else if (line && !line.startsWith('#') && currentName) { m3uStreams[slugify(currentName)] = line; currentName = null; } } console.log(`M3U: ${Object.keys(m3uStreams).length} streams chargés`); } function getStreamUrl(channelName) { const slug = slugify(channelName); let url = null; if (m3uStreams[slug]) { url = m3uStreams[slug]; } else { // Fallback: try a partial match in either direction. for (const [key, u] of Object.entries(m3uStreams)) { if (key.includes(slug) || slug.includes(key)) { url = u; break; } } } if (!url) return null; // Route plain HTTP streams through proxy.php to avoid mixed-content issues. if (url.startsWith('http://')) { return 'proxy.php?url=' + encodeURIComponent(url); } return url; }