Add configuration system

This commit is contained in:
Gogs
2026-07-06 17:17:31 +02:00
parent fe8342772d
commit 566ddacb2f
13 changed files with 318 additions and 176 deletions
+1
View File
@@ -0,0 +1 @@
{}
+8
View File
@@ -0,0 +1,8 @@
{
"title": "TVGuide",
"timezone": "Europe/Vienna",
"language": "de",
"theme": "dark",
"days": 7,
"default_view": "today"
}
+89
View File
@@ -0,0 +1,89 @@
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #101114;
color: #eee;
}
.topbar {
padding: 20px;
background: #181a20;
border-bottom: 1px solid #2b2f3a;
display: flex;
align-items: center;
gap: 28px;
}
.topbar h1 {
margin: 0;
}
.topbar nav {
display: flex;
gap: 14px;
}
.topbar a {
color: #cfd6e6;
text-decoration: none;
}
main {
padding: 20px;
max-width: 1100px;
margin: 0 auto;
}
.channel {
margin-bottom: 28px;
background: #181a20;
border: 1px solid #2b2f3a;
border-radius: 16px;
overflow: hidden;
}
.channel-header {
padding: 14px 18px;
background: #20232c;
font-size: 1.25rem;
font-weight: 700;
}
.programme {
padding: 14px 18px;
border-top: 1px solid #2b2f3a;
}
.programme-time {
color: #9da7bd;
font-size: 0.95rem;
margin-bottom: 4px;
}
.programme-title {
font-size: 1.1rem;
font-weight: 700;
}
.programme-desc {
color: #c9ced8;
margin-top: 6px;
}
.category {
display: inline-block;
margin-top: 8px;
padding: 3px 8px;
border-radius: 999px;
background: #303545;
color: #cfd6e6;
font-size: 0.8rem;
}
.empty {
color: #aaa;
background: #181a20;
border: 1px solid #2b2f3a;
border-radius: 16px;
padding: 18px;
}
+1
View File
@@ -0,0 +1 @@
console.log('TVGuide loaded');
+20 -176
View File
@@ -1,183 +1,27 @@
<?php
$xmlFile = '/var/www/epg/guide.xml';
function parseXmltvTime(string $value): ?DateTime {
if (!preg_match('/^(\d{14})\s+([+-]\d{4})$/', trim($value), $m)) {
return null;
}
require_once '/var/www/src/XmlTv.php';
require_once '/var/www/src/View.php';
require_once '/var/www/src/Config.php';
$dt = DateTime::createFromFormat('YmdHis O', $m[1] . ' ' . $m[2]);
if (!$dt) {
return null;
}
$config = new Config(
'/var/www/config/config.json',
'/var/www/config/channels.json'
);
$dt->setTimezone(new DateTimeZone('Europe/Vienna'));
return $dt;
}
$xmltv = new XmlTv(
'/var/www/epg/guide.xml',
$config->get('timezone', 'Europe/Vienna')
);
$channels = [];
$programmes = [];
$xmltv = new XmlTv('/var/www/epg/guide.xml', 'Europe/Vienna');
[$channels, $programmes] = $xmltv->load();
if (file_exists($xmlFile)) {
$xml = simplexml_load_file($xmlFile);
$contentTemplate = '/var/www/templates/home.php';
foreach ($xml->channel as $channel) {
$id = (string)$channel['id'];
$channels[$id] = [
'id' => $id,
'name' => (string)$channel->{'display-name'},
'icon' => isset($channel->icon) ? (string)$channel->icon['src'] : null,
];
}
foreach ($xml->programme as $programme) {
$channelId = (string)$programme['channel'];
$start = parseXmltvTime((string)$programme['start']);
$stop = parseXmltvTime((string)$programme['stop']);
if (!$start || !$stop) {
continue;
}
$programmes[$channelId][] = [
'title' => (string)$programme->title,
'desc' => (string)$programme->desc,
'category' => (string)$programme->category,
'start' => $start,
'stop' => $stop,
];
}
}
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>TVGuide</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #101114;
color: #eee;
}
header {
padding: 20px;
background: #181a20;
border-bottom: 1px solid #2b2f3a;
}
header h1 {
margin: 0;
}
main {
padding: 20px;
max-width: 1100px;
margin: 0 auto;
}
.channel {
margin-bottom: 28px;
background: #181a20;
border: 1px solid #2b2f3a;
border-radius: 16px;
overflow: hidden;
}
.channel-header {
padding: 14px 18px;
background: #20232c;
font-size: 1.25rem;
font-weight: 700;
}
.programme {
padding: 14px 18px;
border-top: 1px solid #2b2f3a;
}
.programme-time {
color: #9da7bd;
font-size: 0.95rem;
margin-bottom: 4px;
}
.programme-title {
font-size: 1.1rem;
font-weight: 700;
}
.programme-desc {
color: #c9ced8;
margin-top: 6px;
}
.category {
display: inline-block;
margin-top: 8px;
padding: 3px 8px;
border-radius: 999px;
background: #303545;
color: #cfd6e6;
font-size: 0.8rem;
}
.empty {
color: #aaa;
background: #181a20;
border: 1px solid #2b2f3a;
border-radius: 16px;
padding: 18px;
}
</style>
</head>
<body>
<header>
<h1>TVGuide</h1>
</header>
<main>
<?php if (!$channels): ?>
<div class="empty">
Keine Sender gefunden. Prüfe <code>/opt/tvguide-docker/epg/guide.xml</code>.
</div>
<?php endif; ?>
<?php foreach ($channels as $channelId => $channel): ?>
<section class="channel">
<div class="channel-header">
<?= htmlspecialchars($channel['name']) ?>
</div>
<?php if (empty($programmes[$channelId])): ?>
<div class="programme">
<div class="programme-desc">Keine Sendungen gefunden.</div>
</div>
<?php else: ?>
<?php foreach ($programmes[$channelId] as $p): ?>
<div class="programme">
<div class="programme-time">
<?= $p['start']->format('H:i') ?> <?= $p['stop']->format('H:i') ?>
</div>
<div class="programme-title">
<?= htmlspecialchars($p['title']) ?>
</div>
<?php if ($p['desc']): ?>
<div class="programme-desc">
<?= htmlspecialchars($p['desc']) ?>
</div>
<?php endif; ?>
<?php if ($p['category']): ?>
<span class="category"><?= htmlspecialchars($p['category']) ?></span>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</section>
<?php endforeach; ?>
</main>
</body>
</html>
View::render('layout', [
'title' => $config->get('title'),
'channels' => $channels,
'programmes' => $programmes,
'contentTemplate' => $contentTemplate,
]);
+43
View File
@@ -0,0 +1,43 @@
<?php
class Config
{
private array $config = [];
private array $channels = [];
public function __construct(
string $configFile,
string $channelsFile
) {
if (file_exists($configFile)) {
$this->config = json_decode(file_get_contents($configFile), true) ?? [];
}
if (file_exists($channelsFile)) {
$this->channels = json_decode(file_get_contents($channelsFile), true) ?? [];
}
}
public function get(string $key, mixed $default = null): mixed
{
return $this->config[$key] ?? $default;
}
public function getChannels(): array
{
return $this->channels;
}
public function saveChannels(array $channels): void
{
$this->channels = $channels;
file_put_contents(
'/var/www/config/channels.json',
json_encode(
$channels,
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
)
);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
class View
{
public static function render(string $template, array $data = []): void
{
extract($data);
require "/var/www/templates/$template.php";
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
class XmlTv
{
private string $file;
private string $timezone;
public function __construct(string $file, string $timezone = 'Europe/Vienna')
{
$this->file = $file;
$this->timezone = $timezone;
}
public function load(): array
{
$channels = [];
$programmes = [];
if (!file_exists($this->file)) {
return [$channels, $programmes];
}
$xml = simplexml_load_file($this->file);
foreach ($xml->channel as $channel) {
$id = (string)$channel['id'];
$channels[$id] = [
'id' => $id,
'name' => (string)$channel->{'display-name'},
'icon' => isset($channel->icon) ? (string)$channel->icon['src'] : null,
];
}
foreach ($xml->programme as $programme) {
$channelId = (string)$programme['channel'];
$start = $this->parseTime((string)$programme['start']);
$stop = $this->parseTime((string)$programme['stop']);
if (!$start || !$stop) {
continue;
}
$programmes[$channelId][] = [
'title' => (string)$programme->title,
'desc' => (string)$programme->desc,
'category' => (string)$programme->category,
'start' => $start,
'stop' => $stop,
];
}
return [$channels, $programmes];
}
private function parseTime(string $value): ?DateTime
{
if (!preg_match('/^(\d{14})\s+([+-]\d{4})$/', trim($value), $m)) {
return null;
}
$dt = DateTime::createFromFormat('YmdHis O', $m[1] . ' ' . $m[2]);
if (!$dt) {
return null;
}
$dt->setTimezone(new DateTimeZone($this->timezone));
return $dt;
}
}
+16
View File
@@ -0,0 +1,16 @@
<section class="channel">
<div class="channel-header">
<?= htmlspecialchars($channel['name']) ?>
</div>
<?php if (!$programmes): ?>
<div class="programme">
<div class="programme-desc">Keine Sendungen gefunden.</div>
</div>
<?php else: ?>
<?php foreach ($programmes as $p): ?>
<?php View::render('programme', ['p' => $p]); ?>
<?php endforeach; ?>
<?php endif; ?>
</section>
+14
View File
@@ -0,0 +1,14 @@
<?php if (!$channels): ?>
<div class="empty">
Keine Sender gefunden. Prüfe <code>/opt/tvguide-docker/epg/guide.xml</code>.
</div>
<?php endif; ?>
<?php foreach ($channels as $channelId => $channel): ?>
<?php View::render('channel', [
'channelId' => $channelId,
'channel' => $channel,
'programmes' => $programmes[$channelId] ?? []
]); ?>
<?php endforeach; ?>
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title><?= htmlspecialchars($title ?? 'TVGuide') ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/main.css">
</head>
<body>
<header class="topbar">
<h1>TVGuide</h1>
<nav>
<a href="/">Heute</a>
<a href="/?view=now">Jetzt</a>
<a href="/?view=primetime">20:15</a>
</nav>
</header>
<main>
<?php require $contentTemplate; ?>
</main>
<script src="/assets/js/app.js"></script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
<div class="programme">
<div class="programme-time">
<?= $p['start']->format('H:i') ?> <?= $p['stop']->format('H:i') ?>
</div>
<div class="programme-title">
<?= htmlspecialchars($p['title']) ?>
</div>
<?php if ($p['desc']): ?>
<div class="programme-desc">
<?= htmlspecialchars($p['desc']) ?>
</div>
<?php endif; ?>
<?php if ($p['category']): ?>
<span class="category"><?= htmlspecialchars($p['category']) ?></span>
<?php endif; ?>
</div>
+1
View File
@@ -8,6 +8,7 @@ services:
volumes:
- ./app/public:/var/www/html
- ./app/src:/var/www/src
- ./app/templates:/var/www/templates
- ./app/config:/var/www/config
- ./app/cache:/var/www/cache
- ./epg:/var/www/epg