diff --git a/server/jellyfin/dto/mappers.go b/server/jellyfin/dto/mappers.go index 2894edd6e..dc9bc5fbd 100644 --- a/server/jellyfin/dto/mappers.go +++ b/server/jellyfin/dto/mappers.go @@ -6,6 +6,7 @@ import ( "time" "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/utils/slice" ) // Jellyfin wire times are ticks: 100ns units, i.e. 10,000 per millisecond. @@ -139,7 +140,6 @@ func SongToBaseItem(mf model.MediaFile, fields Fields) BaseItemDto { Album: mf.Album, AlbumId: EncodeID(mf.AlbumID), AlbumArtist: mf.AlbumArtist, - Artists: []string{mf.Artist}, RunTimeTicks: TicksFromSeconds(mf.Duration), DateCreated: jellyfinDate(&mf.CreatedAt), Container: mf.Suffix, @@ -153,11 +153,20 @@ func SongToBaseItem(mf model.MediaFile, fields Fields) BaseItemDto { if fields.Has("SortName") { item.SortName = cmp.Or(mf.SortTitle, mf.OrderTitle, mf.Title) } - // Finamp's Now Playing screen reads ArtistItems for the displayed artist (falling back to "Unknown - // Artist" if absent), even though Artists carries the same name. ArtistItems is the track artist; - // AlbumArtists the album artist. - if mf.ArtistID != "" { - item.ArtistItems = []NameGuidPair{{Name: mf.Artist, Id: EncodeID(mf.ArtistID)}} + // Real Jellyfin splits Artists/ArtistItems per track artist (AlbumArtists stays a single credit). + // Participants holds the per-artist list; fall back to the flattened display fields when absent. + if artists := mf.Participants[model.RoleArtist]; len(artists) > 0 { + item.Artists = slice.Map(artists, func(p model.Participant) string { return p.Name }) + item.ArtistItems = slice.Map(artists, func(p model.Participant) NameGuidPair { + return NameGuidPair{Name: p.Name, Id: EncodeID(p.ID)} + }) + } else { + if mf.Artist != "" { + item.Artists = []string{mf.Artist} + } + if mf.ArtistID != "" { + item.ArtistItems = []NameGuidPair{{Name: mf.Artist, Id: EncodeID(mf.ArtistID)}} + } } if mf.AlbumArtistID != "" { item.AlbumArtists = []NameGuidPair{{Name: mf.AlbumArtist, Id: EncodeID(mf.AlbumArtistID)}} diff --git a/server/jellyfin/dto/mappers_test.go b/server/jellyfin/dto/mappers_test.go index db0c56aa3..6bfc9d95a 100644 --- a/server/jellyfin/dto/mappers_test.go +++ b/server/jellyfin/dto/mappers_test.go @@ -99,6 +99,32 @@ var _ = Describe("mappers", func() { Expect(SongToBaseItem(model.MediaFile{ID: "s1", Title: "Song", Artist: "X"}, nil).ArtistItems).To(BeNil()) }) + It("omits Artists when the track has no artist name or participants", func() { + Expect(SongToBaseItem(model.MediaFile{ID: "s1", Title: "Song"}, nil).Artists).To(BeNil()) + }) + + It("splits Artists and ArtistItems per track artist from Participants", func() { + mf := model.MediaFile{ + ID: "s1", Title: "Oooh", + Artist: "De La Soul feat. Redman", ArtistID: "ar-delasoul", + AlbumArtist: "De La Soul", AlbumArtistID: "ar-delasoul", + } + mf.Participants = model.Participants{ + model.RoleArtist: model.ParticipantList{ + {Artist: model.Artist{ID: "ar-delasoul", Name: "De La Soul"}}, + {Artist: model.Artist{ID: "ar-redman", Name: "Redman"}}, + }, + } + item := SongToBaseItem(mf, nil) + Expect(item.Artists).To(Equal([]string{"De La Soul", "Redman"})) + Expect(item.ArtistItems).To(Equal([]NameGuidPair{ + {Name: "De La Soul", Id: EncodeID("ar-delasoul")}, + {Name: "Redman", Id: EncodeID("ar-redman")}, + })) + // AlbumArtists stays single, matching real Jellyfin. + Expect(item.AlbumArtists).To(Equal([]NameGuidPair{{Name: "De La Soul", Id: EncodeID("ar-delasoul")}})) + }) + It("builds a MediaSourceInfo from a media file", func() { mf := model.MediaFile{ID: "s1", Size: 5242880, Suffix: "mp3", BitRate: 320, Duration: 100} src := MediaSourceFromMediaFile(mf)