Add discTitles to OpenSubsonic responses
This commit is contained in:
parent
af7eead037
commit
2c9035fdd0
|
@ -423,7 +423,8 @@ func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model
|
||||||
}
|
}
|
||||||
dir.Year = int32(album.MaxYear)
|
dir.Year = int32(album.MaxYear)
|
||||||
dir.Genre = album.Genre
|
dir.Genre = album.Genre
|
||||||
dir.Genres = itemGenresFromGenres(album.Genres)
|
dir.Genres = buildItemGenres(album.Genres)
|
||||||
|
dir.DiscTitles = buildDiscSubtitles(ctx, *album)
|
||||||
dir.UserRating = int32(album.Rating)
|
dir.UserRating = int32(album.Rating)
|
||||||
if !album.CreatedAt.IsZero() {
|
if !album.CreatedAt.IsZero() {
|
||||||
dir.Created = &album.CreatedAt
|
dir.Created = &album.CreatedAt
|
||||||
|
|
|
@ -5,9 +5,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/model/request"
|
"github.com/navidrome/navidrome/model/request"
|
||||||
"github.com/navidrome/navidrome/server/public"
|
"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.Year = int32(mf.Year)
|
||||||
child.Artist = mf.Artist
|
child.Artist = mf.Artist
|
||||||
child.Genre = mf.Genre
|
child.Genre = mf.Genre
|
||||||
child.Genres = itemGenresFromGenres(mf.Genres)
|
child.Genres = buildItemGenres(mf.Genres)
|
||||||
child.Track = int32(mf.TrackNumber)
|
child.Track = int32(mf.TrackNumber)
|
||||||
child.Duration = int32(mf.Duration)
|
child.Duration = int32(mf.Duration)
|
||||||
child.Size = mf.Size
|
child.Size = mf.Size
|
||||||
|
@ -231,7 +234,7 @@ func childFromAlbum(_ context.Context, al model.Album) responses.Child {
|
||||||
child.Artist = al.AlbumArtist
|
child.Artist = al.AlbumArtist
|
||||||
child.Year = int32(al.MaxYear)
|
child.Year = int32(al.MaxYear)
|
||||||
child.Genre = al.Genre
|
child.Genre = al.Genre
|
||||||
child.Genres = itemGenresFromGenres(al.Genres)
|
child.Genres = buildItemGenres(al.Genres)
|
||||||
child.CoverArt = al.CoverArtID().String()
|
child.CoverArt = al.CoverArtID().String()
|
||||||
child.Created = &al.CreatedAt
|
child.Created = &al.CreatedAt
|
||||||
child.Parent = al.AlbumArtistID
|
child.Parent = al.AlbumArtistID
|
||||||
|
@ -260,10 +263,29 @@ func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
func itemGenresFromGenres(genres model.Genres) []responses.ItemGenre {
|
func buildItemGenres(genres model.Genres) []responses.ItemGenre {
|
||||||
itemGenres := make([]responses.ItemGenre, len(genres))
|
itemGenres := make([]responses.ItemGenre, len(genres))
|
||||||
for i, g := range genres {
|
for i, g := range genres {
|
||||||
itemGenres[i] = responses.ItemGenre{Name: g.Name}
|
itemGenres[i] = responses.ItemGenre{Name: g.Name}
|
||||||
}
|
}
|
||||||
return itemGenres
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package subsonic
|
package subsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
@ -33,4 +36,38 @@ var _ = Describe("helpers", func() {
|
||||||
Expect(mapSlashToDash("AC/DC")).To(Equal("AC_DC"))
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,6 +21,16 @@
|
||||||
"musicBrainzId": "1234",
|
"musicBrainzId": "1234",
|
||||||
"isCompilation": true,
|
"isCompilation": true,
|
||||||
"sortName": "sorted album",
|
"sortName": "sorted album",
|
||||||
|
"discTitles": [
|
||||||
|
{
|
||||||
|
"disc": 1,
|
||||||
|
"title": "disc 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"disc": 2,
|
||||||
|
"title": "disc 2"
|
||||||
|
}
|
||||||
|
],
|
||||||
"song": [
|
"song": [
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
<album id="1" name="album" artist="artist" genre="rock" userRating="0" musicBrainzId="1234" isCompilation="true" sortName="sorted album">
|
<album id="1" name="album" artist="artist" genre="rock" userRating="0" musicBrainzId="1234" isCompilation="true" sortName="sorted album">
|
||||||
<genres name="rock"></genres>
|
<genres name="rock"></genres>
|
||||||
<genres name="progressive"></genres>
|
<genres name="progressive"></genres>
|
||||||
|
<discTitles disc="1" title="disc 1"></discTitles>
|
||||||
|
<discTitles disc="2" title="disc 2"></discTitles>
|
||||||
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321">
|
<song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321">
|
||||||
<genres name="rock"></genres>
|
<genres name="rock"></genres>
|
||||||
<genres name="progressive"></genres>
|
<genres name="progressive"></genres>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"genres": [],
|
"genres": [],
|
||||||
"musicBrainzId": "",
|
"musicBrainzId": "",
|
||||||
"isCompilation": false,
|
"isCompilation": false,
|
||||||
"sortName": ""
|
"sortName": "",
|
||||||
|
"discTitles": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,6 +224,7 @@ type AlbumID3 struct {
|
||||||
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
|
MusicBrainzId string `xml:"musicBrainzId,attr" json:"musicBrainzId"`
|
||||||
IsCompilation bool `xml:"isCompilation,attr" json:"isCompilation"`
|
IsCompilation bool `xml:"isCompilation,attr" json:"isCompilation"`
|
||||||
SortName string `xml:"sortName,attr" json:"sortName"`
|
SortName string `xml:"sortName,attr" json:"sortName"`
|
||||||
|
DiscTitles DiscTitles `xml:"discTitles" json:"discTitles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtistWithAlbumsID3 struct {
|
type ArtistWithAlbumsID3 struct {
|
||||||
|
@ -459,12 +460,7 @@ type ItemGenre struct {
|
||||||
type ItemGenres []ItemGenre
|
type ItemGenres []ItemGenre
|
||||||
|
|
||||||
func (i ItemGenres) MarshalJSON() ([]byte, error) {
|
func (i ItemGenres) MarshalJSON() ([]byte, error) {
|
||||||
if len(i) == 0 {
|
return marshalJSONArray(i)
|
||||||
return json.Marshal([]ItemGenre{})
|
|
||||||
}
|
|
||||||
type Alias []ItemGenre
|
|
||||||
a := (Alias)(i)
|
|
||||||
return json.Marshal(a)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplayGain struct {
|
type ReplayGain struct {
|
||||||
|
@ -475,3 +471,24 @@ type ReplayGain struct {
|
||||||
BaseGain float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"`
|
BaseGain float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"`
|
||||||
FallbackGain float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,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)
|
||||||
|
}
|
||||||
|
|
|
@ -176,6 +176,7 @@ var _ = Describe("Responses", func() {
|
||||||
Id: "1", Name: "album", Artist: "artist", Genre: "rock",
|
Id: "1", Name: "album", Artist: "artist", Genre: "rock",
|
||||||
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
|
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
|
||||||
MusicBrainzId: "1234", IsCompilation: true, SortName: "sorted album",
|
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)
|
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
|
||||||
songs := []Child{{
|
songs := []Child{{
|
||||||
|
|
Loading…
Reference in New Issue