(() => { "use strict"; const list = document.getElementById("ymHistory"); const updated = document.getElementById("ymUpdated"); const refresh = document.getElementById("ymRefresh"); const tabs = Array.from(document.querySelectorAll(".ym-tab")); if (!list || !updated) return; const SOURCES = { lb: "/api/listenbrainz/recent", ym: "/api/ym/history", }; const POLL_MS = 30000; let current = "lb"; let timer = null; let busy = false; const state = { lb: { seen: new Set(), firstLoad: true }, ym: { seen: new Set(), firstLoad: true }, }; const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[c]); const dayLabel = (t) => { if (t.nowplaying) return "сейчас"; if (!t.day) return ""; const today = new Date().toISOString().slice(0, 10); const yest = new Date(Date.now() - 86400000).toISOString().slice(0, 10); if (t.day === today) return "сегодня"; if (t.day === yest) return "вчера"; const [y, m, d] = t.day.split("-"); return `${d}.${m}.${y}`; }; const render = (tracks) => { const st = state[current]; const fresh = []; const html = tracks.map((t) => { const key = `${t.id}:${t.album_id || ""}:${t.day || ""}`; if (!st.seen.has(key)) { st.seen.add(key); if (!st.firstLoad) fresh.push(key); } const isNew = fresh.includes(key); const cover = t.cover ? `` : ``; const artists = (t.artists || []).map(escapeHtml).join(", "); const context = t.context ? ` · ${escapeHtml(t.context)}` : ""; const link = t.url ? `` : `"; return `
  • ${link} ${cover} ${escapeHtml(t.title)} ${artists || "—"}${context} ${dayLabel(t)} ${close}
  • `; }).join(""); list.innerHTML = html || '
  • Пока нет прослушиваний
  • '; st.firstLoad = false; }; const setStatus = (msg, isError) => { list.innerHTML = `
  • ${escapeHtml(msg)}
  • `; }; const tick = async () => { if (busy) return; busy = true; try { const res = await fetch(SOURCES[current], { headers: { Accept: "application/json" } }); const data = await res.json(); if (!res.ok || !data.ok) { setStatus(data.error || `Ошибка ${res.status}`, true); return; } render(data.tracks || []); const t = new Date(data.updated_at * 1000); updated.textContent = `обновлено ${t.toLocaleTimeString("ru-RU")}${data.stale ? " (кэш)" : ""}`; } catch (e) { setStatus("Нет связи с сервером", true); } finally { busy = false; if (timer) clearTimeout(timer); timer = setTimeout(tick, POLL_MS); } }; const poke = () => { if (timer) clearTimeout(timer); tick(); }; tabs.forEach((tab) => { tab.addEventListener("click", () => { const source = tab.dataset.source; if (!source || source === current) return; current = source; tabs.forEach((t) => t.classList.toggle("active", t === tab)); updated.textContent = "подключение…"; poke(); }); }); if (refresh) refresh.addEventListener("click", poke); document.addEventListener("visibilitychange", () => { if (!document.hidden) poke(); }); tick(); })();