const form = document.getElementById("load-form"); const urlInput = document.getElementById("m3u-url"); const statusEl = document.getElementById("status"); const listEl = document.getElementById("channel-list"); const searchEl = document.getElementById("search"); const videoEl = document.getElementById("video"); const nowPlaying = document.querySelector(".np-text"); const overlay = document.getElementById("video-overlay"); let channels = []; let hls = null; /* ── Load form ────────────────────────────── */ form.addEventListener("submit", async (e) => { e.preventDefault(); const url = urlInput.value.trim(); if (!url) return; setStatus("Loading playlist…", ""); listEl.innerHTML = ""; channels = []; try { const r = await fetch(`/api/parse?url=${encodeURIComponent(url)}`); const data = await r.json(); if (data.error) throw new Error(data.error); channels = data.channels; setStatus(`Loaded ${data.count} channels.`, "ok"); renderList(channels); } catch (err) { setStatus("Error: " + err.message, "error"); } }); function setStatus(msg, cls) { statusEl.textContent = msg; statusEl.className = "status " + (cls || ""); } /* ── Render list ──────────────────────────── */ function renderList(items) { listEl.innerHTML = ""; const frag = document.createDocumentFragment(); items.forEach((ch, i) => { const li = document.createElement("li"); li.style.animationDelay = Math.min(i * 12, 400) + "ms"; if (ch.logo) { const img = document.createElement("img"); img.src = ch.logo; img.loading = "lazy"; img.onerror = () => { img.style.display = "none"; }; li.appendChild(img); } const span = document.createElement("span"); span.textContent = ch.name || "(no name)"; li.appendChild(span); li.addEventListener("click", () => playChannel(ch, li)); frag.appendChild(li); }); listEl.appendChild(frag); } /* ── Search ──────────────────────────────── */ searchEl.addEventListener("input", () => { const q = searchEl.value.trim().toLowerCase(); if (!q) { renderList(channels); return; } renderList(channels.filter(c => (c.name || "").toLowerCase().includes(q))); }); /* ── Play channel ────────────────────────── */ function playChannel(ch, li) { document.querySelectorAll("#channel-list li").forEach(x => x.classList.remove("active")); li.classList.add("active"); nowPlaying.textContent = ch.name || "Playing"; overlay.classList.add("hidden"); document.querySelector(".now-playing").classList.add("live"); const proxied = `/api/proxy?url=${encodeURIComponent(ch.url)}`; if (hls) { hls.destroy(); hls = null; } if (videoEl.canPlayType("application/vnd.apple.mpegurl")) { // Safari / iOS native HLS videoEl.src = proxied; videoEl.play().catch(() => {}); return; } if (window.Hls && Hls.isSupported()) { hls = new Hls({ lowLatencyMode: true, backBufferLength: 30, maxBufferLength: 30, }); hls.loadSource(proxied); hls.attachMedia(videoEl); hls.on(Hls.Events.MANIFEST_PARSED, () => { videoEl.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (evt, data) => { if (data.fatal) { console.warn("HLS fatal error:", data); setStatus("Playback error — stream may be offline or blocked.", "error"); } }); } else { setStatus("HLS not supported in this browser.", "error"); } }