Files
Black_Raven 296ef629a8
Build, Test and Push Flask App / docker-build-and-push (push) Successful in 1m28s
Build, Test and Push Flask App / deploy (push) Successful in 1m21s
fix(static): fix view
2026-10-12 14:05:45 +03:00

154 lines
6.0 KiB
JavaScript

(() => {
"use strict";
const list = document.getElementById("ymHistory");
const updated = document.getElementById("ymUpdated");
const refresh = document.getElementById("ymRefresh");
const nowBody = document.getElementById("ymNowBody");
const nowEl = document.getElementById("ymNow");
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) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
})[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
? `<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>';
firstLoad = false;
};
const setStatus = (msg, isError) => {
list.innerHTML = `<li class="ym-status${isError ? " ym-error" : ""}">${escapeHtml(msg)}</li>`;
};
const renderNow = (track) => {
if (!nowBody) return;
if (nowEl) nowEl.style.display = track ? "" : "none";
if (!track) return;
const cover = track.cover
? `<img class="ym-now-cover" src="${escapeHtml(track.cover)}" alt="" loading="lazy" width="72" height="72">`
: `<span class="ym-now-cover ym-cover-empty" aria-hidden="true">♪</span>`;
const artists = (track.artists || []).map(escapeHtml).join(", ");
const context = track.context ? ` <span class="ym-context">· ${escapeHtml(track.context)}</span>` : "";
const inner = `
${cover}
<span class="ym-now-info">
<span class="ym-track-title">${escapeHtml(track.title)}</span>
<span class="ym-artists">${artists || "—"}${context}</span>
</span>`;
nowBody.innerHTML = track.url
? `<a class="ym-now-link" href="${escapeHtml(track.url)}" target="_blank" rel="noopener">${inner}</a>`
: `<div class="ym-now-link">${inner}</div>`;
};
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 = `<span class="ym-now-idle">${escapeHtml(data.error || `Ошибка ${res.status}`)}</span>`;
return;
}
renderNow(data.track);
} catch (e) {
if (nowBody) nowBody.innerHTML = '<span class="ym-now-idle">нет связи</span>';
} 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();
})();