diff --git a/model/metadata/map_participants.go b/model/metadata/map_participants.go index 7dd8c8c3f..b5c6e0c38 100644 --- a/model/metadata/map_participants.go +++ b/model/metadata/map_participants.go @@ -14,49 +14,58 @@ import ( ) type roleTags struct { - name model.TagName - sort model.TagName - mbid model.TagName + name model.TagName + sort model.TagName + mbid model.TagName + credit model.TagName } var roleMappings = map[model.Role]roleTags{ - model.RoleComposer: {name: model.TagComposer, sort: model.TagComposerSort, mbid: model.TagMusicBrainzComposerID}, - model.RoleLyricist: {name: model.TagLyricist, sort: model.TagLyricistSort, mbid: model.TagMusicBrainzLyricistID}, - model.RoleConductor: {name: model.TagConductor, mbid: model.TagMusicBrainzConductorID}, - model.RoleArranger: {name: model.TagArranger, mbid: model.TagMusicBrainzArrangerID}, - model.RoleDirector: {name: model.TagDirector, mbid: model.TagMusicBrainzDirectorID}, - model.RoleProducer: {name: model.TagProducer, mbid: model.TagMusicBrainzProducerID}, - model.RoleEngineer: {name: model.TagEngineer, mbid: model.TagMusicBrainzEngineerID}, - model.RoleMixer: {name: model.TagMixer, mbid: model.TagMusicBrainzMixerID}, - model.RoleRemixer: {name: model.TagRemixer, mbid: model.TagMusicBrainzRemixerID}, - model.RoleDJMixer: {name: model.TagDJMixer, mbid: model.TagMusicBrainzDJMixerID}, + model.RoleComposer: {name: model.TagComposer, sort: model.TagComposerSort, mbid: model.TagMusicBrainzComposerID, credit: model.TagComposerCredit}, + model.RoleLyricist: {name: model.TagLyricist, sort: model.TagLyricistSort, mbid: model.TagMusicBrainzLyricistID, credit: model.TagLyricistCredit}, + model.RoleConductor: {name: model.TagConductor, mbid: model.TagMusicBrainzConductorID, credit: model.TagConductorCredit}, + model.RoleArranger: {name: model.TagArranger, mbid: model.TagMusicBrainzArrangerID, credit: model.TagArrangerCredit}, + model.RoleDirector: {name: model.TagDirector, mbid: model.TagMusicBrainzDirectorID, credit: model.TagDirectorCredit}, + model.RoleProducer: {name: model.TagProducer, mbid: model.TagMusicBrainzProducerID, credit: model.TagProducerCredit}, + model.RoleEngineer: {name: model.TagEngineer, mbid: model.TagMusicBrainzEngineerID, credit: model.TagEngineerCredit}, + model.RoleMixer: {name: model.TagMixer, mbid: model.TagMusicBrainzMixerID, credit: model.TagMixerCredit}, + model.RoleRemixer: {name: model.TagRemixer, mbid: model.TagMusicBrainzRemixerID, credit: model.TagRemixerCredit}, + model.RoleDJMixer: {name: model.TagDJMixer, mbid: model.TagMusicBrainzDJMixerID, credit: model.TagDJMixerCredit}, } func (md Metadata) mapParticipants() model.Participants { participants := make(model.Participants) // Parse track artists - artists := md.parseArtists( - model.TagTrackArtist, model.TagTrackArtists, - model.TagTrackArtistSort, model.TagTrackArtistsSort, - model.TagMusicBrainzArtistID, - ) - participants.Add(model.RoleArtist, artists...) + 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) + trackCredits := md.getArtistValues(model.TagTrackArtistCredit, model.TagTrackArtistsCredit) + trackArtistParticipants := md.buildParticipants(trackNames, trackSorts, trackMbids, trackCredits) + participants.AddParticipants(model.RoleArtist, trackArtistParticipants...) // Parse album artists - albumArtists := md.parseArtists( - model.TagAlbumArtist, model.TagAlbumArtists, - model.TagAlbumArtistSort, model.TagAlbumArtistsSort, - model.TagMusicBrainzAlbumArtistID, - ) - if len(albumArtists) == 1 && albumArtists[0].Name == consts.UnknownArtist { + albumNames := md.getArtistValues(model.TagAlbumArtist, model.TagAlbumArtists) + albumSorts := md.getArtistValues(model.TagAlbumArtistSort, model.TagAlbumArtistsSort) + albumMbids := md.Strings(model.TagMusicBrainzAlbumArtistID) + albumCredits := md.getArtistValues(model.TagAlbumArtistCredit, model.TagAlbumArtistsCredit) + + var albumArtistParticipants []model.Participant + if len(albumNames) == 0 { if md.Bool(model.TagCompilation) { - albumArtists = md.buildArtists([]string{consts.VariousArtists}, nil, []string{consts.VariousArtistsMbzId}) + albumArtistParticipants = md.buildParticipants( + []string{consts.VariousArtists}, nil, + []string{consts.VariousArtistsMbzId}, nil) } else { - albumArtists = artists + albumArtistParticipants = trackArtistParticipants } + } else { + albumArtistParticipants = md.buildParticipants(albumNames, albumSorts, albumMbids, albumCredits) } - participants.Add(model.RoleAlbumArtist, albumArtists...) + participants.AddParticipants(model.RoleAlbumArtist, albumArtistParticipants...) // Parse all other roles for role, info := range roleMappings { @@ -64,8 +73,8 @@ func (md Metadata) mapParticipants() model.Participants { if len(names) > 0 { sorts := md.Strings(info.sort) mbids := md.Strings(info.mbid) - artists := md.buildArtists(names, sorts, mbids) - participants.Add(role, artists...) + credits := md.Strings(info.credit) + participants.AddParticipants(role, md.buildParticipants(names, sorts, mbids, credits)...) } } @@ -110,7 +119,11 @@ func (md Metadata) processPerformers(participants model.Participants, rolesMbzId conf.Server.PID.Artist, id.NewHash, ) - participants.AddWithSubRole(model.RolePerformer, subRole, artist) + participants.AddParticipants(model.RolePerformer, model.Participant{ + Artist: artist, + SubRole: subRole, + CreditedAs: name, + }) } } @@ -143,17 +156,25 @@ func (md Metadata) syncMissingMbzIDs(participants model.Participants) { } } -func (md Metadata) parseArtists( - name model.TagName, names model.TagName, sort model.TagName, - sorts model.TagName, mbid model.TagName, -) []model.Artist { - nameValues := md.getArtistValues(name, names) - sortValues := md.getArtistValues(sort, sorts) - mbids := md.Strings(mbid) - if len(nameValues) == 0 { - nameValues = []string{consts.UnknownArtist} +// buildParticipants builds Artists and wraps each into a Participant with +// CreditedAs populated. credits is paired positionally; if the lengths don't +// match, CreditedAs falls back to the canonical name for every entry. +func (md Metadata) buildParticipants(names, sorts, mbids, credits []string) []model.Participant { + if len(credits) != 0 && len(credits) != len(names) { + credits = nil } - return md.buildArtists(nameValues, sortValues, mbids) + artists := md.buildArtists(names, sorts, mbids) + out := make([]model.Participant, len(artists)) + for i, a := range artists { + p := model.Participant{Artist: a} + if i < len(credits) && credits[i] != "" { + p.CreditedAs = credits[i] + } else { + p.CreditedAs = a.Name + } + out[i] = p + } + return out } func (md Metadata) buildArtists(names, sorts, mbids []string) []model.Artist { diff --git a/model/metadata/map_participants_test.go b/model/metadata/map_participants_test.go index 5ee802ced..7ef6183ee 100644 --- a/model/metadata/map_participants_test.go +++ b/model/metadata/map_participants_test.go @@ -802,4 +802,65 @@ var _ = Describe("Participants", func() { } }) }) + + Describe("CreditedAs population", func() { + It("uses the canonical name as CreditedAs when no credit tag is present", func() { + mf = toMediaFile(model.RawTags{ + "ARTISTS": {"Some Artist"}, + }) + artists := mf.Participants[model.RoleArtist] + Expect(artists).To(HaveLen(1)) + Expect(artists[0].Name).To(Equal("Some Artist")) + Expect(artists[0].CreditedAs).To(Equal("Some Artist")) + }) + + It("uses the credit tag value when present, paired positionally", func() { + mf = toMediaFile(model.RawTags{ + "ARTISTS": {"Planetary Assault Systems", "Other"}, + "ARTISTSCREDIT": {"PAS", "Other"}, + }) + artists := mf.Participants[model.RoleArtist] + Expect(artists).To(HaveLen(2)) + Expect(artists[0].Name).To(Equal("Planetary Assault Systems")) + Expect(artists[0].CreditedAs).To(Equal("PAS")) + Expect(artists[1].Name).To(Equal("Other")) + Expect(artists[1].CreditedAs).To(Equal("Other")) + }) + + It("falls back to canonical name when credit list length differs from name list", func() { + mf = toMediaFile(model.RawTags{ + "ARTISTS": {"A", "B", "C"}, + "ARTISTSCREDIT": {"only one"}, // mismatch + }) + artists := mf.Participants[model.RoleArtist] + Expect(artists).To(HaveLen(3)) + for _, a := range artists { + Expect(a.CreditedAs).To(Equal(a.Name)) + } + }) + + It("populates CreditedAs for non-artist roles (composer)", func() { + mf = toMediaFile(model.RawTags{ + "COMPOSER": {"Real Composer"}, + "COMPOSERCREDIT": {"R. Composer"}, + }) + composers := mf.Participants[model.RoleComposer] + Expect(composers).To(HaveLen(1)) + Expect(composers[0].CreditedAs).To(Equal("R. Composer")) + }) + + It("always populates CreditedAs (never empty)", func() { + mf = toMediaFile(model.RawTags{ + "ARTIST": {"Track Artist"}, + "ALBUMARTIST": {"Album Artist"}, + "COMPOSER": {"A Composer"}, + "PERFORMER:GUITAR": {"A Guitarist"}, + }) + for role, list := range mf.Participants { + for _, p := range list { + Expect(p.CreditedAs).NotTo(BeEmpty(), "role: %s, participant: %+v", role, p) + } + } + }) + }) }) diff --git a/model/participants.go b/model/participants.go index ec1b8e863..5c476fe96 100644 --- a/model/participants.go +++ b/model/participants.go @@ -110,6 +110,12 @@ func (p Participants) AddWithSubRole(role Role, subRole string, artists ...Artis p.add(role, participants...) } +// AddParticipants adds Participants directly (preserving CreditedAs and SubRole), +// ignoring duplicates. +func (p Participants) AddParticipants(role Role, participants ...Participant) { + p.add(role, participants...) +} + func (p Participants) Sort() { for _, artists := range p { slices.SortFunc(artists, func(a1, a2 Participant) int {