Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d0b4796b2 | ||
|
|
de1f5a336c | ||
|
|
4f0e1f55ba | ||
|
|
8ba8c3013e | ||
|
|
9609f096ef | ||
|
|
8c81c05bca | ||
|
|
ff998d379d | ||
|
|
62732ab2c6 | ||
|
|
25dabc97b2 | ||
|
|
e81cd8b64d | ||
|
|
30d8a24927 | ||
|
|
d6532d3501 | ||
|
|
17db9f56f7 |
@@ -0,0 +1,9 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
data
|
||||||
|
assets/preview.png
|
||||||
|
assets/.DS_Store
|
||||||
|
src/.DS_Store
|
||||||
|
.version.json.swp
|
||||||
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
config.json
|
config.json
|
||||||
.DS_Store
|
data/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+12
-2
@@ -4,8 +4,18 @@ RUN docker-php-ext-install curl && \
|
|||||||
a2enmod rewrite
|
a2enmod rewrite
|
||||||
|
|
||||||
COPY . /var/www/html/
|
COPY . /var/www/html/
|
||||||
|
COPY docker/docker-entrypoint.sh /usr/local/bin/gridtv-entrypoint.sh
|
||||||
|
|
||||||
RUN chown -R www-data:www-data /var/www/html && \
|
RUN mkdir -p /data && \
|
||||||
chmod -R 775 /var/www/html
|
chown -R www-data:www-data /var/www/html /data && \
|
||||||
|
find /var/www/html -type d -exec chmod 755 {} \; && \
|
||||||
|
find /var/www/html -type f -exec chmod 644 {} \; && \
|
||||||
|
chmod 755 /usr/local/bin/gridtv-entrypoint.sh
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD php -r "exit(@file_get_contents('http://127.0.0.1/') === false ? 1 : 0);"
|
||||||
|
|
||||||
|
ENTRYPOINT ["gridtv-entrypoint.sh"]
|
||||||
|
CMD ["apache2-foreground"]
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ This demo runs with sample XMLTV feeds to showcase the interface.
|
|||||||
- ⚙️ **Re-editable setup** — protected by an admin key, no SSH required to update config
|
- ⚙️ **Re-editable setup** — protected by an admin key, no SSH required to update config
|
||||||
- 🔔 **Update notifications** — a badge appears in the topbar when a new release is available on GitHub
|
- 🔔 **Update notifications** — a badge appears in the topbar when a new release is available on GitHub
|
||||||
- 🔄 **Auto-reload** EPG every 30 minutes
|
- 🔄 **Auto-reload** EPG every 30 minutes
|
||||||
- 0️⃣ **Zero JS dependencies** — vanilla PHP, hls.js loaded from CDN
|
- 0️⃣ **Zero build tooling** — vanilla PHP/JS/CSS, with bundled local assets
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -70,15 +70,19 @@ docker compose up -d
|
|||||||
|
|
||||||
Then open `http://localhost:8080` and follow the setup wizard.
|
Then open `http://localhost:8080` and follow the setup wizard.
|
||||||
|
|
||||||
|
Docker stores the generated configuration in `./data/config.json` on the host.
|
||||||
|
|
||||||
To run on a custom port:
|
To run on a custom port:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PORT=9000 docker compose up -d
|
PORT=9000 docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To back up or migrate your Docker setup, keep the `data/` directory.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Option B — Apache / Nginx
|
### Option B — Native PHP host
|
||||||
|
|
||||||
#### 1. Clone the repo
|
#### 1. Clone the repo
|
||||||
|
|
||||||
@@ -94,7 +98,25 @@ chmod 775 /var/www/gridtv
|
|||||||
chown -R www-data:www-data /var/www/gridtv
|
chown -R www-data:www-data /var/www/gridtv
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3a. Configure Nginx
|
#### 3. Install php-curl
|
||||||
|
|
||||||
|
The built-in player uses `proxy.php` to relay HTTP streams over HTTPS. This requires the **php-curl** extension:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu — adjust version to match your PHP
|
||||||
|
apt install php8.4-curl
|
||||||
|
systemctl reload apache2 # or: systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Reverse proxy examples
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show reverse proxy / vhost examples</summary>
|
||||||
|
|
||||||
|
These examples are intentionally minimal. Replace `guide.your-domain.com` with your domain and adjust the PHP socket or upstream target to match your host.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Nginx + PHP-FPM</summary>
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
@@ -115,13 +137,16 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 💡 Adjust the PHP version (`php8.2-fpm`) to match the one installed on your server.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nginx -t && systemctl reload nginx
|
nginx -t && systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3b. Configure Apache
|
> Adjust `php8.2-fpm.sock` to match the PHP-FPM version installed on your server.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Apache vhost</summary>
|
||||||
|
|
||||||
```apache
|
```apache
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
@@ -140,16 +165,52 @@ a2enmod php8.4 rewrite
|
|||||||
systemctl reload apache2
|
systemctl reload apache2
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. Install php-curl
|
</details>
|
||||||
|
|
||||||
The built-in player uses `proxy.php` to relay HTTP streams over HTTPS. This requires the **php-curl** extension:
|
<details>
|
||||||
|
<summary>Caddy</summary>
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
guide.your-domain.com {
|
||||||
|
root * /var/www/gridtv
|
||||||
|
php_fastcgi unix//run/php/php8.2-fpm.sock
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Debian/Ubuntu — adjust version to match your PHP
|
systemctl reload caddy
|
||||||
apt install php8.4-curl
|
|
||||||
systemctl reload apache2 # or: systemctl reload nginx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> If you use `xcaddy` or a distro package, keep the same site block and only adapt the PHP-FPM socket path.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Traefik (Docker labels)</summary>
|
||||||
|
|
||||||
|
Use this if GridTV runs in Docker and Traefik is your front proxy:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gridtv:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.gridtv.rule=Host(`guide.your-domain.com`)"
|
||||||
|
- "traefik.http.routers.gridtv.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.gridtv.tls=true"
|
||||||
|
- "traefik.http.services.gridtv.loadbalancer.server.port=80"
|
||||||
|
```
|
||||||
|
|
||||||
|
You still need a running Traefik instance with `websecure` configured and DNS pointing to it.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
#### 5. First launch — Setup
|
#### 5. First launch — Setup
|
||||||
|
|
||||||
Open your browser at `http://guide.your-domain.com`.
|
Open your browser at `http://guide.your-domain.com`.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,87 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow Condensed';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/barlow-condensed-300.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow Condensed';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/barlow-condensed-400.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow Condensed';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/barlow-condensed-600.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Barlow Condensed';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/barlow-condensed-700.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Share Tech Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/share-tech-mono-400.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rajdhani';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/rajdhani-400.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rajdhani';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/rajdhani-600.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rajdhani';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/rajdhani-700.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IM Fell English';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/im-fell-english-400.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IM Fell English';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/im-fell-english-400-italic.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Special Elite';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/assets/fonts/special-elite-400.ttf') format('truetype');
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+2
File diff suppressed because one or more lines are too long
+1
-1
@@ -4,5 +4,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${PORT:-8080}:80"
|
- "${PORT:-8080}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.json:/var/www/html/config.json
|
- ./data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
mkdir -p /data
|
||||||
|
chown -R www-data:www-data /data
|
||||||
|
chmod 775 /data
|
||||||
|
|
||||||
|
rm -f /var/www/html/config.json
|
||||||
|
ln -s /data/config.json /var/www/html/config.json
|
||||||
|
|
||||||
|
exec docker-php-entrypoint "$@"
|
||||||
@@ -1,81 +1,160 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* GridTV — proxy.php (Apache + cURL)
|
* GridTV — proxy.php
|
||||||
|
*
|
||||||
|
* Proxy HTTP->HTTPS restreint aux hotes explicitement configures dans config.json.
|
||||||
|
* Les IP privees et LAN sont autorisees si l'administrateur les a configurees.
|
||||||
|
* Les redirections sont suivies manuellement avec revalidation de l'hote a chaque saut.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$url = $_GET['url'] ?? '';
|
// ── Whitelist : hotes autorises extraits de config.json ───────────────────────
|
||||||
if (empty($url) || !preg_match('#^https?://#i', $url)) {
|
$config_path = __DIR__ . '/config.json';
|
||||||
http_response_code(400); die('Invalid URL');
|
$allowed_hosts = [];
|
||||||
|
|
||||||
|
if (file_exists($config_path)) {
|
||||||
|
$config = json_decode(file_get_contents($config_path), true) ?? [];
|
||||||
|
foreach ($config['epg_sources'] ?? [] as $src) {
|
||||||
|
foreach (['epg_url', 'm3u_url'] as $key) {
|
||||||
|
if (!empty($src[$key])) {
|
||||||
|
$host = strtolower(parse_url($src[$key], PHP_URL_HOST) ?? '');
|
||||||
|
if ($host !== '') $allowed_hosts[] = $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$base = preg_replace('#[^/]*(\?.*)?$#', '', $url);
|
// ── Fonctions ─────────────────────────────────────────────────────────────────
|
||||||
$origin = parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST);
|
|
||||||
$port = parse_url($url, PHP_URL_PORT);
|
function is_allowed_url(string $url, array $allowed_hosts): bool {
|
||||||
if ($port) $origin .= ':' . $port;
|
if (!preg_match('#^https?://#i', $url)) return false;
|
||||||
$path = parse_url($url, PHP_URL_PATH) ?? '';
|
$host = strtolower(parse_url($url, PHP_URL_HOST) ?? '');
|
||||||
|
return $host !== '' && in_array($host, $allowed_hosts, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve_url(string $base, string $location): string {
|
||||||
|
if (preg_match('#^https?://#i', $location)) return $location;
|
||||||
|
$parts = parse_url($base);
|
||||||
|
$origin = $parts['scheme'] . '://' . $parts['host'];
|
||||||
|
if (!empty($parts['port'])) $origin .= ':' . $parts['port'];
|
||||||
|
if ($location[0] === '/') return $origin . $location;
|
||||||
|
return $origin . rtrim(dirname($parts['path'] ?? '/'), '/') . '/' . $location;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch avec redirections manuelles — chaque Location: est revalidee contre la whitelist.
|
||||||
|
* $stream = true : stream chunk par chunk (segments video)
|
||||||
|
* $stream = false : retourne le body complet (playlists m3u8)
|
||||||
|
*/
|
||||||
|
function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $stream = false): array {
|
||||||
|
$max_redirects = 5;
|
||||||
|
|
||||||
|
for ($i = 0; $i <= $max_redirects; $i++) {
|
||||||
|
if (!is_allowed_url($url, $allowed_hosts)) {
|
||||||
|
http_response_code(403); die('Host not allowed after redirect');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0';
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_FOLLOWLOCATION => false,
|
||||||
|
CURLOPT_HEADER => true,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
CURLOPT_USERAGENT => $ua,
|
||||||
|
CURLOPT_HTTPHEADER => ['Accept: */*'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
http_response_code(502); die('Upstream error');
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_headers = substr($response, 0, $header_size);
|
||||||
|
$body = substr($response, $header_size);
|
||||||
|
|
||||||
|
// Redirection
|
||||||
|
if ($code >= 300 && $code < 400) {
|
||||||
|
if (!preg_match('/^Location:\s*(.+)$/mi', $raw_headers, $m)) {
|
||||||
|
http_response_code(502); die('Invalid redirect');
|
||||||
|
}
|
||||||
|
$url = resolve_url($url, trim($m[1]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$code, $raw_headers, $body, $url];
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(508); die('Too many redirects');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Valider l'URL initiale ─────────────────────────────────────────────────────
|
||||||
|
$url = $_GET['url'] ?? '';
|
||||||
|
|
||||||
|
if (!is_allowed_url($url, $allowed_hosts)) {
|
||||||
|
http_response_code(empty($allowed_hosts) ? 503 : 403);
|
||||||
|
die(empty($allowed_hosts) ? 'No sources configured' : 'Host not allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Determiner le type de ressource ───────────────────────────────────────────
|
||||||
|
$path = parse_url($url, PHP_URL_PATH) ?? '';
|
||||||
$is_segment = preg_match('#\.(ts|aac|mp4|m4s|fmp4)(\?|$)#i', $path);
|
$is_segment = preg_match('#\.(ts|aac|mp4|m4s|fmp4)(\?|$)#i', $path);
|
||||||
|
|
||||||
header('Access-Control-Allow-Origin: *');
|
header('Access-Control-Allow-Origin: *');
|
||||||
header('Cache-Control: no-cache');
|
header('Cache-Control: no-cache');
|
||||||
|
|
||||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0';
|
// ── Segment binaire — stream chunk par chunk ───────────────────────────────────
|
||||||
|
|
||||||
if ($is_segment) {
|
if ($is_segment) {
|
||||||
// Segment binaire — stream via cURL chunk par chunk
|
|
||||||
header('Content-Type: video/MP2T');
|
header('Content-Type: video/MP2T');
|
||||||
header('X-Content-Type-Options: nosniff');
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
if (ob_get_level()) ob_end_clean();
|
if (ob_get_level()) ob_end_clean();
|
||||||
|
|
||||||
$ch = curl_init($url);
|
// Pour les segments, on suit les redirections en streaming direct
|
||||||
|
// apres avoir valide l'URL finale via fetch_with_checked_redirects en mode non-stream
|
||||||
|
[$code, , , $final_url] = fetch_with_checked_redirects($url, $allowed_hosts, false);
|
||||||
|
|
||||||
|
if ($code >= 400) { http_response_code($code); die(); }
|
||||||
|
|
||||||
|
// Maintenant streamer l'URL finale
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0';
|
||||||
|
$ch = curl_init($final_url);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => false,
|
||||||
CURLOPT_TIMEOUT => 30,
|
CURLOPT_TIMEOUT => 30,
|
||||||
CURLOPT_USERAGENT => $ua,
|
CURLOPT_USERAGENT => $ua,
|
||||||
CURLOPT_HTTPHEADER => ['Accept: */*'],
|
CURLOPT_HTTPHEADER => ['Accept: */*'],
|
||||||
CURLOPT_RETURNTRANSFER => false,
|
CURLOPT_RETURNTRANSFER => false,
|
||||||
CURLOPT_WRITEFUNCTION => function($ch, $data) {
|
CURLOPT_WRITEFUNCTION => function($ch, $data) {
|
||||||
echo $data;
|
echo $data; flush(); return strlen($data);
|
||||||
flush();
|
|
||||||
return strlen($data);
|
|
||||||
},
|
},
|
||||||
CURLOPT_HEADERFUNCTION => function($ch, $header) {
|
CURLOPT_HEADERFUNCTION => function($ch, $header) {
|
||||||
$h = trim($header);
|
$h = trim($header);
|
||||||
if (preg_match('/^Content-Type:/i', $h)) {
|
if (preg_match('/^Content-Type:/i', $h)) header($h);
|
||||||
header($h);
|
|
||||||
}
|
|
||||||
return strlen($header);
|
return strlen($header);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
$ok = curl_exec($ch);
|
||||||
$ok = curl_exec($ch);
|
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
if (!$ok || $code >= 400) {
|
if (!$ok || $code >= 400) http_response_code($code ?: 502);
|
||||||
http_response_code($code ?: 502);
|
|
||||||
}
|
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
// ── Playlist m3u8 — fetch + réécriture URLs ────────────────────────────────────
|
||||||
} else {
|
} else {
|
||||||
// Playlist .m3u8 — fetch + réécriture URLs
|
[$code, , $body, $final_url] = fetch_with_checked_redirects($url, $allowed_hosts, false);
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt_array($ch, [
|
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
|
||||||
CURLOPT_TIMEOUT => 15,
|
|
||||||
CURLOPT_USERAGENT => $ua,
|
|
||||||
CURLOPT_HTTPHEADER => ['Accept: */*'],
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
]);
|
|
||||||
$body = curl_exec($ch);
|
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($body === false || $code >= 400) {
|
if ($code >= 400) { http_response_code($code); die("Upstream error $code"); }
|
||||||
http_response_code($code ?: 502);
|
|
||||||
die("Upstream error $code");
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/vnd.apple.mpegurl');
|
header('Content-Type: application/vnd.apple.mpegurl');
|
||||||
|
|
||||||
|
$final_parts = parse_url($final_url);
|
||||||
|
$origin = $final_parts['scheme'] . '://' . $final_parts['host'];
|
||||||
|
if (!empty($final_parts['port'])) $origin .= ':' . $final_parts['port'];
|
||||||
|
$base = preg_replace('#[^/]*(\?.*)?$#', '', $final_url);
|
||||||
|
|
||||||
$proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http')
|
$proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http')
|
||||||
. '://' . $_SERVER['HTTP_HOST']
|
. '://' . $_SERVER['HTTP_HOST']
|
||||||
. strtok($_SERVER['REQUEST_URI'], '?')
|
. strtok($_SERVER['REQUEST_URI'], '?')
|
||||||
@@ -85,7 +164,7 @@ if ($is_segment) {
|
|||||||
foreach (explode("\n", $body) as $line) {
|
foreach (explode("\n", $body) as $line) {
|
||||||
$line = rtrim($line);
|
$line = rtrim($line);
|
||||||
if ($line === '' || $line[0] === '#') {
|
if ($line === '' || $line[0] === '#') {
|
||||||
$line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) {
|
$line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $origin, $proxy_base) {
|
||||||
$seg = strpos($m[1], 'http') === 0 ? $m[1]
|
$seg = strpos($m[1], 'http') === 0 ? $m[1]
|
||||||
: ($m[1][0] === '/' ? $origin . $m[1] : $base . $m[1]);
|
: ($m[1][0] === '/' ? $origin . $m[1] : $base . $m[1]);
|
||||||
return 'URI="' . $proxy_base . urlencode($seg) . '"';
|
return 'URI="' . $proxy_base . urlencode($seg) . '"';
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ $submitted_key = trim($_POST['admin_key_input'] ?? $_POST['admin_key_hidden']
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>GridTV — Setup</title>
|
<title>GridTV — Setup</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;400;600;700&display=swap" rel="stylesheet">
|
<link rel="stylesheet" href="/assets/fonts/fonts.css">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg:#0a0b0d;--surface:#111318;--surface2:#181b22;
|
--bg:#0a0b0d;--surface:#111318;--surface2:#181b22;
|
||||||
|
|||||||
+2
-2
@@ -39,12 +39,12 @@ function renderMobile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeCol = document.createElement('div'); timeCol.className='mobile-program-time';
|
const timeCol = document.createElement('div'); timeCol.className='mobile-program-time';
|
||||||
timeCol.innerHTML = `<span>${fmtTime(p.start)}</span><span>${fmtTime(p.stop)}</span>`;
|
timeCol.innerHTML = `<span>${esc(fmtTime(p.start))}</span><span>${esc(fmtTime(p.stop))}</span>`;
|
||||||
item.appendChild(timeCol);
|
item.appendChild(timeCol);
|
||||||
|
|
||||||
const info = document.createElement('div'); info.className='mobile-program-info';
|
const info = document.createElement('div'); info.className='mobile-program-info';
|
||||||
const ep = fmtEpisode(p.season, p.episode);
|
const ep = fmtEpisode(p.season, p.episode);
|
||||||
info.innerHTML = `<div class="mobile-program-title">${p.title}</div><div class="mobile-program-dur">${ep ? ep + ' · ' : ''}${dur} min</div>`;
|
info.innerHTML = `<div class="mobile-program-title">${esc(p.title)}</div><div class="mobile-program-dur">${ep ? esc(ep) + ' · ' : ''}${dur} min</div>`;
|
||||||
item.appendChild(info);
|
item.appendChild(info);
|
||||||
|
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
|
|||||||
+2
-2
@@ -179,12 +179,12 @@ function renderMobileFiltered(filteredChannels, q) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeCol = document.createElement('div'); timeCol.className = 'mobile-program-time';
|
const timeCol = document.createElement('div'); timeCol.className = 'mobile-program-time';
|
||||||
timeCol.innerHTML = `<span>${fmtTime(p.start)}</span><span>${fmtTime(p.stop)}</span>`;
|
timeCol.innerHTML = `<span>${esc(fmtTime(p.start))}</span><span>${esc(fmtTime(p.stop))}</span>`;
|
||||||
item.appendChild(timeCol);
|
item.appendChild(timeCol);
|
||||||
|
|
||||||
const info = document.createElement('div'); info.className = 'mobile-program-info';
|
const info = document.createElement('div'); info.className = 'mobile-program-info';
|
||||||
const ep = fmtEpisode(p.season, p.episode);
|
const ep = fmtEpisode(p.season, p.episode);
|
||||||
info.innerHTML = `<div class="mobile-program-title">${p.title}</div><div class="mobile-program-dur">${ep ? ep + ' · ' : ''}${dur} min</div>`;
|
info.innerHTML = `<div class="mobile-program-title">${esc(p.title)}</div><div class="mobile-program-dur">${ep ? esc(ep) + ' · ' : ''}${dur} min</div>`;
|
||||||
item.appendChild(info);
|
item.appendChild(info);
|
||||||
|
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
|
||||||
|
// 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 channels = [];
|
||||||
let programs = {};
|
let programs = {};
|
||||||
let m3uStreams = {}; // slug normalisé → url stream
|
let m3uStreams = {}; // slug normalisé → url stream
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js"></script>
|
<script src="/assets/vendor/hls.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
<?php
|
<?php
|
||||||
$js_dir = __DIR__ . '/../js/';
|
$js_dir = __DIR__ . '/../js/';
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="GridTV">
|
<meta name="apple-mobile-web-app-title" content="GridTV">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;400;600;700&display=swap" rel="stylesheet">
|
<link rel="stylesheet" href="/assets/fonts/fonts.css">
|
||||||
<?php
|
<?php
|
||||||
$css_dir = __DIR__ . '/../css/';
|
$css_dir = __DIR__ . '/../css/';
|
||||||
$css_files = ['base', 'topbar', 'grid', 'mobile', 'player', 'modals', 'search', 'program', 'favorites', 'updater'];
|
$css_files = ['base', 'topbar', 'grid', 'mobile', 'player', 'modals', 'search', 'program', 'favorites', 'updater'];
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
const CACHE_NAME = 'gridtv-v1';
|
const CACHE_NAME = 'gridtv-v2';
|
||||||
|
|
||||||
// Ressources statiques à mettre en cache immédiatement
|
// Ressources statiques à mettre en cache immédiatement
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.php',
|
'/index.php',
|
||||||
'/manifest.json',
|
'/manifest.json',
|
||||||
|
'/assets/fonts/fonts.css',
|
||||||
'/assets/icon-192.png',
|
'/assets/icon-192.png',
|
||||||
'/assets/icon-512.png',
|
'/assets/icon-512.png',
|
||||||
'https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;400;600;700&display=swap',
|
'/assets/vendor/hls.min.js'
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Install : mise en cache des assets statiques
|
// Install : mise en cache des assets statiques
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* Néons rose/cyan sur noir absolu. Blade Runner.
|
* Néons rose/cyan sur noir absolu. Blade Runner.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #000008;
|
--bg: #000008;
|
||||||
--surface: #080010;
|
--surface: #080010;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* Salle de régie sombre, la base.
|
* Salle de régie sombre, la base.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #0a0b0d;
|
--bg: #0a0b0d;
|
||||||
--surface: #111318;
|
--surface: #111318;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* Papier jauni, encre rouge et bleue, machine à écrire.
|
* Papier jauni, encre rouge et bleue, machine à écrire.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Special+Elite&display=swap');
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #f2e8c8;
|
--bg: #f2e8c8;
|
||||||
--surface: #ede0b0;
|
--surface: #ede0b0;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
* Brun chaud, cuivre, laiton. Très 1880.
|
* Brun chaud, cuivre, laiton. Très 1880.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&display=swap');
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #160e04;
|
--bg: #160e04;
|
||||||
--surface: #201408;
|
--surface: #201408;
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "1.4.0"
|
"version": "1.4.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user