diff --git a/app/config/channels.json b/app/config/channels.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/app/config/channels.json @@ -0,0 +1 @@ +{} diff --git a/app/config/config.json b/app/config/config.json new file mode 100644 index 0000000..ed71cdc --- /dev/null +++ b/app/config/config.json @@ -0,0 +1,8 @@ +{ + "title": "TVGuide", + "timezone": "Europe/Vienna", + "language": "de", + "theme": "dark", + "days": 7, + "default_view": "today" +} diff --git a/app/public/assets/css/main.css b/app/public/assets/css/main.css new file mode 100644 index 0000000..678295f --- /dev/null +++ b/app/public/assets/css/main.css @@ -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; +} diff --git a/app/public/assets/js/app.js b/app/public/assets/js/app.js new file mode 100644 index 0000000..b0ed84b --- /dev/null +++ b/app/public/assets/js/app.js @@ -0,0 +1 @@ +console.log('TVGuide loaded'); diff --git a/app/public/index.php b/app/public/index.php index 91f297d..61e5910 100644 --- a/app/public/index.php +++ b/app/public/index.php @@ -1,183 +1,27 @@ 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, - ]; - } -} -?> - - - - - TVGuide - - - - -
-

TVGuide

-
- -
- -
- Keine Sender gefunden. Prüfe /opt/tvguide-docker/epg/guide.xml. -
- - - $channel): ?> -
-
- -
- - -
-
Keine Sendungen gefunden.
-
- - -
-
- format('H:i') ?> – format('H:i') ?> -
-
- -
- -
- -
- - - - -
- - -
- -
- - +View::render('layout', [ + 'title' => $config->get('title'), + 'channels' => $channels, + 'programmes' => $programmes, + 'contentTemplate' => $contentTemplate, +]); diff --git a/app/src/Config.php b/app/src/Config.php new file mode 100644 index 0000000..e8a04f1 --- /dev/null +++ b/app/src/Config.php @@ -0,0 +1,43 @@ +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 + ) + ); + } +} diff --git a/app/src/View.php b/app/src/View.php new file mode 100644 index 0000000..26e45d2 --- /dev/null +++ b/app/src/View.php @@ -0,0 +1,10 @@ +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; + } +} diff --git a/app/templates/channel.php b/app/templates/channel.php new file mode 100644 index 0000000..5445690 --- /dev/null +++ b/app/templates/channel.php @@ -0,0 +1,16 @@ +
+
+ +
+ + +
+
Keine Sendungen gefunden.
+
+ + + $p]); ?> + + +
+ diff --git a/app/templates/home.php b/app/templates/home.php new file mode 100644 index 0000000..a7192b4 --- /dev/null +++ b/app/templates/home.php @@ -0,0 +1,14 @@ + +
+ Keine Sender gefunden. Prüfe /opt/tvguide-docker/epg/guide.xml. +
+ + + $channel): ?> + $channelId, + 'channel' => $channel, + 'programmes' => $programmes[$channelId] ?? [] + ]); ?> + + diff --git a/app/templates/layout.php b/app/templates/layout.php new file mode 100644 index 0000000..21e7aa1 --- /dev/null +++ b/app/templates/layout.php @@ -0,0 +1,25 @@ + + + + + <?= htmlspecialchars($title ?? 'TVGuide') ?> + + + + +
+

TVGuide

+ +
+ +
+ +
+ + + + diff --git a/app/templates/programme.php b/app/templates/programme.php new file mode 100644 index 0000000..6abe97b --- /dev/null +++ b/app/templates/programme.php @@ -0,0 +1,19 @@ +
+
+ format('H:i') ?> – format('H:i') ?> +
+ +
+ +
+ + +
+ +
+ + + + + +
diff --git a/docker-compose.yaml b/docker-compose.yaml index e66b104..3bd5a93 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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