// Echapper les donnees issues de l'EPG pour eviter les injections HTML function esc(str) { const d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; } 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'); } function copyUrl(type) { const url = type === 'epg' ? currentEpgUrl : currentM3uUrl; if (!url) return; navigator.clipboard.writeText(url).then(() => { const btns = document.querySelectorAll('.copy-btn'); btns.forEach(b => { if (b.textContent.toLowerCase() === type) { b.textContent = L.copied; b.classList.add('copied'); setTimeout(() => { b.textContent = type.toUpperCase(); b.classList.remove('copied'); }, 1800); }}); }).catch(() => { prompt(url, url); }); } function fmtTime(d) { return `${pad(d.getHours())}:${pad(d.getMinutes())}`; } function fmtTimeFull(d) { return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function isMobile() { return window.innerWidth <= 600; } function parseXMLTVDate(str) { if (!str) return null; const m = str.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\s*([+-]\d{4})?/); if (!m) return null; const offset = m[7] ? parseInt(m[7]) : 0; const offsetMs = (Math.floor(Math.abs(offset)/100)*60 + (Math.abs(offset)%100)) * 60000 * (offset<0?-1:1); return new Date(Date.UTC(+m[1],+m[2]-1,+m[3],+m[4],+m[5],+m[6]) - offsetMs); } function updateClock() { const now = new Date(); document.getElementById('clock').textContent = fmtTimeFull(now); document.getElementById('dateLabel').textContent = `${L.days[now.getDay()]} ${now.getDate()} ${L.months[now.getMonth()]} ${now.getFullYear()}`; } setInterval(updateClock, 1000); updateClock(); async function fetchXML(url) { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); return new DOMParser().parseFromString(await res.text(), 'text/xml'); } async function loadEPG(url) { document.getElementById('loading').style.display = 'flex'; document.getElementById('errorMsg').style.display = 'none'; try { let doc; try { doc = await fetchXML(url); } catch(e) { doc = await fetchXML(CORS_PROXY + encodeURIComponent(url)); } parseAndRender(doc); document.getElementById('loading').style.display = 'none'; } catch(e) { document.getElementById('loading').style.display = 'none'; document.getElementById('errorMsg').style.display = 'flex'; document.getElementById('errorDetail').textContent = `${L.epg_error_sub} (${e.message})`; document.getElementById('epgInput').value = url; } } function loadFromInput() { const url = document.getElementById('epgInput').value.trim(); if (url) loadEPG(url); } function parseEpisode(p) { // Format xmltv_ns : "S.E.part" (0-indexed) ex: "0.2.0/1" = S01E03 const ns = p.querySelector('episode-num[system="xmltv_ns"]')?.textContent; if (ns) { const parts = ns.split('.'); const s = parts[0] !== undefined && parts[0].trim() !== '' ? parseInt(parts[0].trim()) + 1 : null; const ePart = parts[1] !== undefined ? parts[1].trim().split('/')[0] : ''; const e = ePart !== '' ? parseInt(ePart) + 1 : null; if (s !== null || e !== null) return { season: s, episode: e }; } // Format onscreen : "S01E03" ou "s1e3" const os = p.querySelector('episode-num[system="onscreen"]')?.textContent; if (os) { const m = os.match(/[Ss](\d+)[Ee](\d+)/); if (m) return { season: parseInt(m[1]), episode: parseInt(m[2]) }; const m2 = os.match(/[Ee][Pp]?\s*(\d+)/i); if (m2) return { season: null, episode: parseInt(m2[1]) }; } return { season: null, episode: null }; } function fmtEpisode(season, episode) { if (season !== null && episode !== null) return `S${String(season).padStart(2,'0')}E${String(episode).padStart(2,'0')}`; if (episode !== null) return `E${String(episode).padStart(2,'0')}`; return null; } function parseAndRender(doc) { channels = []; programs = {}; doc.querySelectorAll('channel').forEach(ch => { const id = ch.getAttribute('id'); const name = ch.querySelector('display-name')?.textContent || id; const icon = ch.querySelector('icon')?.getAttribute('src') || null; const num = parseInt(name.match(/^(\d+)/)?.[1] ?? '9999'); channels.push({ id, name, icon, num }); programs[id] = []; }); channels.sort((a,b) => a.num - b.num); doc.querySelectorAll('programme').forEach(p => { const chId = p.getAttribute('channel'); if (!programs[chId]) programs[chId] = []; const start = parseXMLTVDate(p.getAttribute('start')); const stop = parseXMLTVDate(p.getAttribute('stop')); const title = p.querySelector('title')?.textContent || ''; const desc = p.querySelector('desc')?.textContent || ''; const epInfo = parseEpisode(p); if (start && stop) programs[chId].push({ start, stop, title, desc, ...epInfo }); }); Object.keys(programs).forEach(id => programs[id].sort((a,b) => a.start - b.start)); renderAll(); startLiveUpdates(); } function renderAll() { if (isMobile()) { renderMobile(); } else { renderGrid(); setTimeout(centerNow, 150); } }