Files
GridTV/export.php
T
Johnnybegood90 a81e33ccb8 Add admin health page and printable 24h export
- add a protected health.php page for config, XMLTV and M3U diagnostics
- add shared admin/config helpers for authenticated internal tools
- add a styled 24h export page designed for PDF/print output
- add browser-side PNG export for the daily schedule
- update README with the new admin tools
2026-03-15 01:44:22 +01:00

247 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/src/lib/admin.php';
$config = gridtv_load_config();
gridtv_require_admin($config, 'Export');
[$locale, $L] = gridtv_load_locale($config);
function export_parse_xmltv_programmes(string $xml, string $target_day): array {
libxml_use_internal_errors(true);
$dom = new DOMDocument();
if (!$dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING)) {
return ['channels' => [], 'programmes' => [], 'error' => 'Invalid XMLTV response'];
}
$xp = new DOMXPath($dom);
$channels = [];
foreach ($xp->query('/tv/channel') as $channel) {
$id = $channel->getAttribute('id');
$name = trim($xp->evaluate('string(display-name[1])', $channel));
$icon = trim($xp->evaluate('string(icon/@src)', $channel));
$channels[$id] = ['id' => $id, 'name' => $name ?: $id, 'icon' => $icon];
}
$start_day = new DateTimeImmutable($target_day . ' 00:00:00');
$end_day = $start_day->modify('+1 day');
$rows = [];
foreach ($xp->query('/tv/programme') as $programme) {
$channel_id = $programme->getAttribute('channel');
$start = DateTimeImmutable::createFromFormat('YmdHis O', $programme->getAttribute('start'));
$stop = DateTimeImmutable::createFromFormat('YmdHis O', $programme->getAttribute('stop'));
if (!$start || !$stop) continue;
if ($stop <= $start_day || $start >= $end_day) continue;
$title = trim($xp->evaluate('string(title)', $programme));
$subtitle = trim($xp->evaluate('string(sub-title)', $programme));
$desc = trim($xp->evaluate('string(desc)', $programme));
$rating = trim($xp->evaluate('string(rating/value)', $programme));
$cats = [];
foreach ($xp->query('category', $programme) as $cat) {
$value = trim($cat->textContent);
if ($value !== '') $cats[strtolower($value)] = $value;
}
$rows[$channel_id][] = [
'start' => $start,
'stop' => $stop,
'title' => $title,
'subtitle' => $subtitle,
'desc' => $desc,
'rating' => $rating,
'categories' => array_values($cats),
];
}
uasort($channels, static function ($a, $b) {
preg_match('/^\d+/', $a['name'], $ma);
preg_match('/^\d+/', $b['name'], $mb);
$na = isset($ma[0]) ? (int) $ma[0] : 9999;
$nb = isset($mb[0]) ? (int) $mb[0] : 9999;
return $na <=> $nb ?: strcmp($a['name'], $b['name']);
});
foreach ($rows as &$programmes) {
usort($programmes, static fn($a, $b) => $a['start'] <=> $b['start']);
}
return ['channels' => $channels, 'programmes' => $rows, 'error' => ''];
}
$sources = array_values($config['epg_sources'] ?? []);
$source_index = max(0, min((int) ($_GET['source'] ?? 0), max(count($sources) - 1, 0)));
$source = $sources[$source_index] ?? null;
$target_day = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_GET['day'] ?? '')) ? (string) $_GET['day'] : (new DateTimeImmutable('today'))->format('Y-m-d');
$fetch = $source ? gridtv_fetch_url((string) $source['epg_url'], 25) : ['ok' => false, 'error' => 'No source configured', 'body' => ''];
$parsed = $fetch['ok'] ? export_parse_xmltv_programmes($fetch['body'], $target_day) : ['channels' => [], 'programmes' => [], 'error' => $fetch['error'] ?: 'Unable to fetch XMLTV'];
$print_channels = [];
foreach ($parsed['channels'] as $channel_id => $channel) {
if (!empty($parsed['programmes'][$channel_id])) {
$print_channels[$channel_id] = $channel;
}
}
$day_label = (new DateTimeImmutable($target_day))->format('l d F Y');
?><!DOCTYPE html>
<html lang="<?= htmlspecialchars($locale) ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GridTV — Export 24h</title>
<link rel="stylesheet" href="/assets/fonts/fonts.css">
<script src="/assets/vendor/html2canvas.min.js"></script>
<style>
:root{--paper:#f4ecda;--ink:#22201b;--ink-soft:#675f52;--accent:#aa4231;--accent-2:#264653;--line:#d3c5ab;--stamp:#d8b24a}
*{box-sizing:border-box}body{margin:0;font-family:'Barlow Condensed',sans-serif;background:linear-gradient(180deg,#1f2125 0,#121317 100%);color:#f7f4ee}
.page{max-width:1380px;margin:0 auto;padding:26px 18px 40px}
.toolbar{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:18px}
.heading{max-width:760px}.heading h1{margin:0 0 6px;font-size:34px;letter-spacing:.06em;text-transform:uppercase}.heading p{margin:0;color:#bcb7ad;font-size:14px;line-height:1.5}
.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.controls a,.controls button,.controls select,.controls input{height:42px;border:none}
.controls a,.controls button{display:inline-flex;align-items:center;justify-content:center;padding:0 14px;background:#ebd58a;color:#1f1d18;text-decoration:none;font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;cursor:pointer}
.controls .ghost{background:#262a31;color:#f7f4ee;border:1px solid #3c4450}
.controls select,.controls input{padding:0 12px;background:#262a31;color:#fff;border:1px solid #3c4450;font-family:'Share Tech Mono',monospace}
.sheet-wrap{overflow:auto;padding:8px 0}
.sheet{width:1120px;margin:0 auto;background:var(--paper);color:var(--ink);padding:30px 34px 36px;box-shadow:0 26px 70px rgba(0,0,0,.45);border:10px solid #f8f2e7;position:relative}
.sheet::before{content:'';position:absolute;inset:14px;border:1px solid rgba(38,70,83,.18);pointer-events:none}
.sheet-head{display:flex;justify-content:space-between;gap:20px;align-items:flex-end;border-bottom:3px solid var(--accent);padding-bottom:16px;margin-bottom:18px}
.sheet-title{font-family:'IM Fell English',serif;font-size:44px;line-height:1.05;letter-spacing:.01em}
.sheet-sub{font-family:'Share Tech Mono',monospace;font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-2)}
.sheet-meta{text-align:right}.sheet-meta .group{font-size:22px;font-weight:700}.sheet-meta .date{font-size:14px;color:var(--ink-soft)}
.legend{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px}
.legend span{padding:6px 10px;border:1px solid var(--line);font-family:'Share Tech Mono',monospace;font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.table{display:grid;gap:12px}
.row{display:grid;grid-template-columns:170px 1fr;gap:14px;align-items:stretch}
.channel{border-top:2px solid var(--accent-2);padding-top:8px}
.channel-top{display:flex;gap:10px;align-items:center}
.channel img{width:42px;height:42px;object-fit:contain;background:#fff;border:1px solid var(--line);padding:4px}
.channel-name{font-size:22px;font-weight:700;line-height:1.05}
.timeline{display:grid;grid-template-columns:repeat(24,minmax(0,1fr));border-top:2px solid var(--accent-2);position:relative;min-height:112px}
.hour{border-left:1px dashed var(--line);padding:8px 7px 10px 10px}
.hour:first-child{border-left:none}
.hour-label{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:8px}
.item{display:block;padding:8px 8px 9px;background:rgba(170,66,49,.08);border-left:3px solid var(--accent);margin-bottom:7px}
.item.tight{padding:6px 7px}
.item-title{font-size:14px;font-weight:700;line-height:1.1}
.item-meta{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:.03em;color:var(--ink-soft);margin-top:4px}
.item-sub{font-size:11px;color:var(--ink-soft);margin-top:3px;line-height:1.2}
.empty{color:var(--ink-soft);font-style:italic;font-size:12px;padding-top:24px}
.note{margin-top:16px;font-size:12px;color:var(--ink-soft);text-align:right}
@media print{body{background:#fff}.page{padding:0}.toolbar{display:none}.sheet{box-shadow:none;border:none;width:auto;margin:0}.sheet-wrap{overflow:visible}}
@media (max-width:1200px){.sheet{transform-origin:top center}}
</style>
</head>
<body>
<div class="page">
<div class="toolbar">
<div class="heading">
<h1>Export 24h</h1>
<p>Version “programme télé” pensée pour être imprimée proprement en PDF, ou exportée en image pour lenvoyer facilement à quelquun.</p>
</div>
<div class="controls">
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap">
<select name="source">
<?php foreach ($sources as $i => $src): ?>
<option value="<?= $i ?>"<?= $i === $source_index ? ' selected' : '' ?>><?= htmlspecialchars($src['name']) ?></option>
<?php endforeach; ?>
</select>
<input type="date" name="day" value="<?= htmlspecialchars($target_day) ?>">
<button type="submit" class="ghost">Refresh</button>
</form>
<button type="button" onclick="window.print()">PDF / Print</button>
<button type="button" class="ghost" onclick="downloadImage()">Image</button>
<a href="/health.php" class="ghost">Health</a>
<a href="/index.php" class="ghost">Guide</a>
</div>
</div>
<div class="sheet-wrap">
<div class="sheet" id="exportSheet">
<div class="sheet-head">
<div>
<div class="sheet-sub">TV Guide · 24 hours · Printable edition</div>
<div class="sheet-title">Le programme télé de la journée</div>
</div>
<div class="sheet-meta">
<div class="group"><?= htmlspecialchars($config['group_name'] ?? 'GridTV') ?></div>
<div class="date"><?= htmlspecialchars($day_label) ?> · <?= htmlspecialchars($source['name'] ?? 'Source') ?></div>
</div>
</div>
<div class="legend">
<span>24h view</span>
<span><?= count($print_channels) ?> channel(s)</span>
<span>Generated from XMLTV</span>
</div>
<?php if (!$fetch['ok'] || $parsed['error']): ?>
<div class="empty">Impossible de générer la feuille: <?= htmlspecialchars($parsed['error'] ?: ($fetch['error'] ?? 'Unknown error')) ?></div>
<?php elseif (!$print_channels): ?>
<div class="empty">Aucun programme trouvé pour cette journée.</div>
<?php else: ?>
<div class="table">
<?php foreach ($print_channels as $channel_id => $channel): ?>
<div class="row">
<div class="channel">
<div class="channel-top">
<?php if ($channel['icon']): ?><img src="<?= htmlspecialchars($channel['icon']) ?>" alt=""><?php endif; ?>
<div class="channel-name"><?= htmlspecialchars($channel['name']) ?></div>
</div>
</div>
<div class="timeline">
<?php
for ($hour = 0; $hour < 24; $hour++):
$hour_start = new DateTimeImmutable($target_day . sprintf(' %02d:00:00', $hour));
$hour_end = $hour_start->modify('+1 hour');
$items = [];
foreach ($parsed['programmes'][$channel_id] as $programme) {
if ($programme['stop'] <= $hour_start || $programme['start'] >= $hour_end) continue;
$items[] = $programme;
}
?>
<div class="hour">
<div class="hour-label"><?= sprintf('%02dh', $hour) ?></div>
<?php if (!$items): ?>
<div class="empty">—</div>
<?php else: ?>
<?php foreach ($items as $programme): ?>
<?php
$meta = $programme['start']->format('H:i') . ' - ' . $programme['stop']->format('H:i');
if ($programme['rating']) $meta .= ' · ' . $programme['rating'];
$cats = $programme['categories'] ? implode(' · ', $programme['categories']) : '';
?>
<div class="item<?= strlen($programme['title']) > 28 ? ' tight' : '' ?>">
<div class="item-title"><?= htmlspecialchars($programme['title']) ?></div>
<?php if ($programme['subtitle']): ?><div class="item-sub"><?= htmlspecialchars($programme['subtitle']) ?></div><?php endif; ?>
<div class="item-meta"><?= htmlspecialchars($meta) ?></div>
<?php if ($cats): ?><div class="item-sub"><?= htmlspecialchars($cats) ?></div><?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="note">Astuce: “PDF / Print” donne un export A4/PDF propre. “Image” fabrique un PNG de la feuille affichée.</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
async function downloadImage() {
const sheet = document.getElementById('exportSheet');
if (!sheet || typeof html2canvas !== 'function') return;
const canvas = await html2canvas(sheet, {backgroundColor: '#f4ecda', scale: 2, useCORS: true});
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = 'gridtv-24h-<?= htmlspecialchars($target_day) ?>.png';
link.click();
}
</script>
</body>
</html>