This commit is contained in:
@@ -1,42 +1,105 @@
|
|||||||
(function(){
|
(function () {
|
||||||
const list = document.getElementById('list');
|
const list = document.getElementById('list');
|
||||||
const filter = document.getElementById('filter');
|
const filter = document.getElementById('filter');
|
||||||
|
|
||||||
function render(data){
|
// --- wie in stream.js: mögliche Manifest-Dateien ausprobieren ---
|
||||||
const q = (filter.value||'').toLowerCase();
|
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 = '';
|
list.innerHTML = '';
|
||||||
data.items
|
data.items
|
||||||
.filter(it => !q || it.name.toLowerCase().includes(q))
|
.filter((it) => !q || it.name.toLowerCase().includes(q))
|
||||||
.forEach(it => {
|
.forEach((it) => {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = '/' + encodeURIComponent(it.name);
|
a.href = '/' + encodeURIComponent(it.name);
|
||||||
a.className = 'card';
|
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 = `
|
a.innerHTML = `
|
||||||
<div class="row space-between">
|
<div class="row space-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="title-strong">${it.name}</div>
|
<div class="title-strong">${it.name}</div>
|
||||||
<div class="muted">Zuschauer: ${it.viewers ?? 0}</div>
|
<div class="muted">Zuschauer: ${it.viewers ?? 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pill ${it.live ? 'live':'off'}">${it.live ? 'LIVE' : 'Offline'}</div>
|
<div class="pill ${pillClass}" data-role="live-pill">${pillText}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
list.appendChild(a);
|
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 });
|
const es = new EventSource('/api/streams/events', { withCredentials: false });
|
||||||
es.addEventListener('update', (ev)=>{
|
es.addEventListener('update', (ev) => {
|
||||||
try {
|
try {
|
||||||
last = JSON.parse(ev.data);
|
last = JSON.parse(ev.data);
|
||||||
render(last);
|
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:
|
// Optionaler Reload-Button:
|
||||||
const btn = document.getElementById('reload');
|
const btn = document.getElementById('reload');
|
||||||
if (btn) btn.addEventListener('click', ()=>render(last));
|
if (btn) btn.addEventListener('click', () => render(last));
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user