From bc1a6cb4d38f0be36a893f07302bdcd38a369c2e Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Tue, 10 Mar 2026 10:55:17 +0100 Subject: [PATCH 01/11] test deploy From 6f9819da42bec9747a907f116dd7b84dd817a076 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Tue, 10 Mar 2026 11:02:07 +0100 Subject: [PATCH 02/11] test deploy From 34ff10ecf4768c815228b2b3c8d79849c91f9a3f Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 15:15:19 +0100 Subject: [PATCH 03/11] remove auto deploy --- .github/workflows/deploy.yml | 91 ------------------------------------ 1 file changed, 91 deletions(-) delete mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 5df89f0..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Deploy to server - -on: - push: - branches: - - main - - beta - -jobs: - - deploy-main: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - - steps: - - name: Deploy production + demo - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - key: ${{ secrets.SERVER_SSH_KEY }} - script: | - - deploy_force () { - TARGET=$1 - BRANCH=$2 - - echo "------ Deploying $BRANCH to $TARGET ------" - - mkdir -p "$TARGET" - cd "$TARGET" - - # Si le repo est cassé ou absent → on reclone - if [ ! -d ".git" ]; then - echo "Repo absent ou cassé → reclone" - rm -rf "$TARGET"/* - git clone https://github.com/Johnnybegood90/GridTV.git . - fi - - # Force la branche - git fetch origin - - git checkout -B $BRANCH origin/$BRANCH - - git reset --hard origin/$BRANCH - git clean -fd - - echo "Deploy terminé" - } - - deploy_force "${{ secrets.DEPLOY_PATH }}" main - deploy_force "${{ secrets.DEMO_DEPLOY_PATH }}" main - - - deploy-beta: - if: github.ref == 'refs/heads/beta' - runs-on: ubuntu-latest - - steps: - - name: Deploy beta - uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - key: ${{ secrets.SERVER_SSH_KEY }} - script: | - - deploy_force () { - TARGET=$1 - BRANCH=$2 - - echo "------ Deploying $BRANCH to $TARGET ------" - - mkdir -p "$TARGET" - cd "$TARGET" - - if [ ! -d ".git" ]; then - echo "Repo absent ou cassé → reclone" - rm -rf "$TARGET"/* - git clone https://github.com/Johnnybegood90/GridTV.git . - fi - - git fetch origin - git checkout -B $BRANCH origin/$BRANCH - git reset --hard origin/$BRANCH - git clean -fd - - echo "Deploy terminé" - } - - deploy_force "${{ secrets.BETA_DEPLOY_PATH }}" beta From ed6892a8ae2a6444b72874e3786d248a88d479ef Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:19:24 +0100 Subject: [PATCH 04/11] Correction du player. --- index.php | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- proxy.php | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 proxy.php diff --git a/index.php b/index.php index fb97ee1..176a5cb 100644 --- a/index.php +++ b/index.php @@ -631,6 +631,86 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? ""); #navBtns { display: none !important; } } + + /* ═══════════════════════════════════════════ + PLAYER PiP + ═══════════════════════════════════════════ */ + .pip { + position: fixed; + bottom: 24px; + right: 24px; + width: 380px; + background: #000; + border: 1px solid var(--border-bright); + box-shadow: 0 8px 32px rgba(0,0,0,0.8); + z-index: 2000; + display: none; + flex-direction: column; + } + .pip.visible { display: flex; } + .pip-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + background: var(--surface); + border-bottom: 1px solid var(--border); + gap: 8px; + flex-shrink: 0; + } + .pip-channel { + font-family: var(--font-mono); + font-size: 11px; + color: var(--accent); + letter-spacing: 0.08em; + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + } + .pip-program { + font-size: 11px; + color: var(--text-dim); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 2; + text-align: center; + } + .pip-close { + background: none; + border: none; + color: var(--text-dim); + font-size: 18px; + cursor: pointer; + padding: 0 4px; + line-height: 1; + flex-shrink: 0; + transition: color 0.15s; + } + .pip-close:hover { color: var(--accent-live); } + .pip video { + width: 100%; + aspect-ratio: 16/9; + display: block; + background: #000; + } + .pip-error { + padding: 20px; + text-align: center; + font-family: var(--font-mono); + font-size: 11px; + color: var(--accent-live); + letter-spacing: 0.06em; + } + .channel-cell { cursor: pointer; } + .channel-cell:hover { background: var(--surface2); } + .program-block.is-live { cursor: pointer; } + @media (max-width: 600px) { + .pip { width: calc(100vw - 16px); bottom: 8px; right: 8px; left: 8px; } + } + @@ -1125,12 +1205,21 @@ function parseM3U(text) { function getStreamUrl(channelName) { const slug = slugify(channelName); - if (m3uStreams[slug]) return m3uStreams[slug]; - // Fallback : chercher une clé qui contient le slug ou l'inverse - for (const [key, url] of Object.entries(m3uStreams)) { - if (key.includes(slug) || slug.includes(key)) return url; + let url = null; + if (m3uStreams[slug]) { + url = m3uStreams[slug]; + } else { + // Fallback : chercher une clé qui contient le slug ou l'inverse + for (const [key, u] of Object.entries(m3uStreams)) { + if (key.includes(slug) || slug.includes(key)) { url = u; break; } + } } - return null; + if (!url) return null; + // Passer par proxy.php pour éviter le mixed content HTTP/HTTPS + if (url.startsWith('http://')) { + return 'proxy.php?url=' + encodeURIComponent(url); + } + return url; } // ── PLAYER PiP ──────────────────────────────────────────────────────────────── diff --git a/proxy.php b/proxy.php new file mode 100644 index 0000000..c36eed0 --- /dev/null +++ b/proxy.php @@ -0,0 +1,84 @@ + [ + 'method' => 'GET', + 'timeout' => 15, + 'header' => "User-Agent: GridTV-Proxy/1.0\r\nAccept: */*", + 'follow_location' => 1, +]]; +$body = @file_get_contents($url, false, stream_context_create($opts)); + +if ($body === false) { + http_response_code(502); die('Cannot reach upstream'); +} + +// Détecter si c'est une playlist M3U8 +$is_m3u8 = stripos($url, '.m3u8') !== false + || strpos($body, '#EXTM3U') === 0 + || strpos($body, '#EXT-X-') !== false; + +header('Access-Control-Allow-Origin: *'); +header('Cache-Control: no-cache'); + +if ($is_m3u8) { + header('Content-Type: application/vnd.apple.mpegurl'); + // Réécrire chaque URL de segment/playlist pour passer par ce proxy + $proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http') + . '://' . $_SERVER['HTTP_HOST'] + . strtok($_SERVER['REQUEST_URI'], '?') + . '?url='; + + $lines = explode("\n", $body); + $out = []; + foreach ($lines as $line) { + $line = rtrim($line); + if ($line === '' || $line[0] === '#') { + // Réécrire les URIs dans les tags comme #EXT-X-MAP:URI="..." + $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) { + $seg_url = strpos($m[1], 'http') === 0 ? $m[1] : $base . $m[1]; + return 'URI="' . $proxy_base . urlencode($seg_url) . '"'; + }, $line); + $out[] = $line; + } else { + // Ligne d'URL : segment .ts ou sous-playlist .m3u8 + $seg_url = strpos($line, 'http') === 0 ? $line : $base . $line; + $out[] = $proxy_base . urlencode($seg_url); + } + } + echo implode("\n", $out); +} else { + // Segment binaire (.ts, .aac, etc.) — streamer direct + // Récupérer le Content-Type depuis les headers de réponse + $ct = 'video/MP2T'; + foreach ($http_response_header ?? [] as $h) { + if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) { + $ct = trim($m[1]); break; + } + } + header('Content-Type: ' . $ct); + echo $body; +} From 91ef9d428f881f6336c7bc930fdb664c5afbb570 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:33:05 +0100 Subject: [PATCH 05/11] Correction du player. --- index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.php b/index.php index 176a5cb..07e3da0 100644 --- a/index.php +++ b/index.php @@ -630,7 +630,6 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? ""); .tooltip { display: none !important; } #navBtns { display: none !important; } } - /* ═══════════════════════════════════════════ PLAYER PiP @@ -711,6 +710,7 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? ""); .pip { width: calc(100vw - 16px); bottom: 8px; right: 8px; left: 8px; } } + From df58fd88bfec12b5d454bfbcecb7b3bbcb52e55d Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:37:12 +0100 Subject: [PATCH 06/11] Correction du proxy. --- proxy.php | 66 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/proxy.php b/proxy.php index c36eed0..b7419d9 100644 --- a/proxy.php +++ b/proxy.php @@ -11,32 +11,50 @@ if (empty($url) || !preg_match('#^https?://#i', $url)) { http_response_code(400); die('Invalid URL'); } -// Sécurité : hôtes privés/locaux uniquement -$host = parse_url($url, PHP_URL_HOST); -$ip = gethostbyname($host); -$is_private = !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); -$is_local = in_array($host, ['localhost', '127.0.0.1', '::1']); -if (!$is_private && !$is_local) { - http_response_code(403); die('Only local/private hosts allowed'); +$base = preg_replace('#[^/]*(\?.*)?$#', '', $url); + +// Transmettre les headers du navigateur à Tunarr +$forward_headers = []; +foreach (['HTTP_ACCEPT', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING', 'HTTP_COOKIE'] as $key) { + if (!empty($_SERVER[$key])) { + $name = str_replace('_', '-', substr($key, 5)); + $name = ucwords(strtolower($name), '-'); + $forward_headers[] = "$name: {$_SERVER[$key]}"; + } } +$forward_headers[] = 'User-Agent: ' . ($_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0'); +$forward_headers[] = 'Origin: http://' . parse_url($url, PHP_URL_HOST); +$forward_headers[] = 'Referer: ' . $url; -// Base URL pour réécrire les segments relatifs -$base = preg_replace('#[^/]*$#', '', $url); // tout sauf le dernier segment - -// Fetch $opts = ['http' => [ - 'method' => 'GET', - 'timeout' => 15, - 'header' => "User-Agent: GridTV-Proxy/1.0\r\nAccept: */*", + 'method' => 'GET', + 'timeout' => 15, + 'header' => implode("\r\n", $forward_headers), 'follow_location' => 1, + 'ignore_errors' => true, ]]; -$body = @file_get_contents($url, false, stream_context_create($opts)); -if ($body === false) { - http_response_code(502); die('Cannot reach upstream'); +$ctx = stream_context_create($opts); +$body = @file_get_contents($url, false, $ctx); + +// Lire le vrai status HTTP +$status_line = $http_response_header[0] ?? 'HTTP/1.1 502 Bad Gateway'; +preg_match('#HTTP/\S+\s+(\d+)#', $status_line, $sm); +$status_code = (int)($sm[1] ?? 502); + +if ($body === false || $status_code >= 400) { + http_response_code($status_code ?: 502); + die("Upstream error $status_code for: $url"); +} + +// Content-Type de la réponse distante +$ct = 'application/octet-stream'; +foreach ($http_response_header as $h) { + if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) { + $ct = trim($m[1]); break; + } } -// Détecter si c'est une playlist M3U8 $is_m3u8 = stripos($url, '.m3u8') !== false || strpos($body, '#EXTM3U') === 0 || strpos($body, '#EXT-X-') !== false; @@ -46,7 +64,7 @@ header('Cache-Control: no-cache'); if ($is_m3u8) { header('Content-Type: application/vnd.apple.mpegurl'); - // Réécrire chaque URL de segment/playlist pour passer par ce proxy + $proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?') @@ -57,28 +75,18 @@ if ($is_m3u8) { foreach ($lines as $line) { $line = rtrim($line); if ($line === '' || $line[0] === '#') { - // Réécrire les URIs dans les tags comme #EXT-X-MAP:URI="..." $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) { $seg_url = strpos($m[1], 'http') === 0 ? $m[1] : $base . $m[1]; return 'URI="' . $proxy_base . urlencode($seg_url) . '"'; }, $line); $out[] = $line; } else { - // Ligne d'URL : segment .ts ou sous-playlist .m3u8 $seg_url = strpos($line, 'http') === 0 ? $line : $base . $line; $out[] = $proxy_base . urlencode($seg_url); } } echo implode("\n", $out); } else { - // Segment binaire (.ts, .aac, etc.) — streamer direct - // Récupérer le Content-Type depuis les headers de réponse - $ct = 'video/MP2T'; - foreach ($http_response_header ?? [] as $h) { - if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) { - $ct = trim($m[1]); break; - } - } header('Content-Type: ' . $ct); echo $body; } From 886b42173c01a43cad73112ab9c4af89c91c4bae Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:41:40 +0100 Subject: [PATCH 07/11] Correction du player. --- index.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/index.php b/index.php index 07e3da0..3342197 100644 --- a/index.php +++ b/index.php @@ -1247,12 +1247,20 @@ function openPip(channelName, programTitle) { } if (Hls.isSupported()) { - hlsInstance = new Hls({ enableWorker: true }); + hlsInstance = new Hls({ enableWorker: false, debug: false }); hlsInstance.loadSource(url); hlsInstance.attachMedia(video); - hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {})); + hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { + console.log('[PiP] Manifest parsed, lecture...'); + video.play().catch(e => console.warn('[PiP] play() refusé:', e)); + }); hlsInstance.on(Hls.Events.ERROR, (e, data) => { - if (data.fatal) { err.style.display = 'block'; err.textContent = '⚠ Erreur de lecture du stream'; } + console.error('[PiP] HLS error:', data.type, data.details, data.fatal, data.response); + if (data.fatal) { + const detail = data.response ? ` (HTTP ${data.response.code})` : ` (${data.details})`; + err.style.display = 'block'; + err.textContent = '⚠ Erreur de lecture du stream' + detail; + } }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Safari natif HLS From a4e32c0f766fc26b8d017ace833b7a6cb0e125ba Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:44:30 +0100 Subject: [PATCH 08/11] Correction du proxy. --- proxy.php | 117 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/proxy.php b/proxy.php index b7419d9..da45bc0 100644 --- a/proxy.php +++ b/proxy.php @@ -1,68 +1,76 @@ [ - 'method' => 'GET', - 'timeout' => 15, - 'header' => implode("\r\n", $forward_headers), - 'follow_location' => 1, - 'ignore_errors' => true, -]]; - -$ctx = stream_context_create($opts); -$body = @file_get_contents($url, false, $ctx); - -// Lire le vrai status HTTP -$status_line = $http_response_header[0] ?? 'HTTP/1.1 502 Bad Gateway'; -preg_match('#HTTP/\S+\s+(\d+)#', $status_line, $sm); -$status_code = (int)($sm[1] ?? 502); - -if ($body === false || $status_code >= 400) { - http_response_code($status_code ?: 502); - die("Upstream error $status_code for: $url"); -} - -// Content-Type de la réponse distante -$ct = 'application/octet-stream'; -foreach ($http_response_header as $h) { - if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) { - $ct = trim($m[1]); break; - } -} - -$is_m3u8 = stripos($url, '.m3u8') !== false - || strpos($body, '#EXTM3U') === 0 - || strpos($body, '#EXT-X-') !== false; +$headers = [ + 'User-Agent: ' . ($_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0'), + 'Accept: */*', +]; header('Access-Control-Allow-Origin: *'); header('Cache-Control: no-cache'); -if ($is_m3u8) { +if ($is_segment) { + // Streaming direct chunk par chunk — pas de buffering mémoire + $opts = stream_context_create(['http' => [ + 'method' => 'GET', + 'header' => implode("\r\n", $headers), + 'follow_location' => 1, + 'timeout' => 30, + ]]); + + $handle = @fopen($url, 'rb', false, $opts); + if (!$handle) { http_response_code(502); die('Cannot open stream'); } + + // Récupérer le Content-Type de la réponse + $ct = 'video/MP2T'; + foreach (($http_response_header ?? []) as $h) { + if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) { $ct = trim($m[1]); break; } + } + + header('Content-Type: ' . $ct); + header('X-Accel-Buffering: no'); + + // Désactiver le buffering PHP + if (ob_get_level()) ob_end_clean(); + + while (!feof($handle)) { + $chunk = fread($handle, 65536); // 64KB par chunk + if ($chunk === false || $chunk === '') break; + echo $chunk; + flush(); + } + fclose($handle); + +} else { + // Playlist .m3u8 — fetch complet puis réécriture + $opts = stream_context_create(['http' => [ + 'method' => 'GET', + 'header' => implode("\r\n", $headers), + 'follow_location' => 1, + 'timeout' => 15, + 'ignore_errors' => true, + ]]); + + $body = @file_get_contents($url, false, $opts); + if ($body === false) { http_response_code(502); die('Cannot reach upstream'); } + + $status_line = $http_response_header[0] ?? 'HTTP/1.1 200 OK'; + preg_match('#HTTP/\S+\s+(\d+)#', $status_line, $sm); + if ((int)($sm[1] ?? 200) >= 400) { http_response_code((int)$sm[1]); die('Upstream error'); } + header('Content-Type: application/vnd.apple.mpegurl'); $proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http') @@ -76,17 +84,14 @@ if ($is_m3u8) { $line = rtrim($line); if ($line === '' || $line[0] === '#') { $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) { - $seg_url = strpos($m[1], 'http') === 0 ? $m[1] : $base . $m[1]; - return 'URI="' . $proxy_base . urlencode($seg_url) . '"'; + $seg = strpos($m[1], 'http') === 0 ? $m[1] : $base . $m[1]; + return 'URI="' . $proxy_base . urlencode($seg) . '"'; }, $line); $out[] = $line; } else { - $seg_url = strpos($line, 'http') === 0 ? $line : $base . $line; - $out[] = $proxy_base . urlencode($seg_url); + $seg = strpos($line, 'http') === 0 ? $line : $base . $line; + $out[] = $proxy_base . urlencode($seg); } } echo implode("\n", $out); -} else { - header('Content-Type: ' . $ct); - echo $body; } From 2a74ba7cdf59bd4e3b58c0848779d875d279cc9f Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:47:46 +0100 Subject: [PATCH 09/11] Correction du proxy. Ajout de curl dans php. --- proxy.php | 103 +++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/proxy.php b/proxy.php index da45bc0..587d69a 100644 --- a/proxy.php +++ b/proxy.php @@ -1,8 +1,6 @@ [ - 'method' => 'GET', - 'header' => implode("\r\n", $headers), - 'follow_location' => 1, - 'timeout' => 30, - ]]); + // Segment binaire — stream via cURL chunk par chunk + header('Content-Type: video/MP2T'); + header('X-Content-Type-Options: nosniff'); - $handle = @fopen($url, 'rb', false, $opts); - if (!$handle) { http_response_code(502); die('Cannot open stream'); } - - // Récupérer le Content-Type de la réponse - $ct = 'video/MP2T'; - foreach (($http_response_header ?? []) as $h) { - if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) { $ct = trim($m[1]); break; } - } - - header('Content-Type: ' . $ct); - header('X-Accel-Buffering: no'); - - // Désactiver le buffering PHP if (ob_get_level()) ob_end_clean(); - - while (!feof($handle)) { - $chunk = fread($handle, 65536); // 64KB par chunk - if ($chunk === false || $chunk === '') break; - echo $chunk; - flush(); + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_USERAGENT => $ua, + CURLOPT_HTTPHEADER => ['Accept: */*'], + CURLOPT_RETURNTRANSFER => false, + CURLOPT_WRITEFUNCTION => function($ch, $data) { + echo $data; + flush(); + return strlen($data); + }, + CURLOPT_HEADERFUNCTION => function($ch, $header) { + $h = trim($header); + if (preg_match('/^Content-Type:/i', $h)) { + header($h); + } + return strlen($header); + }, + ]); + + $ok = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if (!$ok || $code >= 400) { + http_response_code($code ?: 502); } - fclose($handle); + curl_close($ch); } else { - // Playlist .m3u8 — fetch complet puis réécriture - $opts = stream_context_create(['http' => [ - 'method' => 'GET', - 'header' => implode("\r\n", $headers), - 'follow_location' => 1, - 'timeout' => 15, - 'ignore_errors' => true, - ]]); + // Playlist .m3u8 — fetch + réécriture URLs + $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); - $body = @file_get_contents($url, false, $opts); - if ($body === false) { http_response_code(502); die('Cannot reach upstream'); } - - $status_line = $http_response_header[0] ?? 'HTTP/1.1 200 OK'; - preg_match('#HTTP/\S+\s+(\d+)#', $status_line, $sm); - if ((int)($sm[1] ?? 200) >= 400) { http_response_code((int)$sm[1]); die('Upstream error'); } + if ($body === false || $code >= 400) { + http_response_code($code ?: 502); + die("Upstream error $code"); + } header('Content-Type: application/vnd.apple.mpegurl'); @@ -78,9 +78,8 @@ if ($is_segment) { . strtok($_SERVER['REQUEST_URI'], '?') . '?url='; - $lines = explode("\n", $body); - $out = []; - foreach ($lines as $line) { + $out = []; + foreach (explode("\n", $body) as $line) { $line = rtrim($line); if ($line === '' || $line[0] === '#') { $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) { From 0354b25f4713a8710a1bff07af21f4e2af8d30d1 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 18:56:50 +0100 Subject: [PATCH 10/11] Correction du proxy. Fix du parsing. --- proxy.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/proxy.php b/proxy.php index 587d69a..4e5610e 100644 --- a/proxy.php +++ b/proxy.php @@ -8,7 +8,10 @@ if (empty($url) || !preg_match('#^https?://#i', $url)) { http_response_code(400); die('Invalid URL'); } -$base = preg_replace('#[^/]*(\?.*)?$#', '', $url); +$base = preg_replace('#[^/]*(\?.*)?$#', '', $url); +$origin = parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST); +$port = parse_url($url, PHP_URL_PORT); +if ($port) $origin .= ':' . $port; $path = parse_url($url, PHP_URL_PATH) ?? ''; $is_segment = preg_match('#\.(ts|aac|mp4|m4s|fmp4)(\?|$)#i', $path); @@ -83,12 +86,14 @@ if ($is_segment) { $line = rtrim($line); if ($line === '' || $line[0] === '#') { $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) { - $seg = strpos($m[1], 'http') === 0 ? $m[1] : $base . $m[1]; + $seg = strpos($m[1], 'http') === 0 ? $m[1] + : ($m[1][0] === '/' ? $origin . $m[1] : $base . $m[1]); return 'URI="' . $proxy_base . urlencode($seg) . '"'; }, $line); $out[] = $line; } else { - $seg = strpos($line, 'http') === 0 ? $line : $base . $line; + $seg = strpos($line, 'http') === 0 ? $line + : ($line[0] === '/' ? $origin . $line : $base . $line); $out[] = $proxy_base . urlencode($seg); } } From ea8edb1b804e712b689e3f85541ae5ef9b8d6ee4 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Wed, 11 Mar 2026 19:00:45 +0100 Subject: [PATCH 11/11] =?UTF-8?q?Mise=20=C3=A0=20jour=20du=20README.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b2e195b..2cc9a07 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ > A real-time IPTV TV guide, built for [Tunarr](https://github.com/chrisbenincasa/tunarr) and any XMLTV/M3U source. ![PHP](https://img.shields.io/badge/PHP-8.0+-777BB4?style=flat-square&logo=php&logoColor=white) +![Apache](https://img.shields.io/badge/Apache-ready-D22128?style=flat-square&logo=apache&logoColor=white) ![Nginx](https://img.shields.io/badge/Nginx-ready-009639?style=flat-square&logo=nginx&logoColor=white) ![License](https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square) @@ -30,9 +31,12 @@ This demo runs with a sample XMLTV feed and fake channels to showcase the interf - 🕶️ **Past programs** automatically dimmed - 📋 **Hover tooltip** — title, synopsis, season/episode, schedule, duration - 🔗 **One-click copy** buttons for EPG & M3U URLs in the topbar +- ▶️ **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 - ⚙️ **Guided setup** on first launch — no config files to edit manually - 🔄 **Auto-reload** EPG every 30 minutes -- 0️⃣ **Zero dependencies** — vanilla PHP + nginx, that's it +- 0️⃣ **Zero JS dependencies** — vanilla PHP, hls.js loaded from CDN --- @@ -40,10 +44,10 @@ This demo runs with a sample XMLTV feed and fake channels to showcase the interf ### Requirements -- A web server running **PHP 8.0+** with **php-fpm** -- **Nginx** (or Apache) +- A web server running **PHP 8.0+** with **php-curl** extension +- **Apache** or **Nginx** - An **EPG source in XMLTV format** (e.g. Tunarr, Jellyfin, xTeVe...) -- *(Optional)* An **M3U playlist** +- *(Optional)* An **M3U playlist** — required for the built-in player --- @@ -59,14 +63,13 @@ cd /var/www/gridtv ### 2. Set permissions ```bash -# The web server needs write access to create config.json during setup chmod 775 /var/www/gridtv chown -R www-data:www-data /var/www/gridtv ``` --- -### 3. Configure Nginx +### 3a. Configure Nginx ```nginx server { @@ -95,7 +98,42 @@ nginx -t && systemctl reload nginx --- -### 4. First launch — Setup +### 3b. Configure Apache + +```apache + + ServerName guide.your-domain.com + DocumentRoot /var/www/gridtv + + + AllowOverride All + Require all granted + + +``` + +```bash +a2enmod php8.4 rewrite +systemctl reload apache2 +``` + +--- + +### 4. 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 + +# Then reload the web server +systemctl reload apache2 # or: systemctl reload nginx +``` + +--- + +### 5. First launch — Setup Open your browser at `http://guide.your-domain.com`. @@ -105,16 +143,15 @@ GridTV detects the missing config and automatically redirects you to the setup p |---|---| | **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, for quick copy from the topbar | +| **M3U URL** *(optional)* | Your IPTV playlist — used by the built-in player to match channels to streams | Once submitted, `config.json` is created on the server. **The setup page becomes inaccessible.** --- -### 5. Editing the config later +### 6. Editing the config later ```bash -# Via SSH nano /var/www/gridtv/config.json ``` @@ -134,8 +171,14 @@ nano /var/www/gridtv/config.json gridtv/ ├── index.php # The TV guide (redirects to setup if no config found) ├── setup.php # First-launch configuration page -├── config.example.json # Config template (copy to config.json and fill in) +├── proxy.php # HTTP→HTTPS stream proxy (required for the player) +├── config.example.json # Config template ├── .gitignore # config.json excluded from the repo +├── themes/ # CSS theme files +│ ├── default.css # Dark studio (default) +│ ├── magazine.css # Vintage newspaper +│ ├── cyberpunk.css # Neon on black +│ └── steampunk.css # Victorian copper └── README.md ``` @@ -143,15 +186,47 @@ gridtv/ --- +## 🎨 Themes + +GridTV ships with 4 built-in themes. To add your own, create a CSS file in `themes/` with these metadata comments at the top: + +```css +/* + * @name My Theme + * @emoji 🌙 + */ + +:root { + --bg: #0a0b0d; + --accent: #e8c842; + /* ... */ +} +``` + +Drop it in `themes/` — it appears in the theme selector automatically, no code changes needed. + +--- + +## ▶️ 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) +- 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. + +--- + ## ⚙️ Advanced configuration The following constants can be tweaked directly in `index.php`: ```js -const PX_PER_MIN = 5; // Horizontal zoom (pixels per minute) -const HOURS_BEFORE = 1; // Past hours visible in the grid -const HOURS_AFTER = 3; // Future hours visible in the grid -const ROW_H = 80; // Channel row height (px) +const PX_PER_MIN = 5; // Horizontal zoom (pixels per minute) +const GRID_HOURS = 72; // Total timeline duration (hours) +const ROW_H = 80; // Channel row height (px) ``` ---