#911: redownload via the album's CANONICAL source (covers the 67% with multiple ids)

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/<id>/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.
This commit is contained in:
BoulderBadgeDad 2026-06-23 11:20:03 -07:00
parent 69cb51cc13
commit 65f73fae92

View file

@ -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/<id>/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;