diff --git a/index.php b/index.php index 570ec47..428862d 100644 --- a/index.php +++ b/index.php @@ -631,6 +631,7 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? ""); #navBtns { display: none !important; } } + @@ -697,6 +698,17 @@ foreach ($theme_files as $file) {
LIVE
+ +
+
+
+
+ +
+ +
⚠ Stream indisponible
+
+
@@ -734,6 +746,7 @@ function msToX(ms) { return (ms - GRID_START.getTime()) / 60000 * PX_PER_MIN; } let channels = []; let programs = {}; +let m3uStreams = {}; // slug normalisé → url stream let scrollListenerAdded = false; // sync scroll vertical chaînes function pad(n) { return String(n).padStart(2,'0'); } @@ -891,6 +904,7 @@ function renderChannels() { const name = document.createElement('div'); name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name; cell.appendChild(name); + cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id))); list.appendChild(cell); }); } @@ -953,6 +967,9 @@ function renderPrograms() { block.addEventListener('mouseenter', e=>showTooltip(e,p)); block.addEventListener('mousemove', e=>moveTooltip(e)); block.addEventListener('mouseleave', hideTooltip); + if (p.start <= now && p.stop > now) { + block.addEventListener('click', () => openPip(ch.name, p.title)); + } row.appendChild(block); }); inner.appendChild(row); @@ -1042,6 +1059,13 @@ function renderMobile() { }); } +// ── HELPERS ────────────────────────────────────────────────────────────────── +function getNowPlaying(chId) { + const now = new Date(); + const p = (programs[chId] || []).find(p => p.start <= now && p.stop > now); + return p ? p.title : null; +} + // ── TOOLTIP ─────────────────────────────────────────────────────────────────── function showTooltip(e, p) { const ep = fmtEpisode(p.season, p.episode); @@ -1060,6 +1084,105 @@ function moveTooltip(e) { function hideTooltip() { document.getElementById('tooltip').classList.remove('visible'); } + +// ── 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); + if (m3uStreams[slug]) return m3uStreams[slug]; + // Fallback : chercher une clé qui contient le slug ou l'inverse + for (const [key, url] of Object.entries(m3uStreams)) { + if (key.includes(slug) || slug.includes(key)) return url; + } + return null; +} + +// ── PLAYER PiP ──────────────────────────────────────────────────────────────── +let hlsInstance = null; + +function openPip(channelName, programTitle) { + const url = getStreamUrl(channelName); + const pip = document.getElementById('pip'); + const video = document.getElementById('pipVideo'); + const err = document.getElementById('pipError'); + + document.getElementById('pipChannel').textContent = channelName; + document.getElementById('pipProgram').textContent = programTitle || ''; + err.style.display = 'none'; + pip.classList.add('visible'); + + // Détruire l'instance HLS précédente + if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } + video.src = ''; + + if (!url) { + err.style.display = 'block'; + err.textContent = '⚠ Aucun stream trouvé pour cette chaîne'; + return; + } + + if (Hls.isSupported()) { + hlsInstance = new Hls({ enableWorker: true }); + hlsInstance.loadSource(url); + hlsInstance.attachMedia(video); + hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {})); + hlsInstance.on(Hls.Events.ERROR, (e, data) => { + if (data.fatal) { err.style.display = 'block'; err.textContent = '⚠ Erreur de lecture du stream'; } + }); + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + // Safari natif HLS + video.src = url; + video.addEventListener('loadedmetadata', () => video.play().catch(() => {})); + } else { + err.style.display = 'block'; + err.textContent = '⚠ HLS non supporté sur ce navigateur'; + } +} + +function closePip() { + const pip = document.getElementById('pip'); + const video = document.getElementById('pipVideo'); + pip.classList.remove('visible'); + if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } + video.pause(); video.src = ''; +} + // ── THÈMES ──────────────────────────────────────────────────────────────────── function setTheme(theme) { document.getElementById('themeStylesheet').href = 'themes/' + theme + '.css'; @@ -1087,7 +1210,7 @@ window.addEventListener('resize', () => { if (m !== lastMobile) { lastMobile=m; scrollListenerAdded=false; if (channels.length) renderAll(); } }); -window.addEventListener('load', () => loadEPG(DEFAULT_EPG_URL)); +window.addEventListener('load', () => { loadEPG(DEFAULT_EPG_URL); loadM3U(DEFAULT_M3U_URL); });