From 8dabd2d1901d3633a3f1c9571591b8fd19f9adb9 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Sat, 14 Mar 2026 04:23:44 +0100 Subject: [PATCH] Adjusting comments into files. --- proxy.php | 33 +++++++++++++++++---------------- setup.php | 4 ++-- src/config.php | 4 ++-- src/css/base.css | 2 +- src/css/grid.css | 2 +- src/css/program.css | 2 +- src/css/search.css | 2 +- src/css/topbar.css | 3 +-- src/js/config.js | 4 ++-- src/js/epg.js | 3 +-- src/js/m3u.js | 11 +++++------ src/js/player.js | 5 ++--- src/js/program.js | 8 ++++---- src/js/search.js | 16 ++++++++-------- src/js/sources.js | 8 ++++---- src/js/themes.js | 3 +-- src/js/utils.js | 13 +++++++------ src/tpl/topbar.php | 5 ++--- sw.js | 14 +++++++------- 19 files changed, 69 insertions(+), 73 deletions(-) diff --git a/proxy.php b/proxy.php index da1d2da..a87bebb 100644 --- a/proxy.php +++ b/proxy.php @@ -2,12 +2,12 @@ /** * 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. + * HTTP->HTTPS proxy restricted to hosts explicitly configured in config.json. + * Private and LAN IPs are allowed only when the administrator configured them. + * Redirects are followed manually and the host is revalidated at every hop. */ -// ── Whitelist : hotes autorises extraits de config.json ─────────────────────── +// ── Allowlist: authorized hosts extracted from config.json ──────────────────── $config_path = __DIR__ . '/config.json'; $allowed_hosts = []; @@ -23,7 +23,7 @@ if (file_exists($config_path)) { } } -// ── Fonctions ───────────────────────────────────────────────────────────────── +// ── Helpers ──────────────────────────────────────────────────────────────────── function is_allowed_url(string $url, array $allowed_hosts): bool { if (!preg_match('#^https?://#i', $url)) return false; @@ -41,9 +41,10 @@ function resolve_url(string $base, string $location): string { } /** - * 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) + * Fetch with manual redirect handling so every Location header is checked + * against the allowlist before following it. + * $stream = true: stream the response chunk by chunk (video segments) + * $stream = false: return the full body (m3u8 playlists) */ function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $stream = false): array { $max_redirects = 5; @@ -76,7 +77,7 @@ function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $s $raw_headers = substr($response, 0, $header_size); $body = substr($response, $header_size); - // Redirection + // Follow redirects manually so each hop is rechecked against the allowlist. if ($code >= 300 && $code < 400) { if (!preg_match('/^Location:\s*(.+)$/mi', $raw_headers, $m)) { http_response_code(502); die('Invalid redirect'); @@ -91,7 +92,7 @@ function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $s http_response_code(508); die('Too many redirects'); } -// ── Valider l'URL initiale ───────────────────────────────────────────────────── +// ── Validate the initial URL ─────────────────────────────────────────────────── $url = $_GET['url'] ?? ''; if (!is_allowed_url($url, $allowed_hosts)) { @@ -99,27 +100,27 @@ if (!is_allowed_url($url, $allowed_hosts)) { die(empty($allowed_hosts) ? 'No sources configured' : 'Host not allowed'); } -// ── Determiner le type de ressource ─────────────────────────────────────────── +// ── Detect the resource type ─────────────────────────────────────────────────── $path = parse_url($url, PHP_URL_PATH) ?? ''; $is_segment = preg_match('#\.(ts|aac|mp4|m4s|fmp4)(\?|$)#i', $path); header('Access-Control-Allow-Origin: *'); header('Cache-Control: no-cache'); -// ── Segment binaire — stream chunk par chunk ─────────────────────────────────── +// ── Binary segment: stream chunk by chunk ────────────────────────────────────── if ($is_segment) { header('Content-Type: video/MP2T'); header('X-Content-Type-Options: nosniff'); if (ob_get_level()) ob_end_clean(); - // 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 + // For segments, resolve redirects first and only stream once the final URL + // has already been validated. [$code, , , $final_url] = fetch_with_checked_redirects($url, $allowed_hosts, false); if ($code >= 400) { http_response_code($code); die(); } - // Maintenant streamer l'URL finale + // Stream the validated final URL. $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Mozilla/5.0'; $ch = curl_init($final_url); curl_setopt_array($ch, [ @@ -142,7 +143,7 @@ if ($is_segment) { if (!$ok || $code >= 400) http_response_code($code ?: 502); curl_close($ch); -// ── Playlist m3u8 — fetch + réécriture URLs ──────────────────────────────────── +// ── m3u8 playlist: fetch and rewrite URLs ────────────────────────────────────── } else { [$code, , $body, $final_url] = fetch_with_checked_redirects($url, $allowed_hosts, false); diff --git a/setup.php b/setup.php index a4492fc..fd96b3b 100644 --- a/setup.php +++ b/setup.php @@ -1,7 +1,7 @@ on en genere une maintenant + // Bootstrap an admin key for older configs that do not have one yet. if (empty($existing['admin_key'])) { $existing['admin_key'] = bin2hex(random_bytes(16)); file_put_contents($config_path, json_encode($existing, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); diff --git a/src/config.php b/src/config.php index 393a8da..d5f9fed 100644 --- a/src/config.php +++ b/src/config.php @@ -9,7 +9,7 @@ if (!file_exists($config_path)) { $config = json_decode(file_get_contents($config_path), true); -// ── Migration automatique ancien format → nouveau ───────────────────────────── +// ── Automatically migrate the legacy config format to the current one ───────── if (isset($config['epg_url']) && !isset($config['epg_sources'])) { $config['epg_sources'] = [[ 'name' => 'Main', @@ -26,7 +26,7 @@ $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) +// Default to the first configured source to keep the rest of the app compatible. $first_source = $epg_sources[0] ?? []; $epg_url = htmlspecialchars($first_source['epg_url'] ?? ''); $m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); diff --git a/src/css/base.css b/src/css/base.css index d8e9188..7b5c1ba 100644 --- a/src/css/base.css +++ b/src/css/base.css @@ -1,4 +1,4 @@ -/* Variables de base — surchargées par le fichier thème */ +/* Base variables overridden by the active theme file */ :root { color-scheme: dark; --bg: #0a0b0d; diff --git a/src/css/grid.css b/src/css/grid.css index b5199ef..5e0f649 100644 --- a/src/css/grid.css +++ b/src/css/grid.css @@ -97,7 +97,7 @@ .timeline-area::-webkit-scrollbar-track { background: transparent; } .timeline-area::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; } - /* Conteneur unique — ruler + programmes scrollent ensemble */ + /* Single container so the ruler and programs scroll together */ .timeline-inner { position: relative; display: block; diff --git a/src/css/program.css b/src/css/program.css index 6c046b4..2c19fb8 100644 --- a/src/css/program.css +++ b/src/css/program.css @@ -39,7 +39,7 @@ .pm-imdb { background: #f5c518; color: #000; } .pm-cancel { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); } - /* Date midnight dans la timebar */ + /* Midnight date marker in the timebar */ .time-tick-label.midnight { color: var(--accent); font-weight: 700; font-size: 11px; white-space: nowrap; diff --git a/src/css/search.css b/src/css/search.css index e76214d..bcba8e1 100644 --- a/src/css/search.css +++ b/src/css/search.css @@ -29,7 +29,7 @@ } .search-clear:hover { color: var(--accent); } - /* Highlight programmes qui matchent */ + /* Highlight programs that match the current query */ .program-block.search-match { border: 1px solid var(--accent); box-shadow: 0 0 6px var(--accent); diff --git a/src/css/topbar.css b/src/css/topbar.css index aec1395..aab55fb 100644 --- a/src/css/topbar.css +++ b/src/css/topbar.css @@ -128,7 +128,7 @@ } /* ═══════════════════════════════════════════ - SÉLECTEUR DE THÈME + THEME SELECTOR ═══════════════════════════════════════════ */ .theme-select { background: transparent; @@ -222,4 +222,3 @@ .source-dropdown-item:hover { background: var(--surface2); color: var(--accent2); } .source-dropdown-item.active { color: var(--accent); } .source-dropdown-personal { color: var(--accent2); } - diff --git a/src/js/config.js b/src/js/config.js index aac6ca6..3149533 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -2,7 +2,7 @@ const EPG_SOURCES = ; const L = ; const ALLOW_PERSONAL_EPG = ; -// Source active (index) +// Index of the currently selected source. let activeSourceIndex = 0; const DEFAULT_EPG_URL = EPG_SOURCES[0]?.epg_url ?? ''; @@ -11,7 +11,7 @@ const PX_PER_MIN = 5; const GRID_HOURS = 72; const CORS_PROXY = 'https://corsproxy.io/?'; -// Ancre : 2h avant maintenant, arrondi au quart d'heure inférieur +// Anchor the grid 2 hours before "now", rounded down to the previous quarter-hour. const _d = new Date(); _d.setMinutes(Math.floor(_d.getMinutes()/15)*15, 0, 0); const GRID_START = new Date(_d.getTime() - 2*3600000); diff --git a/src/js/epg.js b/src/js/epg.js index 3d30c5c..8c2fae5 100644 --- a/src/js/epg.js +++ b/src/js/epg.js @@ -73,7 +73,7 @@ function renderPrograms() { inner.style.width = TOTAL_WIDTH_PX+'px'; inner.style.height = totalH+'px'; - // Remettre la now-line (détruite par innerHTML='') + // Recreate the now-line because it was removed by innerHTML = ''. const nowLine = document.createElement('div'); nowLine.className = 'now-line'; nowLine.id = 'nowLine'; nowLine.style.height = totalH + 'px'; @@ -141,4 +141,3 @@ function shiftView(mins) { area.scrollLeft = Math.max(0, area.scrollLeft + mins * PX_PER_MIN); document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); } - diff --git a/src/js/m3u.js b/src/js/m3u.js index b73f606..b7d8dc5 100644 --- a/src/js/m3u.js +++ b/src/js/m3u.js @@ -1,8 +1,8 @@ // ── M3U PARSER ──────────────────────────────────────────────────────────────── function slugify(str) { return str.toLowerCase() - .replace(/^\d+\s*/, '') // virer le numéro de chaîne en début - .replace(/[^a-z0-9]/g, ''); // garder que alphanum + .replace(/^\d+\s*/, '') // Strip any leading channel number. + .replace(/[^a-z0-9]/g, ''); // Keep only alphanumeric characters. } async function loadM3U(url) { @@ -25,7 +25,7 @@ function parseM3U(text) { for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXTINF')) { - // Extraire le nom : dernière partie après la virgule + // Extract the display name from the last comma-separated segment. const comma = line.lastIndexOf(','); currentName = comma !== -1 ? line.slice(comma + 1).trim() : null; } else if (line && !line.startsWith('#') && currentName) { @@ -42,16 +42,15 @@ function getStreamUrl(channelName) { if (m3uStreams[slug]) { url = m3uStreams[slug]; } else { - // Fallback : chercher une clé qui contient le slug ou l'inverse + // Fallback: try a partial match in either direction. for (const [key, u] of Object.entries(m3uStreams)) { if (key.includes(slug) || slug.includes(key)) { url = u; break; } } } if (!url) return null; - // Passer par proxy.php pour éviter le mixed content HTTP/HTTPS + // Route plain HTTP streams through proxy.php to avoid mixed-content issues. if (url.startsWith('http://')) { return 'proxy.php?url=' + encodeURIComponent(url); } return url; } - diff --git a/src/js/player.js b/src/js/player.js index 1c9c68a..e8e9fff 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -12,7 +12,7 @@ function openPip(channelName, programTitle) { err.style.display = 'none'; pip.classList.add('visible'); - // Détruire l'instance HLS précédente + // Tear down the previous HLS instance before opening another stream. if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } video.src = ''; @@ -39,7 +39,7 @@ function openPip(channelName, programTitle) { } }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { - // Safari natif HLS + // Safari can play HLS natively without hls.js. video.src = url; video.addEventListener('loadedmetadata', () => video.play().catch(() => {})); } else { @@ -55,4 +55,3 @@ function closePip() { if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } video.pause(); video.src = ''; } - diff --git a/src/js/program.js b/src/js/program.js index 534ee5d..aa2dbe0 100644 --- a/src/js/program.js +++ b/src/js/program.js @@ -13,7 +13,7 @@ function openProgramModal(ch, p) { document.getElementById('pm-desc').textContent = p.desc || ''; document.getElementById('pm-desc').style.display = p.desc ? 'block' : 'none'; - // Bouton Watch now — seulement si live ET M3U fourni pour la source active + // Only show "Watch now" when the program is live and the active source has M3U data. const watchBtn = document.getElementById('pm-watch'); if (isLive && currentM3uUrl) { watchBtn.style.display = 'inline-flex'; @@ -22,7 +22,7 @@ function openProgramModal(ch, p) { watchBtn.style.display = 'none'; } - // Bouton IMDb + // IMDb search shortcut. document.getElementById('pm-imdb').href = `https://www.imdb.com/find/?q=${encodeURIComponent(p.title)}&s=tt`; @@ -33,13 +33,13 @@ function closeProgramModal() { document.getElementById('programModal').classList.remove('visible'); } -// Fermer sur clic overlay +// Close when the overlay itself is clicked. document.addEventListener('click', e => { const modal = document.getElementById('programModal'); if (e.target === modal) closeProgramModal(); }); -// Fermer sur Escape +// Close on Escape. document.addEventListener('keydown', e => { if (e.key === 'Escape') closeProgramModal(); }); diff --git a/src/js/search.js b/src/js/search.js index d22f70d..ff89e2d 100644 --- a/src/js/search.js +++ b/src/js/search.js @@ -31,7 +31,7 @@ function onSearchInput(val) { function applySearch(q) { if (!q) { - // Remettre toutes les chaines + // Restore the full channel list when the query is cleared. renderAll(); return; } @@ -40,11 +40,11 @@ function applySearch(q) { today.setHours(0,0,0,0); const tomorrow = new Date(today.getTime() + 24*3600000); - // Filtrer les chaines : nom OU programme aujourd'hui correspond + // Keep channels whose name matches or that have a matching program today. const filtered = channels.filter(ch => { - // Match sur le nom de la chaine + // Match against the channel name. if (ch.name.toLowerCase().includes(q)) return true; - // Match sur les programmes d'aujourd'hui (titre + desc) + // Match against today's programs (title + description). return (programs[ch.id]||[]).some(p => { if (p.stop < today || p.start > tomorrow) return false; if (p.title.toLowerCase().includes(q)) return true; @@ -72,7 +72,7 @@ function renderFiltered(filteredChannels, q) { inner.style.width = TOTAL_WIDTH_PX + 'px'; inner.style.height = (filteredChannels.length * ROW_H) + 'px'; - // Remettre la now-line + // Recreate the now-line after clearing the container. const nowLine = document.createElement('div'); nowLine.className = 'now-line'; nowLine.id = 'nowLine'; nowLine.style.height = (filteredChannels.length * ROW_H) + 'px'; @@ -81,7 +81,7 @@ function renderFiltered(filteredChannels, q) { const now = new Date(); filteredChannels.forEach((ch, i) => { - // Colonne chaîne + // Channel column. const cell = document.createElement('div'); cell.className = 'channel-cell'; if (ch.icon) { @@ -96,7 +96,7 @@ function renderFiltered(filteredChannels, q) { cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id))); list.appendChild(cell); - // Programmes + // Programs row. const row = document.createElement('div'); row.className = 'program-row'; row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`; @@ -109,7 +109,7 @@ function renderFiltered(filteredChannels, q) { const block = document.createElement('div'); block.className = 'program-block'; - // Highlight si le programme matche la recherche + // Highlight programs that match the current search query. const matchesProg = p.title.toLowerCase().includes(q) || (p.desc && p.desc.toLowerCase().includes(q)); if (matchesProg) block.classList.add('search-match'); diff --git a/src/js/sources.js b/src/js/sources.js index 2ceb967..08904ee 100644 --- a/src/js/sources.js +++ b/src/js/sources.js @@ -21,7 +21,7 @@ function switchSource(value) { } function updateCopyButtons() { - // Rien à faire visuellement, copyUrl() lit currentEpgUrl/currentM3uUrl + // No visual update is needed here because copyUrl() reads the current URLs directly. } function openPersonalEpgModal() { @@ -35,7 +35,7 @@ function openPersonalEpgModal() { function closePersonalEpgModal(applied) { document.getElementById('personalEpgModal').classList.remove('visible'); - // Si annulé (pas applied), remettre le select sur la source précédente + // If the modal was cancelled, restore the previously selected source. if (!applied) { const sel = document.getElementById('sourceSelect'); if (sel) sel.value = localStorage.getItem('gridtv-active-source') === 'personal' ? 'personal' : activeSourceIndex; @@ -67,7 +67,7 @@ function closeSourceDropdown() { document.getElementById('sourceDropdownMenu')?.classList.remove('open'); } -// Fermer si clic en dehors +// Close the dropdown when clicking outside of it. document.addEventListener('click', e => { if (!e.target.closest('.source-dropdown')) closeSourceDropdown(); }); @@ -75,7 +75,7 @@ document.addEventListener('click', e => { function updateSourceDropdownLabel(label) { const el = document.getElementById('sourceDropdownLabel'); if (el) el.textContent = label; - // Mettre à jour la classe active + // Update the active item styling in the dropdown. document.querySelectorAll('.source-dropdown-item').forEach((btn, i) => { btn.classList.toggle('active', i === activeSourceIndex); }); diff --git a/src/js/themes.js b/src/js/themes.js index 3fce918..e8506c5 100644 --- a/src/js/themes.js +++ b/src/js/themes.js @@ -1,4 +1,4 @@ -// ── THÈMES ──────────────────────────────────────────────────────────────────── +// ── THEMES ──────────────────────────────────────────────────────────────────── function setTheme(theme) { document.getElementById('themeStylesheet').href = 'themes/' + theme + '.css'; localStorage.setItem('gridtv-theme', theme); @@ -17,4 +17,3 @@ function startLiveUpdates() { setInterval(() => { if (channels.length) renderAll(); }, 60000); setInterval(() => loadEPG(DEFAULT_EPG_URL), 30*60*1000); } - diff --git a/src/js/utils.js b/src/js/utils.js index 6e8c324..4de08da 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -1,5 +1,5 @@ -// Echapper les donnees issues de l'EPG pour eviter les injections HTML +// Escape EPG-provided strings before injecting them into HTML. function esc(str) { const d = document.createElement('div'); d.textContent = str || ''; @@ -8,8 +8,8 @@ function esc(str) { let channels = []; let programs = {}; -let m3uStreams = {}; // slug normalisé → url stream -let scrollListenerAdded = false; // sync scroll vertical chaînes +let m3uStreams = {}; // Normalized slug -> stream URL. +let scrollListenerAdded = false; // Keeps the channel column vertically synced with the timeline. function pad(n) { return String(n).padStart(2,'0'); } @@ -35,6 +35,8 @@ function parseXMLTVDate(str) { if (!str) return null; const m = str.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\s*([+-]\d{4})?/); if (!m) return null; + // XMLTV timestamps use a compact timezone suffix like +0200, so we rebuild + // the UTC instant manually before letting Date convert it to local time. const offset = m[7] ? parseInt(m[7]) : 0; const offsetMs = (Math.floor(Math.abs(offset)/100)*60 + (Math.abs(offset)%100)) * 60000 * (offset<0?-1:1); return new Date(Date.UTC(+m[1],+m[2]-1,+m[3],+m[4],+m[5],+m[6]) - offsetMs); @@ -78,7 +80,7 @@ function loadFromInput() { } function parseEpisode(p) { - // Format xmltv_ns : "S.E.part" (0-indexed) ex: "0.2.0/1" = S01E03 + // xmltv_ns format: "S.E.part" (0-indexed), e.g. "0.2.0/1" -> S01E03 const ns = p.querySelector('episode-num[system="xmltv_ns"]')?.textContent; if (ns) { const parts = ns.split('.'); @@ -87,7 +89,7 @@ function parseEpisode(p) { const e = ePart !== '' ? parseInt(ePart) + 1 : null; if (s !== null || e !== null) return { season: s, episode: e }; } - // Format onscreen : "S01E03" ou "s1e3" + // onscreen format: "S01E03" or "s1e3" const os = p.querySelector('episode-num[system="onscreen"]')?.textContent; if (os) { const m = os.match(/[Ss](\d+)[Ee](\d+)/); @@ -136,4 +138,3 @@ function renderAll() { if (isMobile()) { renderMobile(); } else { renderGrid(); setTimeout(centerNow, 150); } } - diff --git a/src/tpl/topbar.php b/src/tpl/topbar.php index 5014075..43468b3 100644 --- a/src/tpl/topbar.php +++ b/src/tpl/topbar.php @@ -37,7 +37,7 @@ " oninput="onSearchInput(this.value)" /> - diff --git a/sw.js b/sw.js index 8e58b22..d0dc69b 100644 --- a/sw.js +++ b/sw.js @@ -1,6 +1,6 @@ const CACHE_NAME = 'gridtv-v2'; -// Ressources statiques à mettre en cache immédiatement +// Static assets cached during installation. const STATIC_ASSETS = [ '/', '/index.php', @@ -11,7 +11,7 @@ const STATIC_ASSETS = [ '/assets/vendor/hls.min.js' ]; -// Install : mise en cache des assets statiques +// Install: cache the static assets. self.addEventListener('install', e => { e.waitUntil( caches.open(CACHE_NAME) @@ -20,7 +20,7 @@ self.addEventListener('install', e => { ); }); -// Activate : purger les anciens caches +// Activate: remove outdated caches. self.addEventListener('activate', e => { e.waitUntil( caches.keys().then(keys => @@ -29,11 +29,11 @@ self.addEventListener('activate', e => { ); }); -// Fetch : stratégie selon le type de ressource +// Fetch: choose a caching strategy based on resource type. self.addEventListener('fetch', e => { const url = new URL(e.request.url); - // Flux EPG/M3U et API GitHub : network only (pas de cache) + // EPG/M3U feeds and the GitHub API stay network-only. if ( url.pathname.includes('xmltv') || url.pathname.includes('.m3u') || @@ -43,7 +43,7 @@ self.addEventListener('fetch', e => { return; } - // Assets statiques : cache first + // Static assets use a cache-first strategy. if ( e.request.destination === 'style' || e.request.destination === 'script' || @@ -63,7 +63,7 @@ self.addEventListener('fetch', e => { return; } - // Pages PHP (index.php) : network first, fallback cache + // PHP pages use network-first with cache fallback. e.respondWith( fetch(e.request) .then(res => {