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"] ?? ""); M3U + 1 || $allow_personal_epg): ?> + + $src): ?> + = htmlspecialchars($src['name']) ?> + + + โ Personal EPG + + + + + + + + โ Personal EPG + Utilisez votre propre source EPG/M3U sur cette instance. + + URL EPG (XMLTV) * + + + + URL M3U (optionnel) + + + + Annuler + Appliquer + + + +