From 34e0b408f8a6413aa23e52d2945a34590b8b8f32 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 19:31:41 +0100 Subject: [PATCH 1/4] feat: multi-EPG support + auto-migration old config format - setup.php: dynamic EPG source fields (+ Add source button) - setup.php: toggle to allow personal EPG for visitors - index.php: auto-migrates old epg_url/m3u_url to epg_sources[] on first load - index.php: EPG_SOURCES array injected from PHP into JS - Backward compatible: single-source instances migrate silently --- index.php | 43 ++++-- setup.php | 395 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 277 insertions(+), 161 deletions(-) diff --git a/index.php b/index.php index 3342197..a962074 100644 --- a/index.php +++ b/index.php @@ -1,14 +1,35 @@ 'Main', + 'epg_url' => $config['epg_url'], + 'm3u_url' => $config['m3u_url'] ?? '', + ]]; + $config['allow_personal_epg'] = false; + unset($config['epg_url'], $config['m3u_url']); + file_put_contents($config_path, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +} +// ───────────────────────────────────────────────────────────────────────────── + +$group_name = htmlspecialchars($config['group_name'] ?? 'GridTV'); +$epg_sources = $config['epg_sources'] ?? []; +$allow_personal_epg = !empty($config['allow_personal_epg']); + +// Première source par défaut (pour compatibilité avec le reste du code) +$first_source = $epg_sources[0] ?? []; +$epg_url = htmlspecialchars($first_source['epg_url'] ?? ''); +$m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); ?> @@ -809,8 +830,14 @@ foreach ($theme_files as $file) {
From edf76d58ce22414e57a205385455b965c3af5ee7 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 19:38:50 +0100 Subject: [PATCH 2/4] feat: EPG source switcher in topbar + Personal EPG modal --- index.php | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/index.php b/index.php index a962074..d98b71b 100644 --- a/index.php +++ b/index.php @@ -652,6 +652,67 @@ $m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); #navBtns { display: none !important; } } + + + /* ── MODAL PERSONAL EPG ─────────────────────────────────────────────────── */ + .modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.7); + z-index: 3000; display: none; align-items: center; justify-content: center; + } + .modal-overlay.visible { display: flex; } + .modal-card { + background: var(--surface); border: 1px solid var(--border-bright); + padding: 28px; width: 100%; max-width: 420px; margin: 16px; + } + .modal-title { + font-family: var(--font-mono); font-size: 13px; color: var(--accent); + letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 6px; + } + .modal-subtitle { font-size: 12px; color: var(--text-dim); margin-bottom: 20px; line-height: 1.5; } + .modal-field { margin-bottom: 14px; } + .modal-field label { + display: block; font-family: var(--font-mono); font-size: 10px; + letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 6px; + } + .modal-field input { + width: 100%; background: var(--surface2); border: 1px solid var(--border-bright); + color: var(--text-bright); font-family: var(--font-mono); font-size: 12px; + padding: 9px 10px; outline: none; transition: border-color 0.15s; + } + .modal-field input:focus { border-color: var(--accent); } + .modal-field input::placeholder { color: var(--text-dim); opacity: 0.4; } + .modal-actions { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; } + .modal-btn-cancel { + background: none; border: 1px solid var(--border-bright); color: var(--text-dim); + font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; + text-transform: uppercase; padding: 8px 16px; cursor: pointer; transition: border-color 0.15s; + } + .modal-btn-cancel:hover { border-color: var(--text-dim); color: var(--text); } + .modal-btn-apply { + background: var(--accent); border: none; color: #000; + font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; + text-transform: uppercase; padding: 8px 20px; cursor: pointer; transition: opacity 0.15s; + } + .modal-btn-apply:hover { opacity: 0.85; } + + /* ── SOURCE SELECTOR ─────────────────────────────────────────────────────── */ + .source-select { + background: var(--surface2); + border: 1px solid var(--border-bright); + color: var(--text); + font-family: var(--font-mono); + font-size: 11px; + padding: 5px 8px; + cursor: pointer; + letter-spacing: 0.06em; + height: 28px; + outline: none; + transition: border-color 0.15s, color 0.15s; + max-width: 130px; + } + .source-select:hover { border-color: var(--accent2); color: var(--accent2); } + .source-select option { background: #111; color: #eee; font-family: sans-serif; } + /* ═══════════════════════════════════════════ PLAYER PiP ═══════════════════════════════════════════ */ @@ -772,6 +833,16 @@ $m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); + 1 || $allow_personal_epg): ?> + + + + + + + + From e72105fac260f98e7464daa6ce48b3a50a9a159b Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 19:48:17 +0100 Subject: [PATCH 3/4] fix: persist active EPG source selection in localStorage --- index.php | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/index.php b/index.php index d98b71b..2f1ab46 100644 --- a/index.php +++ b/index.php @@ -1279,9 +1279,9 @@ function switchSource(value) { activeSourceIndex = idx; currentEpgUrl = src.epg_url; currentM3uUrl = src.m3u_url ?? ''; + localStorage.setItem('gridtv-active-source', idx); loadEPG(currentEpgUrl); if (currentM3uUrl) loadM3U(currentM3uUrl); - // Mettre à jour les boutons copy updateCopyButtons(); } @@ -1298,11 +1298,13 @@ function openPersonalEpgModal() { modal.classList.add('visible'); } -function closePersonalEpgModal() { +function closePersonalEpgModal(applied) { document.getElementById('personalEpgModal').classList.remove('visible'); - // Si annulé, remettre le select sur la source précédente - const sel = document.getElementById('sourceSelect'); - if (sel) sel.value = activeSourceIndex; + // Si annulé (pas applied), remettre le select sur la source précédente + if (!applied) { + const sel = document.getElementById('sourceSelect'); + if (sel) sel.value = localStorage.getItem('gridtv-active-source') === 'personal' ? 'personal' : activeSourceIndex; + } } function applyPersonalEpg() { @@ -1311,9 +1313,12 @@ function applyPersonalEpg() { if (!epg) { alert('URL EPG obligatoire'); return; } localStorage.setItem('gridtv-personal-epg', epg); localStorage.setItem('gridtv-personal-m3u', m3u); + localStorage.setItem('gridtv-active-source', 'personal'); currentEpgUrl = epg; currentM3uUrl = m3u; - closePersonalEpgModal(); + const sel = document.getElementById('sourceSelect'); + if (sel) sel.value = 'personal'; + closePersonalEpgModal(true); loadEPG(epg); if (m3u) loadM3U(m3u); } @@ -1460,7 +1465,34 @@ window.addEventListener('resize', () => { if (m !== lastMobile) { lastMobile=m; scrollListenerAdded=false; if (channels.length) renderAll(); } }); -window.addEventListener('load', () => { loadEPG(DEFAULT_EPG_URL); loadM3U(DEFAULT_M3U_URL); }); +window.addEventListener('load', () => { + const savedSource = localStorage.getItem('gridtv-active-source'); + if (savedSource === 'personal') { + const epg = localStorage.getItem('gridtv-personal-epg'); + const m3u = localStorage.getItem('gridtv-personal-m3u') ?? ''; + if (epg) { + currentEpgUrl = epg; + currentM3uUrl = m3u; + const sel = document.getElementById('sourceSelect'); + if (sel) sel.value = 'personal'; + loadEPG(epg); + if (m3u) loadM3U(m3u); + return; + } + } else if (savedSource !== null) { + const idx = parseInt(savedSource); + const src = EPG_SOURCES[idx]; + if (src) { + activeSourceIndex = idx; + currentEpgUrl = src.epg_url; + currentM3uUrl = src.m3u_url ?? ''; + const sel = document.getElementById('sourceSelect'); + if (sel) sel.value = idx; + } + } + loadEPG(currentEpgUrl); + loadM3U(currentM3uUrl); +}); From 2b163be684490f79ff5e61d04854b247ba27082a Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 19:51:25 +0100 Subject: [PATCH 4/4] Readme update --- README.md | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2cc9a07..002c2cf 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ This demo runs with a sample XMLTV feed and fake channels to showcase the interf - ▶️ **Built-in HLS player** — click any channel or live program to watch in a PiP overlay - 🔀 **HTTP→HTTPS proxy** — streams Tunarr over HTTP transparently from an HTTPS page - 🎨 **Theme system** — drop a CSS file in `themes/` and it appears in the menu automatically +- 📡 **Multi-EPG sources** — configure multiple EPG/M3U sources, switch from the topbar +- 👤 **Personal EPG** — optionally let visitors use your instance with their own EPG/M3U - ⚙️ **Guided setup** on first launch — no config files to edit manually - 🔄 **Auto-reload** EPG every 30 minutes - 0️⃣ **Zero JS dependencies** — vanilla PHP, hls.js loaded from CDN @@ -141,9 +143,9 @@ GridTV detects the missing config and automatically redirects you to the setup p | Field | Description | |---|---| -| **Group name** | Displayed top-left in the topbar (e.g. *MyTV*, *FamilyTV*...) | -| **EPG URL (XMLTV)** | Your electronic program guide URL | -| **M3U URL** *(optional)* | Your IPTV playlist — used by the built-in player to match channels to streams | +| **Group name** | Displayed top-left in the topbar | +| **EPG sources** | Add one or more XMLTV sources, each with an optional M3U URL | +| **Personal EPG** | Toggle to allow visitors to use your instance with their own EPG/M3U | Once submitted, `config.json` is created on the server. **The setup page becomes inaccessible.** @@ -158,11 +160,24 @@ nano /var/www/gridtv/config.json ```json { "group_name": "MyGroup TV", - "epg_url": "http://192.168.0.3:8000/api/xmltv.xml", - "m3u_url": "http://192.168.0.3:8000/api/channels.m3u" + "epg_sources": [ + { + "name": "Main", + "epg_url": "http://192.168.0.3:8000/api/xmltv.xml", + "m3u_url": "http://192.168.0.3:8000/api/channels.m3u" + }, + { + "name": "Sports", + "epg_url": "http://192.168.0.3:8001/api/xmltv.xml", + "m3u_url": "" + } + ], + "allow_personal_epg": true } ``` +> Instances running the old single-source format (`epg_url` at root) are **migrated automatically** on first load — no manual action needed. + --- ## 📁 Project structure @@ -207,12 +222,20 @@ Drop it in `themes/` — it appears in the theme selector automatically, no code --- +## 📡 Multiple EPG Sources + +Configure as many EPG/M3U sources as you want in `config.json`. A dropdown appears in the topbar when more than one source is defined. + +If `allow_personal_epg` is `true`, a **"✏ Personal EPG"** option appears in the dropdown, letting any visitor enter their own XMLTV/M3U URLs. Their choice is saved in `localStorage` — zero server impact. + +--- + ## ▶️ Built-in Player Click on any **channel name** or **currently airing program** to open a PiP (picture-in-picture) player in the bottom-right corner. The player requires: -- An **M3U URL** configured in setup (used to match channel names to stream URLs) +- An **M3U URL** configured in the active source (used to match channel names to stream URLs) - The **php-curl** extension installed on the server If your stream source (e.g. Tunarr) serves streams over HTTP while GridTV runs on HTTPS, `proxy.php` handles the relay transparently — no browser mixed-content errors.