(() => { "use strict"; const list = document.getElementById("ymHistory"); const updated = document.getElementById("ymUpdated"); const refresh = document.getElementById("ymRefresh"); const nowBody = document.getElementById("ymNowBody"); if (!list || !updated) return; const SOURCE = "/api/listenbrainz/recent"; const NOW_SOURCE = "/api/listenbrainz/now"; const POLL_MS = 30000; const NOW_POLL_MS = 10000; let timer = null; let nowTimer = null; let busy = false; let nowBusy = false; const seen = new Set(); let 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 fresh = []; const html = tracks.map((t) => { const key = `${t.id}:${t.album_id || ""}:${t.day || ""}`; if (!seen.has(key)) { seen.add(key); if (!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 || '
  • Пока нет прослушиваний
  • '; firstLoad = false; }; const setStatus = (msg, isError) => { list.innerHTML = `
  • ${escapeHtml(msg)}
  • `; }; const renderNow = (track) => { if (!nowBody) return; if (!track) { nowBody.innerHTML = 'ничего не играет'; return; } const cover = track.cover ? `` : ``; const artists = (track.artists || []).map(escapeHtml).join(", "); const context = track.context ? ` · ${escapeHtml(track.context)}` : ""; const inner = ` ${cover} ${escapeHtml(track.title)} ${artists || "—"}${context} `; nowBody.innerHTML = track.url ? `${inner}` : ``; }; const tick = async () => { if (busy) return; busy = true; try { const res = await fetch(SOURCE, { 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 tickNow = async () => { if (nowBusy) return; nowBusy = true; try { const res = await fetch(NOW_SOURCE, { headers: { Accept: "application/json" } }); const data = await res.json(); if (!res.ok || !data.ok) { if (nowBody) nowBody.innerHTML = `${escapeHtml(data.error || `Ошибка ${res.status}`)}`; return; } renderNow(data.track); } catch (e) { if (nowBody) nowBody.innerHTML = 'нет связи'; } finally { nowBusy = false; if (nowTimer) clearTimeout(nowTimer); nowTimer = setTimeout(tickNow, NOW_POLL_MS); } }; const poke = () => { if (timer) clearTimeout(timer); if (nowTimer) clearTimeout(nowTimer); tick(); tickNow(); }; if (refresh) refresh.addEventListener("click", poke); document.addEventListener("visibilitychange", () => { if (!document.hidden) poke(); }); tick(); tickNow(); })();