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'); } $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' => '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_fetch' => $epg_fetch, 'epg_stats' => $epg_stats, 'm3u_fetch' => $m3u_fetch, 'm3u_stats' => $m3u_stats, ]; } ?>