File size: 3,710 Bytes
cb118ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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");
  }
}