false, 'error' => 'PHP DOM/XML extension is not installed.']; } if ($xml === '') { return ['ok' => false, 'error' => 'Empty response']; } libxml_use_internal_errors(true); $dom = new DOMDocument(); $loaded = $dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING); $errors = array_map(static fn($e) => trim($e->message), libxml_get_errors()); libxml_clear_errors(); if (!$loaded) { return ['ok' => false, 'error' => $errors[0] ?? 'Invalid XML']; } $xp = new DOMXPath($dom); $programmes = $xp->query('/tv/programme'); $channels = $xp->query('/tv/channel'); $categories = $xp->query('/tv/programme/category'); $subtitles = $xp->query('/tv/programme/sub-title'); $ratings = $xp->query('/tv/programme/rating'); $dates = $xp->query('/tv/programme/date'); $first = null; $last = null; foreach ($programmes as $programme) { $start = $programme->getAttribute('start'); $stop = $programme->getAttribute('stop'); if ($start !== '' && ($first === null || strcmp($start, $first) < 0)) $first = $start; if ($stop !== '' && ($last === null || strcmp($stop, $last) > 0)) $last = $stop; } return [ 'ok' => true, 'channels' => $channels->length, 'programmes' => $programmes->length, 'categories' => $categories->length, 'subtitles' => $subtitles->length, 'ratings' => $ratings->length, 'dates' => $dates->length, 'first_start' => $first, 'last_stop' => $last, ]; } function health_parse_m3u(string $body): array { $lines = preg_split('/\r\n|\r|\n/', $body); $streams = 0; $extinf = 0; foreach ($lines as $line) { $line = trim($line); if ($line === '') continue; if (stripos($line, '#EXTINF') === 0) { $extinf++; continue; } if ($line[0] !== '#') $streams++; } return ['streams' => $streams, 'extinf' => $extinf]; } function health_format_xmltv_stamp(?string $stamp): string { if (!$stamp) return '—'; $dt = DateTime::createFromFormat('YmdHis O', $stamp); if (!$dt) return $stamp; return $dt->format('d/m/Y H:i'); } function health_probe_payload(): array { $config_path = gridtv_config_path(); $config_exists = file_exists($config_path); $config_valid = false; $config = []; if ($config_exists) { $decoded = json_decode(file_get_contents($config_path), true); if (is_array($decoded)) { $config = $decoded; $config_valid = true; } } $sources = $config_valid ? array_values($config['epg_sources'] ?? []) : []; $first_source = $sources[0] ?? null; $checks = [ 'config_exists' => $config_exists, 'config_valid' => $config_valid, 'php_curl' => extension_loaded('curl'), 'php_xml' => class_exists('DOMDocument'), 'source_configured' => !empty($sources), ]; $details = [ 'group_name' => $config['group_name'] ?? 'GridTV', 'source_count' => count($sources), ]; if ($first_source && !empty($first_source['epg_url'])) { $fetch = gridtv_fetch_url((string) $first_source['epg_url'], 12); $checks['xmltv_reachable'] = $fetch['ok']; $details['xmltv_status'] = $fetch['status']; $details['xmltv_time_total'] = round((float) $fetch['time_total'], 3); if ($fetch['ok']) { $stats = health_parse_xmltv($fetch['body']); $checks['xmltv_valid'] = !empty($stats['ok']); if (!empty($stats['ok'])) { $details['xmltv_channels'] = $stats['channels']; $details['xmltv_programmes'] = $stats['programmes']; } else { $details['xmltv_error'] = $stats['error'] ?? 'Invalid XMLTV'; } } else { $details['xmltv_error'] = $fetch['error'] ?: ('HTTP ' . $fetch['status']); } } else { $checks['xmltv_reachable'] = false; $details['xmltv_error'] = 'No XMLTV source configured'; } $ok = !in_array(false, $checks, true); return [ 'ok' => $ok, 'status' => $ok ? 'ok' : 'degraded', 'timestamp' => gmdate('c'), 'checks' => $checks, 'details' => $details, ]; } function health_build_payload(array $config, string $locale): array { $config_path = gridtv_config_path(); $config_exists = file_exists($config_path); $config_writable = $config_exists ? is_writable($config_path) : is_writable(dirname($config_path)); $sources = array_values($config['epg_sources'] ?? []); $checks = [ [ 'label' => 'Config file', 'ok' => $config_exists, 'detail' => $config_exists ? basename($config_path) . ' found' : 'Missing config.json', ], [ 'label' => 'Config writable', 'ok' => $config_writable, 'detail' => $config_writable ? 'Writable by PHP' : 'Not writable by PHP', ], [ 'label' => 'PHP version', 'ok' => version_compare(PHP_VERSION, '8.0.0', '>='), 'detail' => 'PHP ' . PHP_VERSION, ], [ 'label' => 'php-curl', 'ok' => extension_loaded('curl'), 'detail' => extension_loaded('curl') ? 'Extension loaded' : 'Missing php-curl', ], [ 'label' => 'php-xml / DOM', 'ok' => class_exists('DOMDocument'), 'detail' => class_exists('DOMDocument') ? 'Extension loaded' : 'Missing php-xml / DOM', ], [ 'label' => 'Configured sources', 'ok' => count($sources) > 0, 'detail' => count($sources) . ' source(s) in config', ], ]; $source_reports = []; foreach ($sources as $index => $source) { $epg_url = trim((string) ($source['epg_url'] ?? '')); $m3u_url = trim((string) ($source['m3u_url'] ?? '')); $epg_fetch = $epg_url !== '' ? gridtv_fetch_url($epg_url, 25) : ['ok' => false, 'status' => 0, 'error' => 'Missing URL', 'body' => '', 'content_type' => '', 'final_url' => '', 'time_total' => 0.0]; $epg_stats = $epg_fetch['ok'] ? health_parse_xmltv($epg_fetch['body']) : ['ok' => false, 'error' => $epg_fetch['error'] ?: 'HTTP ' . $epg_fetch['status']]; $m3u_fetch = null; $m3u_stats = null; if ($m3u_url !== '') { $m3u_fetch = gridtv_fetch_url($m3u_url, 20); $m3u_stats = $m3u_fetch['ok'] ? health_parse_m3u($m3u_fetch['body']) : null; } $source_reports[] = [ 'name' => $source['name'] ?: 'Source ' . ($index + 1), 'epg_url' => $epg_url, 'm3u_url' => $m3u_url, 'epg_host' => gridtv_extract_host($epg_url), 'm3u_host' => gridtv_extract_host($m3u_url), 'epg_fetch' => $epg_fetch, 'epg_stats' => $epg_stats, 'm3u_fetch' => $m3u_fetch, 'm3u_stats' => $m3u_stats, ]; } return [ 'summary' => [ 'group_name' => $config['group_name'] ?? 'GridTV', 'source_count' => count($sources), 'locale' => strtoupper($locale), 'config_size' => $config_exists ? gridtv_format_bytes((int) filesize($config_path)) : '0 B', 'config_path' => $config_path, ], 'checks' => $checks, 'sources' => $source_reports, 'helpers' => [ 'status_ok' => 'OK', 'status_bad' => 'Issue', ], ]; } if (isset($_GET['format']) && $_GET['format'] === 'probe') { $payload = health_probe_payload(); http_response_code($payload['ok'] ? 200 : 503); if (isset($_GET['plain']) && $_GET['plain'] === '1') { header('Content-Type: text/plain; charset=UTF-8'); echo $payload['ok'] ? 'OK' : 'KO'; } else { header('Content-Type: application/json; charset=UTF-8'); echo json_encode($payload, JSON_UNESCAPED_SLASHES); } exit; } $config = gridtv_load_config(); gridtv_require_admin($config, 'Health'); [$locale, $L] = gridtv_load_locale($config); if (isset($_GET['format']) && $_GET['format'] === 'json') { header('Content-Type: application/json; charset=UTF-8'); echo json_encode(health_build_payload($config, $locale), JSON_UNESCAPED_SLASHES); exit; } ?> GridTV — Health
Page de diagnostic admin. L’écran apparaît tout de suite, puis les checks XMLTV/M3U se remplissent en arrière-plan.
Loading diagnostics…
Configuration, XMLTV et M3U sont testés en arrière-plan.

Monitoring probe

Use these public endpoints with Uptime Kuma or any HTTP monitoring tool. They return 200 when the instance is healthy and 503 when a critical check fails.

JSON probe
?format=probe
Plain text probe
?format=probe&plain=1
Group
Sources
Locale
Config Size