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.versionUrl = options.versionUrl ?? null; this.version = null; 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 hasChanged() { if (!this.versionUrl) { return true; } const data = await TVGuideLive.fetchJson(this.versionUrl); const version = String(data.version ?? '0'); if (this.version === null) { this.version = version; return true; } if (this.version !== version) { this.version = version; return true; } return false; } async refresh() { const grid = this.getGrid(); if (!grid) return; if (!(await this.hasChanged())) { 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); } };