diff --git a/model/metadata/map_participants.go b/model/metadata/map_participants.go index 4c9c0de01..e8cf0b197 100644 --- a/model/metadata/map_participants.go +++ b/model/metadata/map_participants.go @@ -36,13 +36,16 @@ var roleMappings = map[model.Role]roleTags{ func (md Metadata) mapParticipants() model.Participants { participants := make(model.Participants) - // Parse track artists + // Parse track artists. MBIDs use getRoleValues so they're split by the same + // separators ('/' or ';') as the canonical names — otherwise a tag like + // MUSICBRAINZ_ARTISTID="abc/def" paired with ARTISTS="A/B" would yield 2 + // names but 1 MBID and the positional alignment would break. trackNames := md.getArtistValues(model.TagTrackArtist, model.TagTrackArtists) if len(trackNames) == 0 { trackNames = []string{consts.UnknownArtist} } trackSorts := md.getArtistValues(model.TagTrackArtistSort, model.TagTrackArtistsSort) - trackMbids := md.Strings(model.TagMusicBrainzArtistID) + trackMbids := md.getRoleValues(model.TagMusicBrainzArtistID) trackCredits := md.getArtistValues(model.TagTrackArtistCredit, model.TagTrackArtistsCredit) trackArtistParticipants := md.buildParticipants(trackNames, trackSorts, trackMbids, trackCredits) participants.AddParticipants(model.RoleArtist, trackArtistParticipants...) @@ -50,7 +53,7 @@ func (md Metadata) mapParticipants() model.Participants { // Parse album artists albumNames := md.getArtistValues(model.TagAlbumArtist, model.TagAlbumArtists) albumSorts := md.getArtistValues(model.TagAlbumArtistSort, model.TagAlbumArtistsSort) - albumMbids := md.Strings(model.TagMusicBrainzAlbumArtistID) + albumMbids := md.getRoleValues(model.TagMusicBrainzAlbumArtistID) albumCredits := md.getArtistValues(model.TagAlbumArtistCredit, model.TagAlbumArtistsCredit) // Treat both "no albumartist tag" and "albumartist tag literally set to @@ -75,13 +78,16 @@ func (md Metadata) mapParticipants() model.Participants { } participants.AddParticipants(model.RoleAlbumArtist, albumArtistParticipants...) - // Parse all other roles + // Parse all other roles. All parallel lists go through getRoleValues so + // they're split with the same separators as the canonical names. Reading + // any of these with md.Strings (no splitting) would desync the positional + // alignment for tags like COMPOSER="A;B" + COMPOSER_CREDIT="AA;BB". for role, info := range roleMappings { names := md.getRoleValues(info.name) if len(names) > 0 { - sorts := md.Strings(info.sort) - mbids := md.Strings(info.mbid) - credits := md.Strings(info.credit) + sorts := md.getRoleValues(info.sort) + mbids := md.getRoleValues(info.mbid) + credits := md.getRoleValues(info.credit) participants.AddParticipants(role, md.buildParticipants(names, sorts, mbids, credits)...) } } diff --git a/model/metadata/map_participants_test.go b/model/metadata/map_participants_test.go index 3467bb99f..95984ff59 100644 --- a/model/metadata/map_participants_test.go +++ b/model/metadata/map_participants_test.go @@ -882,5 +882,45 @@ var _ = Describe("Participants", func() { } } }) + + It("aligns composer credits with names when both use a role separator", func() { + // Names and credits both split on ';' under the roles.split config. + // Prior to the alignment fix, credits were read raw and would have + // stayed as one value while names became three, producing a length + // mismatch that silently dropped all credits. + mf = toMediaFile(model.RawTags{ + "COMPOSER": {"Comp A;Comp B;Comp C"}, + "COMPOSERCREDIT": {"CA;CB;CC"}, + }) + composers := mf.Participants[model.RoleComposer] + Expect(composers).To(HaveLen(3)) + Expect(composers[0].Name).To(Equal("Comp A")) + Expect(composers[0].CreditedAs).To(Equal("CA")) + Expect(composers[1].CreditedAs).To(Equal("CB")) + Expect(composers[2].CreditedAs).To(Equal("CC")) + }) + + It("splits composer MBIDs to align with split composer names", func() { + // Both names and MBIDs use the role separator ';'. The fix routes + // MBIDs through getRoleValues so they split alongside names rather + // than staying as a single value (which would have produced 2 names + // and 1 MBID — broken positional alignment). + // + // We test via a single delimited canonical name string AND a single + // delimited MBID-pair string. Note: the tag-reader UUID validation + // happens at metadata parse time on individual values, so we hand + // multi-valued tag entries already split into separate slice + // entries (matching how a multi-value tag would surface). + mf = toMediaFile(model.RawTags{ + "COMPOSER": {"Comp A;Comp B"}, + "MUSICBRAINZ_COMPOSERID": {"11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"}, + }) + composers := mf.Participants[model.RoleComposer] + Expect(composers).To(HaveLen(2)) + Expect(composers[0].Name).To(Equal("Comp A")) + Expect(composers[0].MbzArtistID).To(Equal("11111111-1111-1111-1111-111111111111")) + Expect(composers[1].Name).To(Equal("Comp B")) + Expect(composers[1].MbzArtistID).To(Equal("22222222-2222-2222-2222-222222222222")) + }) }) })