From 65f73fae9258c8639c1ccf4047878679e7ab1936 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 23 Jun 2026 11:20:03 -0700 Subject: [PATCH] #911: redownload via the album's CANONICAL source (covers the 67% with multiple ids) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass only checked spotify then itunes. But ~67% of a real library (46k/69k albums) carry BOTH a spotify and itunes id, and the canonical priority is spotify>deezer>itunes>mb>…, so a spotify-first guess diverges from what the Enhanced view actually tags/displays the album as (e.g. a deezer-canonical album with an itunes id too). Now redownload reuses _getEnhancedAlbumCanonicalSource (the view's single source of truth) and fetches via the same /api/album//tracks?source= endpoint the view uses for its canonical tracklist — so a redownload is always the exact edition on screen, across every source. The stored spotify/iTunes id + a last-resort search remain as fallbacks. Frontend-only; the album endpoints and canonical resolver are unchanged. --- webui/static/library.js | 85 +++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/webui/static/library.js b/webui/static/library.js index 6be1e029..080b8fc4 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -5417,14 +5417,20 @@ async function redownloadLibraryAlbum(album, artistName, btn) { const albumName = album.title || ''; const spotifyAlbumId = album.spotify_album_id || ''; const itunesAlbumId = album.itunes_album_id || ''; + // #911 — the album's CANONICAL source (the same one the Enhanced view tags + displays it as) + // wins. Redownload must pull THAT exact edition, not a fresh search that can resolve to a + // different one (issue: matched the 66-track 'Original Soundtrack Collection', a search got + // the 19-track 'Volume 1'). _getEnhancedAlbumCanonicalSource is the single source of truth + // for which source identifies this album, across spotify/deezer/itunes/musicbrainz/… + const canonical = _getEnhancedAlbumCanonicalSource(album); - if (!spotifyAlbumId && !itunesAlbumId && !albumName) { + if (!canonical && !spotifyAlbumId && !itunesAlbumId && !albumName) { showToast('No album ID or name available for redownload', 'warning'); return; } - // Fetch a specific album edition by its source id. The iTunes endpoint returns a - // Spotify-shaped payload, so all the downstream handling is identical. + // Fetch a specific album edition by its source id (the Spotify/iTunes endpoints both return + // a Spotify-shaped payload, so downstream handling is identical). const fetchAlbumBySource = (source, id, name, artist) => { const params = new URLSearchParams({ name: name || albumName, artist: artist || artistName || '' }); const base = source === 'itunes' ? '/api/itunes/album/' : '/api/spotify/album/'; @@ -5435,40 +5441,55 @@ async function redownloadLibraryAlbum(album, artistName, btn) { try { if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } - // #911 — redownload the edition the user ACTUALLY matched, using the album row's - // stored source id. A fresh search can resolve to a different edition (e.g. a - // single-volume release instead of the matched full collection), so use the - // spotify/iTunes id when present and only fall back to a search if neither exists. - let response; - if (spotifyAlbumId) { - response = await fetchAlbumBySource('spotify', spotifyAlbumId); - } else if (itunesAlbumId) { - response = await fetchAlbumBySource('itunes', itunesAlbumId); - } + let albumData = null; - if (!response || !response.ok) { - const query = `${artistName || ''} ${albumName}`.trim(); - const searchResp = await fetch('/api/enhanced-search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query }) - }); - if (!searchResp.ok) throw new Error('Album search failed'); - const searchData = await searchResp.json(); - const spotHit = searchData.spotify_albums?.[0]; - const found = spotHit || searchData.itunes_albums?.[0]; - if (!found || !found.id) { - showToast(`Could not find "${albumName}" by ${artistName || 'unknown'}`, 'warning'); - return; + // 1) Primary: the canonical tagged source (any source), via the SAME + // /api/album//tracks endpoint the Enhanced view uses for its canonical tracklist — + // so a redownload is always the album the user is actually looking at. + if (canonical) { + const params = new URLSearchParams({ name: albumName, artist: artistName || '', source: canonical.source }); + const r = await fetch(`/api/album/${encodeURIComponent(canonical.id)}/tracks?${params}`); + if (r.ok) { + const data = await r.json(); + if (data && data.success && Array.isArray(data.tracks) && data.tracks.length) { + albumData = { ...data.album, tracks: data.tracks }; // normalize to {…, tracks:[]} + } } - // Fetch from the MATCHING source endpoint — the old fallback always hit the - // Spotify endpoint, which is wrong for an iTunes search hit. - response = await fetchAlbumBySource(spotHit ? 'spotify' : 'itunes', found.id, found.name, found.artist); } - if (!response.ok) throw new Error(`Failed to load album: ${response.status}`); + // 2) Fallback: the stored spotify/iTunes id, then a last-resort search. + if (!albumData) { + let response; + if (spotifyAlbumId) { + response = await fetchAlbumBySource('spotify', spotifyAlbumId); + } else if (itunesAlbumId) { + response = await fetchAlbumBySource('itunes', itunesAlbumId); + } + + if (!response || !response.ok) { + const query = `${artistName || ''} ${albumName}`.trim(); + const searchResp = await fetch('/api/enhanced-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }) + }); + if (!searchResp.ok) throw new Error('Album search failed'); + const searchData = await searchResp.json(); + const spotHit = searchData.spotify_albums?.[0]; + const found = spotHit || searchData.itunes_albums?.[0]; + if (!found || !found.id) { + showToast(`Could not find "${albumName}" by ${artistName || 'unknown'}`, 'warning'); + return; + } + // Fetch from the MATCHING source endpoint — the old fallback always hit Spotify, + // which is wrong for an iTunes search hit. + response = await fetchAlbumBySource(spotHit ? 'spotify' : 'itunes', found.id, found.name, found.artist); + } + + if (!response.ok) throw new Error(`Failed to load album: ${response.status}`); + albumData = await response.json(); + } - const albumData = await response.json(); if (!albumData || !albumData.tracks || albumData.tracks.length === 0) { showToast(`No tracks found for "${albumName}"`, 'warning'); return;