From b910e32d6c289e784409fdfc00e70cbb7a1d565f Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Fri, 10 Apr 2026 10:18:19 -0400 Subject: [PATCH 1/3] text representation and disambiguation to releases --- discover.go | 12 +- static/app.js | 195 +++++++------ static/style.css | 736 ++++++++++++++++++++++++++++------------------- 3 files changed, 552 insertions(+), 391 deletions(-) diff --git a/discover.go b/discover.go index cf4e653..2158ed5 100644 --- a/discover.go +++ b/discover.go @@ -26,10 +26,14 @@ type mbMedia struct { } type mbRelease struct { - ID string `json:"id"` - Title string `json:"title"` - Date string `json:"date"` - Country string `json:"country"` + ID string `json:"id"` + Title string `json:"title"` + Date string `json:"date"` + Country string `json:"country"` + Disambiguation string `json:"disambiguation"` + TextRepresentation struct { + Language string `json:"language"` + } `json:"text-representation"` Media []mbMedia `json:"media"` ArtistCredit []mbArtistCredit `json:"artist-credit"` ReleaseGroup struct { diff --git a/static/app.js b/static/app.js index c06e801..e4e4104 100644 --- a/static/app.js +++ b/static/app.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; // IDs of fetch cards we've already created, so we don't duplicate them. const knownFetchIds = new Set(); -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener("DOMContentLoaded", () => { initTabs(); initSearch(); initFetchList(); @@ -12,105 +12,124 @@ document.addEventListener('DOMContentLoaded', () => { // ── Tabs ─────────────────────────────────────────────────────────────────────── function initTabs() { - document.querySelector('.tabs').addEventListener('click', e => { - const btn = e.target.closest('.tab-btn'); + 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'); + 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'; +let searchType = "release"; function initSearch() { - document.querySelector('.type-toggle').addEventListener('click', e => { - const btn = e.target.closest('.type-btn'); + 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(); }); + 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'); + 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); + 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); + document.querySelectorAll(".type-btn").forEach((b) => { + b.classList.toggle("active", b.dataset.type === type); }); } function doSearch() { - const q = document.getElementById('search-q').value.trim(); + const q = document.getElementById("search-q").value.trim(); if (!q) return; - const btn = document.getElementById('search-btn'); - const resultsEl = document.getElementById('search-results'); + const btn = document.getElementById("search-btn"); + const resultsEl = document.getElementById("search-results"); btn.disabled = true; - btn.textContent = 'Searching\u2026'; + 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); }); + .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 => { + .then((data) => renderResults(data)) + .catch((err) => { resultsEl.innerHTML = `

Error: ${esc(err.message)}

`; }) .finally(() => { btn.disabled = false; - btn.textContent = 'Search'; + btn.textContent = "Search"; }); } // ── Results rendering ────────────────────────────────────────────────────────── function renderResults(data) { - const el = document.getElementById('search-results'); + 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(''); + 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 meta = [year, type, formats, country].filter(Boolean).join(' \u00b7 '); + 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)} - ${meta ? `${esc(meta)}` : ''} + ${esc(artist)} \u2014 ${esc(r.title)}${dis} + ${meta ? `${esc(meta)}` : ""}