feat(subsonic): serve CreditedAs as name in contributor lists

This commit is contained in:
Deluan 2026-05-24 19:47:59 -03:00
parent 8b5e50a1aa
commit e2fd0959bd
3 changed files with 66 additions and 3 deletions

View File

@ -92,6 +92,21 @@ func (p ParticipantList) Join(sep string) string {
}), sep)
}
// JoinCredited joins the credited names of the participants with sep.
// Falls back to Name when CreditedAs is empty.
func (p ParticipantList) JoinCredited(sep string) string {
return strings.Join(slice.Map(p, func(part Participant) string {
n := part.CreditedAs
if n == "" {
n = part.Name
}
if part.SubRole != "" {
return n + " (" + part.SubRole + ")"
}
return n
}), sep)
}
type Participants map[Role]ParticipantList
// Add adds the artists to the role, ignoring duplicates.

View File

@ -271,7 +271,7 @@ func osChildFromMediaFile(ctx context.Context, mf model.MediaFile) *responses.Op
child.DisplayAlbumArtist = mf.AlbumArtist
child.AlbumArtists = artistRefs(mf.Participants[model.RoleAlbumArtist])
var contributors []responses.Contributor
child.DisplayComposer = mf.Participants[model.RoleComposer].Join(consts.ArtistJoiner)
child.DisplayComposer = mf.Participants[model.RoleComposer].JoinCredited(consts.ArtistJoiner)
for role, participants := range mf.Participants {
if role == model.RoleArtist || role == model.RoleAlbumArtist {
continue
@ -282,7 +282,7 @@ func osChildFromMediaFile(ctx context.Context, mf model.MediaFile) *responses.Op
SubRole: participant.SubRole,
Artist: responses.ArtistID3Ref{
Id: participant.ID,
Name: participant.Name,
Name: participantDisplayName(participant),
},
})
}
@ -296,11 +296,20 @@ func artistRefs(participants model.ParticipantList) []responses.ArtistID3Ref {
return slice.Map(participants, func(p model.Participant) responses.ArtistID3Ref {
return responses.ArtistID3Ref{
Id: p.ID,
Name: p.Name,
Name: participantDisplayName(p),
}
})
}
// participantDisplayName returns CreditedAs if set, otherwise the canonical Name.
// Legacy rows (pre-rescan) have empty CreditedAs and continue to show canonical.
func participantDisplayName(p model.Participant) string {
if p.CreditedAs != "" {
return p.CreditedAs
}
return p.Name
}
func fakePath(mf model.MediaFile) string {
builder := strings.Builder{}

View File

@ -317,6 +317,45 @@ var _ = Describe("helpers", func() {
Expect(child.Title).To(Equal(""))
})
})
It("returns CreditedAs as the contributor name when present, with canonical artist ID", func() {
mf := model.MediaFile{
ID: "song-1",
Participants: model.Participants{
model.RoleArtist: model.ParticipantList{
{Artist: model.Artist{ID: "canon-1", Name: "Planetary Assault Systems"}, CreditedAs: "PAS"},
},
model.RoleComposer: model.ParticipantList{
{Artist: model.Artist{ID: "canon-2", Name: "Real Composer"}, CreditedAs: "R. Composer"},
},
},
}
child := childFromMediaFile(context.Background(), mf)
Expect(child.OpenSubsonicChild).NotTo(BeNil())
Expect(child.OpenSubsonicChild.Artists).To(HaveLen(1))
Expect(child.OpenSubsonicChild.Artists[0].Id).To(Equal("canon-1"))
Expect(child.OpenSubsonicChild.Artists[0].Name).To(Equal("PAS"))
Expect(child.OpenSubsonicChild.Contributors).To(HaveLen(1))
Expect(child.OpenSubsonicChild.Contributors[0].Artist.Id).To(Equal("canon-2"))
Expect(child.OpenSubsonicChild.Contributors[0].Artist.Name).To(Equal("R. Composer"))
Expect(child.OpenSubsonicChild.DisplayComposer).To(Equal("R. Composer"))
})
It("falls back to canonical Name when CreditedAs is empty (legacy participant rows)", func() {
mf := model.MediaFile{
ID: "song-2",
Participants: model.Participants{
model.RoleArtist: model.ParticipantList{
{Artist: model.Artist{ID: "canon-3", Name: "Some Artist"}}, // no CreditedAs
},
},
}
child := childFromMediaFile(context.Background(), mf)
Expect(child.OpenSubsonicChild).NotTo(BeNil())
Expect(child.OpenSubsonicChild.Artists[0].Name).To(Equal("Some Artist"))
})
})
Describe("osChildFromMediaFile", func() {