Add configuration system
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"title": "TVGuide",
|
||||||
|
"timezone": "Europe/Vienna",
|
||||||
|
"language": "de",
|
||||||
|
"theme": "dark",
|
||||||
|
"days": 7,
|
||||||
|
"default_view": "today"
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
console.log('TVGuide loaded');
|
||||||
+20
-176
@@ -1,183 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
$xmlFile = '/var/www/epg/guide.xml';
|
|
||||||
|
|
||||||
function parseXmltvTime(string $value): ?DateTime {
|
require_once '/var/www/src/XmlTv.php';
|
||||||
if (!preg_match('/^(\d{14})\s+([+-]\d{4})$/', trim($value), $m)) {
|
require_once '/var/www/src/View.php';
|
||||||
return null;
|
require_once '/var/www/src/Config.php';
|
||||||
}
|
|
||||||
|
|
||||||
$dt = DateTime::createFromFormat('YmdHis O', $m[1] . ' ' . $m[2]);
|
$config = new Config(
|
||||||
if (!$dt) {
|
'/var/www/config/config.json',
|
||||||
return null;
|
'/var/www/config/channels.json'
|
||||||
}
|
);
|
||||||
|
|
||||||
$dt->setTimezone(new DateTimeZone('Europe/Vienna'));
|
$xmltv = new XmlTv(
|
||||||
return $dt;
|
'/var/www/epg/guide.xml',
|
||||||
}
|
$config->get('timezone', 'Europe/Vienna')
|
||||||
|
);
|
||||||
|
|
||||||
$channels = [];
|
$xmltv = new XmlTv('/var/www/epg/guide.xml', 'Europe/Vienna');
|
||||||
$programmes = [];
|
[$channels, $programmes] = $xmltv->load();
|
||||||
|
|
||||||
if (file_exists($xmlFile)) {
|
$contentTemplate = '/var/www/templates/home.php';
|
||||||
$xml = simplexml_load_file($xmlFile);
|
|
||||||
|
|
||||||
foreach ($xml->channel as $channel) {
|
View::render('layout', [
|
||||||
$id = (string)$channel['id'];
|
'title' => $config->get('title'),
|
||||||
$channels[$id] = [
|
'channels' => $channels,
|
||||||
'id' => $id,
|
'programmes' => $programmes,
|
||||||
'name' => (string)$channel->{'display-name'},
|
'contentTemplate' => $contentTemplate,
|
||||||
'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>
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class View
|
||||||
|
{
|
||||||
|
public static function render(string $template, array $data = []): void
|
||||||
|
{
|
||||||
|
extract($data);
|
||||||
|
require "/var/www/templates/$template.php";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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; ?>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -8,6 +8,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./app/public:/var/www/html
|
- ./app/public:/var/www/html
|
||||||
- ./app/src:/var/www/src
|
- ./app/src:/var/www/src
|
||||||
|
- ./app/templates:/var/www/templates
|
||||||
- ./app/config:/var/www/config
|
- ./app/config:/var/www/config
|
||||||
- ./app/cache:/var/www/cache
|
- ./app/cache:/var/www/cache
|
||||||
- ./epg:/var/www/epg
|
- ./epg:/var/www/epg
|
||||||
|
|||||||
Reference in New Issue
Block a user