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:
Samidy 2026-03-12 04:55:26 +03:00 committed by GitHub
commit 8b09635272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 145 additions and 8 deletions

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);