Merge pull request #297 from DanTheMan827/copilot/fix-invalid-total-tracks
Fix per-disc track total and add total disc count to embedded metadata
This commit is contained in:
commit
8b09635272
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
js/downloads.js
101
js/downloads.js
|
|
@ -103,6 +103,63 @@ async function createDiscLayoutContext(tracks, api) {
|
|||
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) {
|
||||
return `Disc ${discNumber}`;
|
||||
}
|
||||
|
|
@ -321,15 +378,24 @@ async function downloadTrackBlob(
|
|||
// 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 {
|
||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||
if (albumData.album) {
|
||||
if (albumData.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist)) {
|
||||
enrichedTrack.album = {
|
||||
...enrichedTrack.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) {
|
||||
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);
|
||||
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) {
|
||||
|
|
@ -1132,7 +1208,8 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
|||
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
|
||||
|
||||
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 releaseDateStr =
|
||||
fullAlbum.releaseDate ||
|
||||
|
|
@ -1303,7 +1380,8 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
|||
if (signal.aborted) break;
|
||||
const album = selectedReleases[albumIndex];
|
||||
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);
|
||||
}
|
||||
completeBulkDownload(notification, true);
|
||||
|
|
@ -1447,15 +1525,24 @@ export async function downloadTrackWithMetadata(track, quality, api, lyricsManag
|
|||
// Continue with available track payload
|
||||
}
|
||||
|
||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
||||
if (enrichedTrack.album?.id) {
|
||||
try {
|
||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||
if (albumData.album) {
|
||||
if (albumData.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist)) {
|
||||
enrichedTrack.album = {
|
||||
...enrichedTrack.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) {
|
||||
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.trackNumber = track.trackNumber;
|
||||
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.isrc = track.isrc;
|
||||
data.explicit = Boolean(track.explicit);
|
||||
|
|
|
|||
Loading…
Reference in a new issue