fix(participants): preserve later non-empty CreditedAs on dedup merge

Participants.add deduplicates by ID+SubRole. The same artist appearing
on multiple tracks of one album would silently keep only the
first-seen CreditedAs, so the album-level participants JSON could
end up with a credit from one track applied to all tracks of that
album.

Change the dedup to update the existing entry's CreditedAs when a
later occurrence has a non-empty value. Per-track media_file
participants are unaffected (each track's JSON is built
independently). The album-level merge result is inherently lossy
when tracks differ, but no longer silently drops a meaningful credit
based on arrival order.
This commit is contained in:
Deluan 2026-05-24 21:29:47 -03:00
parent 1cf5a9f215
commit c898f0e2a9
2 changed files with 49 additions and 6 deletions

View File

@ -156,16 +156,25 @@ func (p Participants) Merge(other Participants) {
}
func (p Participants) add(role Role, participants ...Participant) {
seen := make(map[string]struct{}, len(p[role]))
for _, artist := range p[role] {
seen[artist.ID+artist.SubRole] = struct{}{}
seen := make(map[string]int, len(p[role]))
for i, artist := range p[role] {
seen[artist.ID+artist.SubRole] = i
}
for _, participant := range participants {
key := participant.ID + participant.SubRole
if _, ok := seen[key]; !ok {
seen[key] = struct{}{}
p[role] = append(p[role], participant)
if idx, ok := seen[key]; ok {
// Same artist/sub-role seen before. The merge (e.g. building
// album.Participants from per-track participants) is inherently
// lossy when tracks differ on CreditedAs, but silently dropping
// the later value can mean the wrong credit ends up on the album.
// Prefer the most recently observed non-empty CreditedAs.
if participant.CreditedAs != "" {
p[role][idx].CreditedAs = participant.CreditedAs
}
continue
}
seen[key] = len(p[role])
p[role] = append(p[role], participant)
}
}

View File

@ -116,6 +116,40 @@ var _ = Describe("Participants", func() {
RoleAlbumArtist: []Participant{_p("3", "AlbumArtist1"), _p("4", "AlbumArtist2"), _p("7", "AlbumArtist3"), _p("8", "AlbumArtist4")},
}))
})
It("upgrades CreditedAs from a later occurrence when the dedup key matches", func() {
// Same artist (same ID+SubRole) credited differently on two tracks.
// The merge is inherently lossy; the later non-empty credit wins so
// it isn't silently dropped just because of arrival order.
p1 := Participants{
RoleArtist: []Participant{
{Artist: Artist{ID: "a1", Name: "Aphex Twin"}, CreditedAs: "AFX"},
},
}
p2 := Participants{
RoleArtist: []Participant{
{Artist: Artist{ID: "a1", Name: "Aphex Twin"}, CreditedAs: "Aphex Twin"},
},
}
p1.Merge(p2)
Expect(p1[RoleArtist]).To(HaveLen(1))
Expect(p1[RoleArtist][0].CreditedAs).To(Equal("Aphex Twin"))
})
It("does not overwrite an existing CreditedAs with an empty one", func() {
p1 := Participants{
RoleArtist: []Participant{
{Artist: Artist{ID: "a1", Name: "Aphex Twin"}, CreditedAs: "AFX"},
},
}
p2 := Participants{
RoleArtist: []Participant{
{Artist: Artist{ID: "a1", Name: "Aphex Twin"}}, // empty CreditedAs
},
}
p1.Merge(p2)
Expect(p1[RoleArtist][0].CreditedAs).To(Equal("AFX"))
})
})
Describe("Hash", func() {