"use strict"; // IDs of fetch cards we've already created, so we don't duplicate them. const knownFetchIds = new Set(); document.addEventListener("DOMContentLoaded", () => { initTabs(); initSearch(); initFetchList(); }); // ── Tabs ─────────────────────────────────────────────────────────────────────── function initTabs() { document.querySelector(".tabs").addEventListener("click", (e) => { const btn = e.target.closest(".tab-btn"); if (!btn) return; showTab(btn.dataset.tab); }); } function showTab(name) { document .querySelectorAll(".tab-pane") .forEach((p) => p.classList.remove("active")); document .querySelectorAll(".tab-btn") .forEach((b) => b.classList.remove("active")); document.getElementById("tab-" + name).classList.add("active"); document .querySelector(`.tab-btn[data-tab="${name}"]`) .classList.add("active"); } // ── Search ───────────────────────────────────────────────────────────────────── let searchType = "release"; function initSearch() { document.querySelector(".type-toggle").addEventListener("click", (e) => { const btn = e.target.closest(".type-btn"); if (btn) setSearchType(btn.dataset.type); }); const searchBtn = document.getElementById("search-btn"); const searchInput = document.getElementById("search-q"); searchBtn.addEventListener("click", doSearch); searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doSearch(); }); // Event delegation for dynamically rendered result buttons document.getElementById("search-results").addEventListener("click", (e) => { const btn = e.target.closest(".fetch-btn"); if (!btn || btn.disabled) return; if (btn.dataset.fetchType === "artist") startArtistFetch(btn); else startReleaseFetch(btn); }); } function setSearchType(type) { searchType = type; document.querySelectorAll(".type-btn").forEach((b) => { b.classList.toggle("active", b.dataset.type === type); }); } function doSearch() { const q = document.getElementById("search-q").value.trim(); if (!q) return; const btn = document.getElementById("search-btn"); const resultsEl = document.getElementById("search-results"); btn.disabled = true; btn.textContent = "Searching\u2026"; resultsEl.innerHTML = '

Searching MusicBrainz\u2026

'; fetch(`/discover/search?q=${encodeURIComponent(q)}&type=${searchType}`) .then((r) => { if (!r.ok) return r.text().then((t) => { throw new Error(t || r.statusText); }); return r.json(); }) .then((data) => renderResults(data)) .catch((err) => { resultsEl.innerHTML = `

Error: ${esc(err.message)}

`; }) .finally(() => { btn.disabled = false; btn.textContent = "Search"; }); } // ── Results rendering ────────────────────────────────────────────────────────── function renderResults(data) { const el = document.getElementById("search-results"); if (!data || data.length === 0) { el.innerHTML = '

No results found.

'; return; } const renderer = searchType === "artist" ? renderArtist : renderRelease; el.innerHTML = data.map(renderer).join(""); } function renderRelease(r) { const credits = r["artist-credit"] ?? []; const artist = credits.map((c) => c.name || c.artist?.name || "").join("") || "Unknown Artist"; const year = r.date?.substring(0, 4) ?? ""; const type = r["release-group"]?.["primary-type"] ?? ""; const country = r.country ?? ""; const formats = [ ...new Set((r.media ?? []).map((m) => m.format).filter(Boolean)), ].join("+"); const lang = r["text-representation"]?.language ?? ""; const meta = [year, type, formats, country, lang] .filter(Boolean) .join(" \u00b7 "); const dis = r.disambiguation ? ` (${esc(r.disambiguation)})` : ""; const coverUrl = `https://coverartarchive.org/release/${r.id}/front-250`; return `
${esc(artist)} \u2014 ${esc(r.title)}${dis} ${meta ? `${esc(meta)}` : ""}
`; } function renderArtist(a) { const dis = a.disambiguation ? ` (${esc(a.disambiguation)})` : ""; return `
${esc(a.name)}${dis} ${a.country ? `${esc(a.country)}` : ""}
`; } // ── Fetch operations ─────────────────────────────────────────────────────────── function startReleaseFetch(btn) { const { id, artist, album } = btn.dataset; btn.disabled = true; btn.textContent = "Fetching\u2026"; fetch("/discover/fetch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, artist, album }), }) .then((r) => { if (!r.ok) return r.text().then((t) => { throw new Error(t || r.statusText); }); return r.json(); }) .then(() => { addFetchCard(id, `${artist} \u2014 ${album}`); pollFetch(id); }) .catch((err) => { btn.disabled = false; btn.textContent = "Fetch"; showFetchError(err.message); }); } function startArtistFetch(btn) { const { id, name } = btn.dataset; btn.disabled = true; btn.textContent = "Fetching\u2026"; fetch("/discover/fetch/artist", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, name }), }) .then((r) => { if (!r.ok) return r.text().then((t) => { throw new Error(t || r.statusText); }); return r.json(); }) .then(() => { addFetchCard(id, `${name} \u2014 full discography`); pollFetch(id); }) .catch((err) => { btn.disabled = false; btn.textContent = "Fetch All"; showFetchError(err.message); }); } // ── Fetch cards ──────────────────────────────────────────────────────────────── function addFetchCard(id, title) { knownFetchIds.add(id); const list = document.getElementById("fetch-list"); const card = document.createElement("div"); card.className = "fetch-card"; card.id = `fetch-${id}`; card.innerHTML = `
${esc(title)} In progress\u2026
`; list.prepend(card); } function pollFetch(id) { fetch(`/discover/fetch/status?id=${encodeURIComponent(id)}`) .then((r) => r.json()) .then((data) => { const logEl = document.getElementById(`flog-${id}`); const statusEl = document.getElementById(`fstatus-${id}`); const card = document.getElementById(`fetch-${id}`); if (logEl && data.log) { logEl.innerHTML = data.log .map((l) => `
${esc(l)}
`) .join(""); logEl.scrollTop = logEl.scrollHeight; } if (data.done) { if (data.success) { statusEl?.setAttribute("class", "fetch-status fetch-status-ok"); if (statusEl) statusEl.textContent = "\u2713 done"; card?.classList.add("fetch-card-ok"); } else { statusEl?.setAttribute("class", "fetch-status fetch-status-err"); if (statusEl) statusEl.textContent = "\u2717 failed"; card?.classList.add("fetch-card-err"); if (data.error && logEl) { logEl.innerHTML += `
${esc(data.error)}
`; logEl.scrollTop = logEl.scrollHeight; } } } else { setTimeout(() => pollFetch(id), 2000); } }) .catch(() => setTimeout(() => pollFetch(id), 3000)); } // ── Fetch list polling ───────────────────────────────────────────────────────── // Polls /discover/fetch/list every 5 s to discover server-created fetch entries // (e.g. per-album cards spawned during an artist fetch) and create cards for them. function initFetchList() { pollFetchList(); } function pollFetchList() { fetch("/discover/fetch/list") .then((r) => (r.ok ? r.json() : null)) .then((items) => { if (!items) return; for (const item of items) { if (!knownFetchIds.has(item.id)) { knownFetchIds.add(item.id); addFetchCard(item.id, item.title); if (!item.done) pollFetch(item.id); } } }) .catch(() => {}) .finally(() => setTimeout(pollFetchList, 5000)); } // ── Utilities ────────────────────────────────────────────────────────────────── function showFetchError(msg) { const list = document.getElementById("fetch-list"); const el = document.createElement("div"); el.className = "fetch-card fetch-card-err"; el.innerHTML = `
Fetch failed \u2717 error
${esc(msg)}
`; list.prepend(el); } function esc(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); }