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. diff --git a/index.php b/index.php index 3342197..2f1ab46 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'] ?? ''); ?> @@ -631,6 +652,67 @@ $m3u_url = htmlspecialchars($config["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 โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ @@ -751,6 +833,16 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? ""); + 1 || $allow_personal_epg): ?> + + + + + + + + diff --git a/setup.php b/setup.php index 538d575..5867366 100644 --- a/setup.php +++ b/setup.php @@ -1,38 +1,74 @@ 'Main', + 'epg_url' => $existing['epg_url'], + 'm3u_url' => $existing['m3u_url'] ?? '', + ]]; + $existing['allow_personal_epg'] = false; + unset($existing['epg_url'], $existing['m3u_url']); + file_put_contents($config_path, json_encode($existing, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } http_response_code(403); die('Configuration dรฉjร  effectuรฉe. Modifiez config.json via SSH pour changer les paramรจtres.'); } -$error = ''; +$error = ''; $success = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $group_name = trim($_POST['group_name'] ?? ''); - $epg_url = trim($_POST['epg_url'] ?? ''); - $m3u_url = trim($_POST['m3u_url'] ?? ''); + $group_name = trim($_POST['group_name'] ?? ''); + $allow_personal_epg = isset($_POST['allow_personal_epg']); - if (empty($group_name) || empty($epg_url)) { - $error = 'Le nom du groupe et l\'URL EPG sont obligatoires.'; - } elseif (!filter_var($epg_url, FILTER_VALIDATE_URL)) { - $error = 'L\'URL EPG ne semble pas valide.'; - } elseif (!empty($m3u_url) && !filter_var($m3u_url, FILTER_VALIDATE_URL)) { - $error = 'L\'URL M3U ne semble pas valide.'; - } else { - $config = [ - 'group_name' => $group_name, - 'epg_url' => $epg_url, - 'm3u_url' => $m3u_url, + // Rรฉcupรฉrer les sources EPG dynamiques + $names = $_POST['source_name'] ?? []; + $epg_urls = $_POST['source_epg_url'] ?? []; + $m3u_urls = $_POST['source_m3u_url'] ?? []; + + $epg_sources = []; + foreach ($names as $i => $name) { + $epg_url = trim($epg_urls[$i] ?? ''); + $m3u_url = trim($m3u_urls[$i] ?? ''); + if (empty($epg_url)) continue; + if (!filter_var($epg_url, FILTER_VALIDATE_URL)) { + $error = "URL EPG invalide pour la source ยซ $name ยป."; break; + } + if (!empty($m3u_url) && !filter_var($m3u_url, FILTER_VALIDATE_URL)) { + $error = "URL M3U invalide pour la source ยซ $name ยป."; break; + } + $epg_sources[] = [ + 'name' => trim($name) ?: "Source " . ($i + 1), + 'epg_url' => $epg_url, + 'm3u_url' => $m3u_url, ]; - $written = file_put_contents( - __DIR__ . '/config.json', - json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) - ); - if ($written === false) { - $error = 'Impossible d\'รฉcrire config.json. Vรฉrifiez les permissions du dossier (chmod 775 .).'; + } + + if (empty($error)) { + if (empty($group_name)) { + $error = 'Le nom du groupe est obligatoire.'; + } elseif (empty($epg_sources)) { + $error = 'Au moins une source EPG est obligatoire.'; } else { - $success = true; + $config = [ + 'group_name' => $group_name, + 'epg_sources' => $epg_sources, + 'allow_personal_epg' => $allow_personal_epg, + ]; + $written = file_put_contents( + $config_path, + json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + if ($written === false) { + $error = 'Impossible d\'รฉcrire config.json. Vรฉrifiez les permissions (chmod 775 .).'; + } else { + $success = true; + } } } } @@ -46,153 +82,133 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -230,21 +246,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
S'affiche en haut ร  gauche dans la topbar.
-
- - -
URL du fichier XMLTV gรฉnรฉrรฉ par Tunarr ou autre source.
+
Sources EPG
+ +
+
+
SOURCE 1
+ +
+
+ + +
+
+ + +
+
+ + +
Utilisรฉ par le lecteur intรฉgrรฉ pour matcher les chaรฎnes.
+
+
+
-
- - -
Permet de copier l'URL M3U depuis la topbar du guide.
+ + +
Options
+ +
+
+ Autoriser les EPG personnels + Les visiteurs peuvent utiliser votre instance avec leur propre EPG/M3U. +
+
@@ -260,5 +298,56 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ +