121 lines
4.5 KiB
JavaScript
121 lines
4.5 KiB
JavaScript
(() => {
|
|
"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
|
|
? `<img class="ym-cover" src="${escapeHtml(t.cover)}" alt="" loading="lazy" width="48" height="48">`
|
|
: `<span class="ym-cover ym-cover-empty" aria-hidden="true">♪</span>`;
|
|
const artists = (t.artists || []).map(escapeHtml).join(", ");
|
|
const context = t.context ? ` <span class="ym-context">· ${escapeHtml(t.context)}</span>` : "";
|
|
const link = t.url
|
|
? `<a class="ym-link" href="${escapeHtml(t.url)}" target="_blank" rel="noopener">`
|
|
: `<div class="ym-link">`;
|
|
const close = t.url ? "</a>" : "</div>";
|
|
return `<li class="ym-track${isNew ? " ym-new" : ""}${t.nowplaying ? " ym-playing" : ""}">
|
|
${link}
|
|
${cover}
|
|
<span class="ym-info">
|
|
<span class="ym-track-title">${escapeHtml(t.title)}</span>
|
|
<span class="ym-artists">${artists || "—"}${context}</span>
|
|
</span>
|
|
<span class="ym-day${t.nowplaying ? " ym-now" : ""}">${dayLabel(t)}</span>
|
|
${close}
|
|
</li>`;
|
|
}).join("");
|
|
|
|
list.innerHTML = html || '<li class="ym-status">Пока нет прослушиваний</li>';
|
|
st.firstLoad = false;
|
|
};
|
|
|
|
const setStatus = (msg, isError) => {
|
|
list.innerHTML = `<li class="ym-status${isError ? " ym-error" : ""}">${escapeHtml(msg)}</li>`;
|
|
};
|
|
|
|
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();
|
|
})();
|