Live Ansicht
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
require_once '/var/www/src/Config.php';
|
||||
require_once '/var/www/src/Services/ProgrammeService.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$config = new Config(
|
||||
'/var/www/config/config.json',
|
||||
'/var/www/config/channels.json'
|
||||
);
|
||||
|
||||
$timezone = $config->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);
|
||||
@@ -0,0 +1,61 @@
|
||||
function nowCreateCard(item) {
|
||||
const article = document.createElement('article');
|
||||
article.className = 'now-card';
|
||||
|
||||
article.innerHTML = `
|
||||
<div class="now-channel">
|
||||
<span data-live-channel-name></span>
|
||||
<span data-live-favorite></span>
|
||||
</div>
|
||||
|
||||
<h3 data-live-title></h3>
|
||||
|
||||
<div class="now-subtitle" data-live-subtitle></div>
|
||||
|
||||
<div class="now-time" data-live-time></div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" data-live-progress></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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: '<div class="empty">Aktuell wurden keine laufenden Sendungen gefunden.</div>'
|
||||
});
|
||||
|
||||
liveNow.start();
|
||||
});
|
||||
@@ -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 ?? '<div class="empty">Keine Daten gefunden.</div>';
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -20,5 +20,8 @@
|
||||
</div>
|
||||
|
||||
<script type="module" src="/assets/js/app.js"></script>
|
||||
<script src="/assets/js/live.js"></script>
|
||||
<script src="/assets/js/live-now.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="now-grid">
|
||||
<div class="now-grid" data-live-now-grid>
|
||||
<?php foreach ($currentProgrammes as $item): ?>
|
||||
<article class="now-card">
|
||||
<div class="now-channel">
|
||||
|
||||
Reference in New Issue
Block a user