fix: correct total tracks per disc and add total discs to metadata for multi-disc albums
This commit is contained in:
parent
2a01fe3227
commit
e1d7744ab2
3 changed files with 145 additions and 8 deletions
49
js/api.js
49
js/api.js
|
|
@ -1489,6 +1489,55 @@ export class LosslessAPI {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
track.album?.id &&
|
||||||
|
(track.album?.totalDiscs == null || track.album?.numberOfTracksOnDisc == null)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Broad disc-field resolver — mirrors getExplicitTrackDiscNumber in downloads.js
|
||||||
|
const resolveDiscNumber = (t) => {
|
||||||
|
const candidates = [
|
||||||
|
t.volumeNumber,
|
||||||
|
t.discNumber,
|
||||||
|
t.mediaNumber,
|
||||||
|
t.media_number,
|
||||||
|
t.volume,
|
||||||
|
t.disc,
|
||||||
|
t.disc_no,
|
||||||
|
t.discNo,
|
||||||
|
t.disc_number,
|
||||||
|
t.mediaMetadata?.discNumber,
|
||||||
|
];
|
||||||
|
for (const c of candidates) {
|
||||||
|
const parsed = parseInt(c, 10);
|
||||||
|
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const albumData = await this.getAlbum(track.album.id);
|
||||||
|
if (albumData.tracks?.length > 0) {
|
||||||
|
const discTrackCounts = new Map();
|
||||||
|
let maxDiscNumber = 0;
|
||||||
|
for (const t of albumData.tracks) {
|
||||||
|
const dn = resolveDiscNumber(t);
|
||||||
|
discTrackCounts.set(dn, (discTrackCounts.get(dn) || 0) + 1);
|
||||||
|
if (dn > maxDiscNumber) maxDiscNumber = dn;
|
||||||
|
}
|
||||||
|
const totalDiscs = maxDiscNumber || 1;
|
||||||
|
const discNumber = resolveDiscNumber(track);
|
||||||
|
enrichedTrack.album = {
|
||||||
|
...(enrichedTrack.album || {}),
|
||||||
|
totalDiscs: track.album?.totalDiscs ?? totalDiscs,
|
||||||
|
numberOfTracksOnDisc:
|
||||||
|
track.album?.numberOfTracksOnDisc ?? discTrackCounts.get(discNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch album for disc info:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises);
|
blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
101
js/downloads.js
101
js/downloads.js
|
|
@ -103,6 +103,63 @@ async function createDiscLayoutContext(tracks, api) {
|
||||||
return { separateByDisc: false, resolveDiscNumber: () => 1 };
|
return { separateByDisc: false, resolveDiscNumber: () => 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function computeDiscInfo(tracks, api = null) {
|
||||||
|
// First pass: collect explicit disc numbers from the raw track objects.
|
||||||
|
const explicitDiscNumbers = tracks.map((track) => getExplicitTrackDiscNumber(track));
|
||||||
|
const explicitDistinct = new Set(explicitDiscNumbers.filter(Boolean));
|
||||||
|
|
||||||
|
let resolvedDiscNumbers = explicitDiscNumbers;
|
||||||
|
|
||||||
|
// Some providers omit disc fields in the album payload. When we can't
|
||||||
|
// distinguish discs from the raw data and an API instance is provided,
|
||||||
|
// hydrate missing disc numbers via full-track metadata (mirrors the logic
|
||||||
|
// in createDiscLayoutContext).
|
||||||
|
if (explicitDistinct.size <= 1 && api) {
|
||||||
|
const hydratedDiscNumbers = await Promise.all(
|
||||||
|
tracks.map(async (track, index) => {
|
||||||
|
if (explicitDiscNumbers[index]) return explicitDiscNumbers[index];
|
||||||
|
try {
|
||||||
|
const fullTrack = await api.getTrackMetadata(track.id);
|
||||||
|
return getExplicitTrackDiscNumber(fullTrack);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const hydratedDistinct = new Set(hydratedDiscNumbers.filter(Boolean));
|
||||||
|
if (hydratedDistinct.size > 1) {
|
||||||
|
resolvedDiscNumbers = hydratedDiscNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracksPerDisc = new Map();
|
||||||
|
let maxDiscNumber = 0;
|
||||||
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
|
const discNumber = resolvedDiscNumbers[i] || 1;
|
||||||
|
tracksPerDisc.set(discNumber, (tracksPerDisc.get(discNumber) || 0) + 1);
|
||||||
|
if (discNumber > maxDiscNumber) {
|
||||||
|
maxDiscNumber = discNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { totalDiscs: maxDiscNumber || 1, tracksPerDisc, resolvedDiscNumbers };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function annotateTracksWithDiscInfo(tracks, api = null) {
|
||||||
|
const { totalDiscs, tracksPerDisc, resolvedDiscNumbers } = await computeDiscInfo(tracks, api);
|
||||||
|
return tracks.map((track, index) => {
|
||||||
|
const discNumber = resolvedDiscNumbers[index] || 1;
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
album: {
|
||||||
|
...(track.album || {}),
|
||||||
|
totalDiscs,
|
||||||
|
numberOfTracksOnDisc: tracksPerDisc.get(discNumber),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getDiscFolderName(discNumber) {
|
function getDiscFolderName(discNumber) {
|
||||||
return `Disc ${discNumber}`;
|
return `Disc ${discNumber}`;
|
||||||
}
|
}
|
||||||
|
|
@ -321,15 +378,24 @@ async function downloadTrackBlob(
|
||||||
// Non-fatal: continue with best available track payload
|
// Non-fatal: continue with best available track payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
if (enrichedTrack.album?.id) {
|
||||||
try {
|
try {
|
||||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||||
if (albumData.album) {
|
if (albumData.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist)) {
|
||||||
enrichedTrack.album = {
|
enrichedTrack.album = {
|
||||||
...enrichedTrack.album,
|
...enrichedTrack.album,
|
||||||
...albumData.album,
|
...albumData.album,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (albumData.tracks?.length > 0) {
|
||||||
|
const { totalDiscs, tracksPerDisc } = await computeDiscInfo(albumData.tracks, api);
|
||||||
|
const discNumber = getExplicitTrackDiscNumber(enrichedTrack) || 1;
|
||||||
|
enrichedTrack.album = {
|
||||||
|
...enrichedTrack.album,
|
||||||
|
totalDiscs,
|
||||||
|
numberOfTracksOnDisc: tracksPerDisc.get(discNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch album data for metadata:', error);
|
console.warn('Failed to fetch album data for metadata:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -1090,7 +1156,17 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana
|
||||||
});
|
});
|
||||||
|
|
||||||
const coverBlob = await getCoverBlob(api, album.cover || album.album?.cover || album.coverId);
|
const coverBlob = await getCoverBlob(api, album.cover || album.album?.cover || album.coverId);
|
||||||
await startBulkDownload(tracks, folderName, api, quality, lyricsManager, 'album', album.title, coverBlob, album);
|
await startBulkDownload(
|
||||||
|
await annotateTracksWithDiscInfo(tracks, api),
|
||||||
|
folderName,
|
||||||
|
api,
|
||||||
|
quality,
|
||||||
|
lyricsManager,
|
||||||
|
'album',
|
||||||
|
album.title,
|
||||||
|
coverBlob,
|
||||||
|
album
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyricsManager = null) {
|
export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyricsManager = null) {
|
||||||
|
|
@ -1132,7 +1208,8 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
|
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { album: fullAlbum, tracks } = await api.getAlbum(album.id);
|
const { album: fullAlbum, tracks: rawTracks } = await api.getAlbum(album.id);
|
||||||
|
const tracks = await annotateTracksWithDiscInfo(rawTracks, api);
|
||||||
const coverBlob = await getCoverBlob(api, fullAlbum.cover || album.cover);
|
const coverBlob = await getCoverBlob(api, fullAlbum.cover || album.cover);
|
||||||
const releaseDateStr =
|
const releaseDateStr =
|
||||||
fullAlbum.releaseDate ||
|
fullAlbum.releaseDate ||
|
||||||
|
|
@ -1303,7 +1380,8 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
if (signal.aborted) break;
|
if (signal.aborted) break;
|
||||||
const album = selectedReleases[albumIndex];
|
const album = selectedReleases[albumIndex];
|
||||||
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
|
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
|
||||||
const { tracks } = await api.getAlbum(album.id);
|
const { tracks: rawTracks } = await api.getAlbum(album.id);
|
||||||
|
const tracks = await annotateTracksWithDiscInfo(rawTracks, api);
|
||||||
await bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification);
|
await bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification);
|
||||||
}
|
}
|
||||||
completeBulkDownload(notification, true);
|
completeBulkDownload(notification, true);
|
||||||
|
|
@ -1447,15 +1525,24 @@ export async function downloadTrackWithMetadata(track, quality, api, lyricsManag
|
||||||
// Continue with available track payload
|
// Continue with available track payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
if (enrichedTrack.album?.id) {
|
||||||
try {
|
try {
|
||||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||||
if (albumData.album) {
|
if (albumData.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist)) {
|
||||||
enrichedTrack.album = {
|
enrichedTrack.album = {
|
||||||
...enrichedTrack.album,
|
...enrichedTrack.album,
|
||||||
...albumData.album,
|
...albumData.album,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (albumData.tracks?.length > 0) {
|
||||||
|
const { totalDiscs, tracksPerDisc } = await computeDiscInfo(albumData.tracks, api);
|
||||||
|
const discNumber = getExplicitTrackDiscNumber(enrichedTrack) || 1;
|
||||||
|
enrichedTrack.album = {
|
||||||
|
...enrichedTrack.album,
|
||||||
|
totalDiscs,
|
||||||
|
numberOfTracksOnDisc: tracksPerDisc.get(discNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch album data for metadata:', error);
|
console.warn('Failed to fetch album data for metadata:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
||||||
data.albumArtist = track.album?.artist?.name || track.artist?.name;
|
data.albumArtist = track.album?.artist?.name || track.artist?.name;
|
||||||
data.trackNumber = track.trackNumber;
|
data.trackNumber = track.trackNumber;
|
||||||
data.discNumber = track.volumeNumber ?? track.discNumber;
|
data.discNumber = track.volumeNumber ?? track.discNumber;
|
||||||
data.totalTracks = track.album.numberOfTracks;
|
data.totalTracks = track.album.numberOfTracksOnDisc ?? track.album.numberOfTracks;
|
||||||
|
data.totalDiscs = track.album.totalDiscs;
|
||||||
data.copyright = track.copyright;
|
data.copyright = track.copyright;
|
||||||
data.isrc = track.isrc;
|
data.isrc = track.isrc;
|
||||||
data.explicit = Boolean(track.explicit);
|
data.explicit = Boolean(track.explicit);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue