mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-30 16:56:22 -04:00
fix(jellyfin): split Artists/ArtistItems per track artist (#5810)
* fix(jellyfin): split Artists/ArtistItems per track artist The Jellyfin API returned a single ArtistItems entry built from the flattened display artist, so a multi-artist track (e.g. "De La Soul feat. Redman") lost its individual artists and dropped every artist id but the first — while the same track's OpenSubsonic response correctly split them. Real Jellyfin splits Artists/ArtistItems one entry per track artist. Build both from Participants[RoleArtist], which is already loaded on the search and browse paths, falling back to the flattened display fields when absent. AlbumArtists stays a single credit, matching real Jellyfin. * fix(jellyfin): omit empty Artists in the fallback path When a track has no participants and no display artist, the fallback set Artists to a single-element slice holding an empty string. Only populate it when the display artist is non-empty, so untagged tracks omit the field instead of sending [""].
This commit is contained in:
@@ -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)}}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user