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>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/assets/js/app.js"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="now-grid">
|
<div class="now-grid" data-live-now-grid>
|
||||||
<?php foreach ($currentProgrammes as $item): ?>
|
<?php foreach ($currentProgrammes as $item): ?>
|
||||||
<article class="now-card">
|
<article class="now-card">
|
||||||
<div class="now-channel">
|
<div class="now-channel">
|
||||||
|
|||||||
Reference in New Issue
Block a user