From dca1c235dcb8c98eb6abc7359d7d869d6b0f20ea Mon Sep 17 00:00:00 2001 From: Chad Miller Date: Fri, 19 Dec 2025 21:24:56 -0800 Subject: [PATCH] feat(teal-scrobble): improve track search with smart album matching - Show all album versions for each track in dropdown - Sort results by quality: official albums first, compilations last - Prefer original releases (earliest date) over reissues - Dedupe to show one entry per album (keeping best version) - Remove separate album search field (track dropdown now includes album) - Make selected artist/track tags full width - Increase search limit to 50 recordings --- teal-scrobble.html | 210 ++++++++++++++++----------------------------- 1 file changed, 76 insertions(+), 134 deletions(-) diff --git a/teal-scrobble.html b/teal-scrobble.html index 510ba15..3a8d750 100644 --- a/teal-scrobble.html +++ b/teal-scrobble.html @@ -228,14 +228,16 @@ /* Selected Tag */ .selected-tag { - display: inline-flex; + display: flex; align-items: center; + justify-content: space-between; gap: 0.5rem; background: var(--bg-hover); border: 1px solid var(--accent); border-radius: 0.375rem; - padding: 0.5rem 0.75rem; + padding: 0.75rem; color: var(--text-primary); + width: 100%; } .selected-tag button { @@ -579,7 +581,7 @@ if (query.length < 2) return []; const fullQuery = `${query} AND arid:${artistMbid}`; - const url = `${MB_API}/recording?query=${encodeURIComponent(fullQuery)}&fmt=json&limit=20`; + const url = `${MB_API}/recording?query=${encodeURIComponent(fullQuery)}&fmt=json&limit=50`; const res = await fetch(url, { headers: MB_HEADERS }); if (!res.ok) throw new Error("MusicBrainz search failed"); @@ -588,19 +590,6 @@ return data.recordings || []; } - async function searchReleases(query, artistMbid) { - if (query.length < 2) return []; - - const fullQuery = `${query} AND arid:${artistMbid}`; - const url = `${MB_API}/release?query=${encodeURIComponent(fullQuery)}&fmt=json&limit=10`; - const res = await fetch(url, { headers: MB_HEADERS }); - - if (!res.ok) throw new Error("MusicBrainz search failed"); - - const data = await res.json(); - return data.releases || []; - } - function debounce(fn, ms) { return (...args) => { clearTimeout(searchTimeout); @@ -804,15 +793,9 @@
-
-
- -
-
-
- - -
+
+ +
@@ -841,7 +824,6 @@ renderArtistField(); renderTrackField(); - renderAlbumField(); } function renderArtistField() { @@ -874,9 +856,12 @@ const container = document.getElementById("track-field"); if (state.selectedRecording) { + const albumText = state.selectedRecording.releaseName + ? ` · ${esc(state.selectedRecording.releaseName)}` + : ""; container.innerHTML = `
- ${esc(state.selectedRecording.title)} + ${esc(state.selectedRecording.title)}${albumText}
`; @@ -898,27 +883,6 @@ } } - function renderAlbumField() { - const container = document.getElementById("album-field"); - const disabled = !state.selectedArtist; - const albumName = state.selectedRecording?.releaseName || ""; - - container.innerHTML = ` -
- - -
- `; - } - // ============================================================================= // ARTIST SEARCH HANDLERS // ============================================================================= @@ -977,7 +941,6 @@ renderArtistField(); renderTrackField(); - renderAlbumField(); updateSubmitButton(); } @@ -988,7 +951,6 @@ renderArtistField(); renderTrackField(); - renderAlbumField(); updateSubmitButton(); } @@ -1020,9 +982,60 @@ return; } - dropdown.innerHTML = recordings - .map((r, i) => { - const release = r.releases?.[0]; + // Flatten recordings into recording+release pairs + let items = []; + recordings.forEach((r) => { + const releases = r.releases || []; + if (releases.length === 0) { + items.push({ recording: r, release: null }); + } else { + releases.forEach((release) => { + items.push({ recording: r, release }); + }); + } + }); + + // Sort: prefer official albums without secondary types, then by date + items.sort((a, b) => { + const aRelease = a.release || {}; + const bRelease = b.release || {}; + const aGroup = aRelease["release-group"] || {}; + const bGroup = bRelease["release-group"] || {}; + + // Prefer official status + const aOfficial = aRelease.status === "Official" ? 0 : 1; + const bOfficial = bRelease.status === "Official" ? 0 : 1; + if (aOfficial !== bOfficial) return aOfficial - bOfficial; + + // Prefer albums without secondary types (not compilations) + const aIsCompilation = (aGroup["secondary-types"] || []).length > 0 ? 1 : 0; + const bIsCompilation = (bGroup["secondary-types"] || []).length > 0 ? 1 : 0; + if (aIsCompilation !== bIsCompilation) return aIsCompilation - bIsCompilation; + + // Prefer primary type "Album" + const aIsAlbum = aGroup["primary-type"] === "Album" ? 0 : 1; + const bIsAlbum = bGroup["primary-type"] === "Album" ? 0 : 1; + if (aIsAlbum !== bIsAlbum) return aIsAlbum - bIsAlbum; + + // Prefer earlier release date (original release) + const aDate = aRelease.date || "9999"; + const bDate = bRelease.date || "9999"; + return aDate.localeCompare(bDate); + }); + + // Dedupe after sorting - keep first (best) occurrence of each track+album + const seen = new Set(); + items = items.filter((item) => { + const key = `${item.recording.title}|${item.release?.title || ""}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + dropdown.innerHTML = items + .map((item, i) => { + const r = item.recording; + const release = item.release; const duration = r.length ? formatDuration(Math.floor(r.length / 1000)) : ""; const album = release?.title || ""; const artUrl = release?.id @@ -1030,33 +1043,34 @@ : ""; return ` -
+
${artUrl ? `` : ""}
-
${esc(r.title)} ${duration ? `${duration}` : ""}
- ${album ? `
${esc(album)}
` : ""} +
${esc(r.title)}${album ? ` · ${esc(album)}` : ""}
+ ${duration ? `
${duration}
` : ""}
`; }) .join(""); - dropdown.dataset.recordings = JSON.stringify(recordings); + dropdown.dataset.items = JSON.stringify(items); } catch (error) { dropdown.innerHTML = `
Search failed
`; } }, 300); - function selectRecording(index) { + function selectRecordingItem(index) { const dropdown = document.getElementById("track-dropdown"); - const recordings = JSON.parse(dropdown.dataset.recordings || "[]"); - const recording = recordings[index]; + const items = JSON.parse(dropdown.dataset.items || "[]"); + const item = items[index]; - if (!recording) return; + if (!item) return; - const release = recording.releases?.[0]; + const recording = item.recording; + const release = item.release; const durationSecs = recording.length ? Math.floor(recording.length / 1000) : null; state.selectedRecording = { @@ -1076,7 +1090,6 @@ : ""; renderTrackField(); - renderAlbumField(); updateSubmitButton(); } @@ -1085,7 +1098,6 @@ document.getElementById("duration-display").value = ""; renderTrackField(); - renderAlbumField(); updateSubmitButton(); } @@ -1096,76 +1108,6 @@ return `${m}:${s.toString().padStart(2, "0")}`; } - // ============================================================================= - // ALBUM SEARCH HANDLERS - // ============================================================================= - - const handleAlbumInput = debounce(async (query) => { - const dropdown = document.getElementById("album-dropdown"); - - // Update the release name in state as user types - if (state.selectedRecording) { - state.selectedRecording.releaseName = query || null; - state.selectedRecording.releaseMbid = null; - } - - if (!state.selectedArtist || query.length < 2) { - dropdown.classList.add("hidden"); - return; - } - - dropdown.innerHTML = `
Searching...
`; - dropdown.classList.remove("hidden"); - - try { - const releases = await searchReleases(query, state.selectedArtist.mbid); - - if (releases.length === 0) { - dropdown.innerHTML = `
No albums found
`; - return; - } - - dropdown.innerHTML = releases - .map((r, i) => { - const date = r.date ? r.date.substring(0, 4) : ""; - const artUrl = r.id ? `https://coverartarchive.org/release/${r.id}/front-250` : ""; - - return ` -
-
- ${artUrl ? `` : ""} -
-
-
${esc(r.title)}
- ${date ? `
${date}
` : ""} -
-
- `; - }) - .join(""); - - dropdown.dataset.releases = JSON.stringify(releases); - } catch (error) { - dropdown.innerHTML = `
Search failed
`; - } - }, 300); - - function selectRelease(index) { - const dropdown = document.getElementById("album-dropdown"); - const releases = JSON.parse(dropdown.dataset.releases || "[]"); - const release = releases[index]; - - if (!release) return; - - if (state.selectedRecording) { - state.selectedRecording.releaseName = release.title; - state.selectedRecording.releaseMbid = release.id; - } - - dropdown.classList.add("hidden"); - document.getElementById("album-input").value = release.title; - } - // ============================================================================= // TIME TOGGLE AND SUBMIT // ============================================================================= -- knotmirror 0.0.1