diff --git a/app/public/api/live-now.php b/app/public/api/live-now.php new file mode 100644 index 0000000..fbaaedd --- /dev/null +++ b/app/public/api/live-now.php @@ -0,0 +1,44 @@ +get('timezone', 'Europe/Vienna'); + +$cacheFile = '/var/www/cache/guide.json'; + +if (!file_exists($cacheFile)) { + http_response_code(404); + echo json_encode([ + 'error' => 'guide.json not found', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +$data = json_decode(file_get_contents($cacheFile), true); + +if (!is_array($data)) { + http_response_code(500); + echo json_encode([ + 'error' => 'guide.json is invalid', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +$service = new ProgrammeService($timezone); +$items = $service->getCurrentProgrammes($data, $config); + +$now = new DateTimeImmutable('now', new DateTimeZone($timezone)); + +echo json_encode([ + 'now' => $now->format(DateTimeInterface::ATOM), + 'timestamp' => $now->getTimestamp(), + 'items' => $items, +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/app/public/assets/js/live-now.js b/app/public/assets/js/live-now.js new file mode 100644 index 0000000..5abbf4b --- /dev/null +++ b/app/public/assets/js/live-now.js @@ -0,0 +1,61 @@ +function nowCreateCard(item) { + const article = document.createElement('article'); + article.className = 'now-card'; + + article.innerHTML = ` +
+ + +
+ +

+ +
+ +
+ +
+
+
+ `; + + nowUpdateCard(article, item); + return article; +} + +function nowUpdateCard(card, item) { + card.querySelector('[data-live-channel-name]').textContent = item.channel_name; + card.querySelector('[data-live-favorite]').textContent = item.favorite ? '⭐' : ''; + card.querySelector('[data-live-title]').textContent = item.title; + + const subtitle = card.querySelector('[data-live-subtitle]'); + subtitle.textContent = item.subtitle || ''; + subtitle.style.display = item.subtitle ? '' : 'none'; + + nowTickCard(card, item); +} + +function nowTickCard(card, item) { + card.querySelector('[data-live-time]').textContent = + `${TVGuideLive.formatTime(item.start)} – ${TVGuideLive.formatTime(item.stop)} · noch ${TVGuideLive.calcRemainingMinutes(item.stop)} Min.`; + + card.querySelector('[data-live-progress]').style.width = + `${TVGuideLive.calcPercent(item.start, item.stop)}%`; +} + +document.addEventListener('DOMContentLoaded', () => { + if (!window.LiveEngine) return; + + const liveNow = new LiveEngine({ + url: '/api/live-now.php', + interval: 30000, + gridSelector: '[data-live-now-grid]', + itemKey: item => item.channel_id, + createItem: nowCreateCard, + updateItem: nowUpdateCard, + tickItem: nowTickCard, + emptyHtml: '
Aktuell wurden keine laufenden Sendungen gefunden.
' + }); + + liveNow.start(); +}); diff --git a/app/public/assets/js/live.js b/app/public/assets/js/live.js new file mode 100644 index 0000000..c73fe51 --- /dev/null +++ b/app/public/assets/js/live.js @@ -0,0 +1,138 @@ +window.TVGuideLive = { + formatTime(timestamp) { + const date = new Date(timestamp * 1000); + return String(date.getHours()).padStart(2, '0') + ':' + + String(date.getMinutes()).padStart(2, '0'); + }, + + calcPercent(start, stop) { + const now = Math.floor(Date.now() / 1000); + const duration = Math.max(1, stop - start); + const elapsed = Math.max(0, now - start); + return Math.min(100, Math.max(0, Math.round((elapsed / duration) * 100))); + }, + + calcRemainingMinutes(stop) { + const now = Math.floor(Date.now() / 1000); + return Math.max(0, Math.ceil((stop - now) / 60)); + }, + + async fetchJson(url) { + const response = await fetch(url, { cache: 'no-store' }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return await response.json(); + } +}; + +window.LiveEngine = class LiveEngine { + constructor(options) { + this.url = options.url; + this.interval = options.interval ?? 30000; + this.gridSelector = options.gridSelector; + this.itemKey = options.itemKey; + this.createItem = options.createItem; + this.updateItem = options.updateItem; + this.tickItem = options.tickItem; + this.emptyHtml = options.emptyHtml ?? '
Keine Daten gefunden.
'; + + this.items = new Map(); + this.initialized = false; + this.refreshTimer = null; + this.tickTimer = null; + } + + getGrid() { + return document.querySelector(this.gridSelector); + } + + async refresh() { + const grid = this.getGrid(); + if (!grid) return; + + try { + const data = await TVGuideLive.fetchJson(this.url); + const items = data.items ?? []; + + if (items.length === 0) { + grid.innerHTML = this.emptyHtml; + this.items.clear(); + this.initialized = true; + return; + } + + if (!this.initialized) { + grid.innerHTML = ''; + this.initialized = true; + } + + const empty = grid.querySelector('.empty'); + if (empty) empty.remove(); + + this.renderDiff(grid, items); + } catch (error) { + console.error('Live-EPG konnte nicht aktualisiert werden:', error); + } + } + + renderDiff(grid, items) { + const seen = new Set(); + + for (const item of items) { + const key = String(this.itemKey(item)); + seen.add(key); + this.items.set(key, item); + + let element = grid.querySelector(`[data-live-key="${CSS.escape(key)}"]`); + + if (!element) { + element = this.createItem(item); + element.dataset.liveKey = key; + grid.appendChild(element); + } else { + this.updateItem(element, item); + } + } + + grid.querySelectorAll('[data-live-key]').forEach(element => { + const key = element.dataset.liveKey; + + if (!seen.has(key)) { + element.remove(); + this.items.delete(key); + } + }); + } + + tick() { + const grid = this.getGrid(); + if (!grid || !this.tickItem) return; + + grid.querySelectorAll('[data-live-key]').forEach(element => { + const key = element.dataset.liveKey; + const item = this.items.get(key); + + if (item) { + this.tickItem(element, item); + } + }); + } + + start() { + this.refresh(); + + this.refreshTimer = setInterval(() => this.refresh(), this.interval); + + if (this.tickItem) { + this.tickTimer = setInterval(() => this.tick(), 1000); + } + } + + stop() { + if (this.refreshTimer) clearInterval(this.refreshTimer); + if (this.tickTimer) clearInterval(this.tickTimer); + } +}; diff --git a/app/templates/layout.php b/app/templates/layout.php index 516d066..0683af8 100644 --- a/app/templates/layout.php +++ b/app/templates/layout.php @@ -20,5 +20,8 @@ + + + diff --git a/app/templates/now.php b/app/templates/now.php index 5b8614d..cdca45e 100644 --- a/app/templates/now.php +++ b/app/templates/now.php @@ -17,7 +17,7 @@ -
+