From 2c9035fdd02566a551b65c83cd288591faaf7b7a Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 8 Dec 2023 20:04:17 -0500 Subject: [PATCH] Add discTitles to OpenSubsonic responses --- server/subsonic/browsing.go | 3 +- server/subsonic/helpers.go | 28 ++++++++++++-- server/subsonic/helpers_test.go | 37 +++++++++++++++++++ ...mWithSongsID3 with data should match .JSON | 10 +++++ ...umWithSongsID3 with data should match .XML | 2 + ...thSongsID3 without data should match .JSON | 3 +- server/subsonic/responses/responses.go | 29 ++++++++++++--- server/subsonic/responses/responses_test.go | 1 + 8 files changed, 102 insertions(+), 11 deletions(-) diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index 8337315f76c..a85ff5cde1d 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -423,7 +423,8 @@ func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model } dir.Year = int32(album.MaxYear) dir.Genre = album.Genre - dir.Genres = itemGenresFromGenres(album.Genres) + dir.Genres = buildItemGenres(album.Genres) + dir.DiscTitles = buildDiscSubtitles(ctx, *album) dir.UserRating = int32(album.Rating) if !album.CreatedAt.IsZero() { dir.Created = &album.CreatedAt diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index ab06b3a08d2..19f29a2081f 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -5,9 +5,12 @@ import ( "fmt" "mime" "net/http" + "sort" + "strconv" "strings" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/public" @@ -153,7 +156,7 @@ func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child child.Year = int32(mf.Year) child.Artist = mf.Artist child.Genre = mf.Genre - child.Genres = itemGenresFromGenres(mf.Genres) + child.Genres = buildItemGenres(mf.Genres) child.Track = int32(mf.TrackNumber) child.Duration = int32(mf.Duration) child.Size = mf.Size @@ -231,7 +234,7 @@ func childFromAlbum(_ context.Context, al model.Album) responses.Child { child.Artist = al.AlbumArtist child.Year = int32(al.MaxYear) child.Genre = al.Genre - child.Genres = itemGenresFromGenres(al.Genres) + child.Genres = buildItemGenres(al.Genres) child.CoverArt = al.CoverArtID().String() child.Created = &al.CreatedAt child.Parent = al.AlbumArtistID @@ -260,10 +263,29 @@ func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child return children } -func itemGenresFromGenres(genres model.Genres) []responses.ItemGenre { +func buildItemGenres(genres model.Genres) []responses.ItemGenre { itemGenres := make([]responses.ItemGenre, len(genres)) for i, g := range genres { itemGenres[i] = responses.ItemGenre{Name: g.Name} } return itemGenres } + +func buildDiscSubtitles(ctx context.Context, a model.Album) responses.DiscTitles { + if len(a.Discs) == 0 { + return nil + } + discTitles := responses.DiscTitles{} + for num, title := range a.Discs { + n, err := strconv.Atoi(num) + if err != nil { + log.Warn(ctx, "Invalid disc number", "num", num, "title", title, "album", a.Name, "artist", a.AlbumArtist, err) + continue + } + discTitles = append(discTitles, responses.DiscTitle{Disc: n, Title: title}) + } + sort.Slice(discTitles, func(i, j int) bool { + return discTitles[i].Disc < discTitles[j].Disc + }) + return discTitles +} diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go index 91b2f100067..5cc6141b667 100644 --- a/server/subsonic/helpers_test.go +++ b/server/subsonic/helpers_test.go @@ -1,7 +1,10 @@ package subsonic import ( + "context" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/server/subsonic/responses" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -33,4 +36,38 @@ var _ = Describe("helpers", func() { Expect(mapSlashToDash("AC/DC")).To(Equal("AC_DC")) }) }) + + Describe("buildDiscTitles", func() { + It("should return nil when album has no discs", func() { + album := model.Album{} + Expect(buildDiscSubtitles(context.Background(), album)).To(BeNil()) + }) + + It("should return correct disc titles when album has discs with valid disc numbers", func() { + album := model.Album{ + Discs: map[string]string{ + "1": "Disc 1", + "2": "Disc 2", + }, + } + expected := responses.DiscTitles{ + {Disc: 1, Title: "Disc 1"}, + {Disc: 2, Title: "Disc 2"}, + } + Expect(buildDiscSubtitles(context.Background(), album)).To(Equal(expected)) + }) + + It("should skip discs with invalid disc numbers", func() { + album := model.Album{ + Discs: map[string]string{ + "1": "Disc 1", + "two": "Disc 2", + }, + } + expected := responses.DiscTitles{ + {Disc: 1, Title: "Disc 1"}, + } + Expect(buildDiscSubtitles(context.Background(), album)).To(Equal(expected)) + }) + }) }) diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON index da546b70b91..15ddbbcea50 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON @@ -21,6 +21,16 @@ "musicBrainzId": "1234", "isCompilation": true, "sortName": "sorted album", + "discTitles": [ + { + "disc": 1, + "title": "disc 1" + }, + { + "disc": 2, + "title": "disc 2" + } + ], "song": [ { "id": "1", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML index 535393582a9..cfe3da99db4 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML @@ -2,6 +2,8 @@ + + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON index ead02f6f5dd..9508e14ba89 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON @@ -11,6 +11,7 @@ "genres": [], "musicBrainzId": "", "isCompilation": false, - "sortName": "" + "sortName": "", + "discTitles": [] } } diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 04bac482a05..a2c7e676fd8 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -224,6 +224,7 @@ type AlbumID3 struct { MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"` IsCompilation bool `xml:"isCompilation,attr" json:"isCompilation"` SortName string `xml:"sortName,attr" json:"sortName"` + DiscTitles DiscTitles `xml:"discTitles" json:"discTitles"` } type ArtistWithAlbumsID3 struct { @@ -459,12 +460,7 @@ type ItemGenre struct { type ItemGenres []ItemGenre func (i ItemGenres) MarshalJSON() ([]byte, error) { - if len(i) == 0 { - return json.Marshal([]ItemGenre{}) - } - type Alias []ItemGenre - a := (Alias)(i) - return json.Marshal(a) + return marshalJSONArray(i) } type ReplayGain struct { @@ -475,3 +471,24 @@ type ReplayGain struct { BaseGain float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"` FallbackGain float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,omitempty"` } + +type DiscTitle struct { + Disc int `xml:"disc,attr,omitempty" json:"disc,omitempty"` + Title string `xml:"title,attr,omitempty" json:"title,omitempty"` +} + +type DiscTitles []DiscTitle + +func (d DiscTitles) MarshalJSON() ([]byte, error) { + return marshalJSONArray(d) +} + +// marshalJSONArray marshals a slice of any type to JSON. If the slice is empty, it is marshalled as an +// empty array instead of null. +func marshalJSONArray[T any](v []T) ([]byte, error) { + if len(v) == 0 { + return json.Marshal([]T{}) + } + a := v + return json.Marshal(a) +} diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index 23f1abaf09d..17ea9ec3d12 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -176,6 +176,7 @@ var _ = Describe("Responses", func() { Id: "1", Name: "album", Artist: "artist", Genre: "rock", Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}}, MusicBrainzId: "1234", IsCompilation: true, SortName: "sorted album", + DiscTitles: DiscTitles{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}}, } t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC) songs := []Child{{