// ── M3U PARSER ──────────────────────────────────────────────────────────────── function slugify(str) { return str.toLowerCase() .replace(/^\d+\s*/, '') // virer le numéro de chaîne en début .replace(/[^a-z0-9]/g, ''); // garder que alphanum } async function loadM3U(url) { if (!url) 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')) { // Extraire le nom : dernière partie après la virgule 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 : chercher une clé qui contient le slug ou l'inverse for (const [key, u] of Object.entries(m3uStreams)) { if (key.includes(slug) || slug.includes(key)) { url = u; break; } } } if (!url) return null; // Passer par proxy.php pour éviter le mixed content HTTP/HTTPS if (url.startsWith('http://')) { return 'proxy.php?url=' + encodeURIComponent(url); } return url; }