diff --git a/web/static/js/index.js b/web/static/js/index.js index f247e93..c08b1ed 100644 --- a/web/static/js/index.js +++ b/web/static/js/index.js @@ -1,42 +1,105 @@ -(function(){ +(function () { const list = document.getElementById('list'); const filter = document.getElementById('filter'); - function render(data){ - const q = (filter.value||'').toLowerCase(); + // --- wie in stream.js: mögliche Manifest-Dateien ausprobieren --- + async function chooseManifest(name) { + const enc = encodeURIComponent(name); + const candidates = [ + `/hls/${enc}/index.m3u8`, + `/hls/${enc}/main_stream.m3u8`, + ]; + for (const url of candidates) { + try { + const r = await fetch(url, { cache: 'no-store' }); + if (r.ok) return url; + } catch (_) {} + } + return null; + } + + // Kleinster gemeinsamer Nenner: API-Live ODER Manifest erreichbar + const probeCache = new Map(); // vermeidet Doppel-Requests pro Render + + async function probeLive(name) { + if (probeCache.has(name)) return probeCache.get(name); + const p = (async () => { + const url = await chooseManifest(name); + return !!url; + })(); + probeCache.set(name, p); + return p; + } + + function render(data) { + const q = (filter.value || '').toLowerCase(); list.innerHTML = ''; data.items - .filter(it => !q || it.name.toLowerCase().includes(q)) - .forEach(it => { + .filter((it) => !q || it.name.toLowerCase().includes(q)) + .forEach((it) => { const a = document.createElement('a'); a.href = '/' + encodeURIComponent(it.name); a.className = 'card'; + a.dataset.stream = it.name; + + // Grundzustand: was die API sagt + const apiLive = !!it.live; + const pillClass = apiLive ? 'live' : 'off'; + const pillText = apiLive ? 'LIVE' : 'Offline'; + a.innerHTML = `
${it.name}
Zuschauer: ${it.viewers ?? 0}
-
${it.live ? 'LIVE' : 'Offline'}
+
${pillText}
`; + list.appendChild(a); + + // Sofort im Hintergrund „echtes“ Live prüfen und ggf. überschreiben + probeLive(it.name).then((isLive) => { + if (isLive || apiLive) { + const pill = a.querySelector('[data-role="live-pill"]'); + if (pill) { + pill.className = 'pill live'; + pill.textContent = 'LIVE'; + } + } + }); }); } - filter.addEventListener('input', ()=>{/* re-render mit letztem snapshot */} - ); + // Filter rendert jetzt wirklich neu (vorher: leerer Handler) + filter.addEventListener('input', () => render(last)); - let last = {items:[]}; + let last = { items: [] }; + + // Erst-Load (falls Server SSE erst später schickt) + (async function initialFetch() { + try { + const r = await fetch('/api/streams', { cache: 'no-store' }); + if (r.ok) { + last = await r.json(); + render(last); + } + } catch (_) {} + })(); + + // Live-Updates via SSE (wie gehabt) const es = new EventSource('/api/streams/events', { withCredentials: false }); - es.addEventListener('update', (ev)=>{ + es.addEventListener('update', (ev) => { try { last = JSON.parse(ev.data); render(last); - } catch(e) { console.warn('sse parse', e); } + } catch (e) { + console.warn('sse parse', e); + } }); - es.onerror = (e)=>console.warn('sse error', e); + es.onerror = (e) => console.warn('sse error', e); // Optionaler Reload-Button: const btn = document.getElementById('reload'); - if (btn) btn.addEventListener('click', ()=>render(last)); + if (btn) btn.addEventListener('click', () => render(last)); })();