mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-13 21:58:42 +01:00
Merge branch 'master' into os-lyrics
This commit is contained in:
commit
66ca4ccecf
69 changed files with 758 additions and 438 deletions
|
|
@ -143,7 +143,7 @@ func createAdminUser(ctx context.Context, ds model.DataStore, username, password
|
|||
Email: "",
|
||||
NewPassword: password,
|
||||
IsAdmin: true,
|
||||
LastLoginAt: now,
|
||||
LastLoginAt: &now,
|
||||
}
|
||||
err := ds.User(ctx).Put(&initialUser)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
|
|
@ -31,9 +32,11 @@ var _ = Describe("Auth", func() {
|
|||
})
|
||||
|
||||
Describe("createAdmin", func() {
|
||||
var createdAt time.Time
|
||||
BeforeEach(func() {
|
||||
req = httptest.NewRequest("POST", "/createAdmin", strings.NewReader(`{"username":"johndoe", "password":"secret"}`))
|
||||
resp = httptest.NewRecorder()
|
||||
createdAt = time.Now()
|
||||
createAdmin(ds)(resp, req)
|
||||
})
|
||||
|
||||
|
|
@ -43,6 +46,7 @@ var _ = Describe("Auth", func() {
|
|||
Expect(err).To(BeNil())
|
||||
Expect(u.Password).ToNot(BeEmpty())
|
||||
Expect(u.IsAdmin).To(BeTrue())
|
||||
Expect(*u.LastLoginAt).To(BeTemporally(">=", createdAt, time.Second))
|
||||
})
|
||||
|
||||
It("returns the expected payload", func() {
|
||||
|
|
|
|||
|
|
@ -352,12 +352,12 @@ func (api *Router) buildArtistDirectory(ctx context.Context, artist *model.Artis
|
|||
dir.Name = artist.Name
|
||||
dir.PlayCount = artist.PlayCount
|
||||
if artist.PlayCount > 0 {
|
||||
dir.Played = &artist.PlayDate
|
||||
dir.Played = artist.PlayDate
|
||||
}
|
||||
dir.AlbumCount = int32(artist.AlbumCount)
|
||||
dir.UserRating = int32(artist.Rating)
|
||||
if artist.Starred {
|
||||
dir.Starred = &artist.StarredAt
|
||||
dir.Starred = artist.StarredAt
|
||||
}
|
||||
|
||||
albums, err := api.ds.Album(ctx).GetAllWithoutGenres(filter.AlbumsByArtistID(artist.ID))
|
||||
|
|
@ -390,13 +390,13 @@ func (api *Router) buildAlbumDirectory(ctx context.Context, album *model.Album)
|
|||
dir.Parent = album.AlbumArtistID
|
||||
dir.PlayCount = album.PlayCount
|
||||
if album.PlayCount > 0 {
|
||||
dir.Played = &album.PlayDate
|
||||
dir.Played = album.PlayDate
|
||||
}
|
||||
dir.UserRating = int32(album.Rating)
|
||||
dir.SongCount = int32(album.SongCount)
|
||||
dir.CoverArt = album.CoverArtID().String()
|
||||
if album.Starred {
|
||||
dir.Starred = &album.StarredAt
|
||||
dir.Starred = album.StarredAt
|
||||
}
|
||||
|
||||
mfs, err := api.ds.MediaFile(ctx).GetAll(filter.SongsByAlbum(album.ID))
|
||||
|
|
@ -410,30 +410,7 @@ func (api *Router) buildAlbumDirectory(ctx context.Context, album *model.Album)
|
|||
|
||||
func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model.MediaFiles) *responses.AlbumWithSongsID3 {
|
||||
dir := &responses.AlbumWithSongsID3{}
|
||||
dir.Id = album.ID
|
||||
dir.Name = album.Name
|
||||
dir.Artist = album.AlbumArtist
|
||||
dir.ArtistId = album.AlbumArtistID
|
||||
dir.CoverArt = album.CoverArtID().String()
|
||||
dir.SongCount = int32(album.SongCount)
|
||||
dir.Duration = int32(album.Duration)
|
||||
dir.PlayCount = album.PlayCount
|
||||
if album.PlayCount > 0 {
|
||||
dir.Played = &album.PlayDate
|
||||
}
|
||||
dir.Year = int32(album.MaxYear)
|
||||
dir.Genre = album.Genre
|
||||
dir.Genres = itemGenresFromGenres(album.Genres)
|
||||
dir.UserRating = int32(album.Rating)
|
||||
if !album.CreatedAt.IsZero() {
|
||||
dir.Created = &album.CreatedAt
|
||||
}
|
||||
if album.Starred {
|
||||
dir.Starred = &album.StarredAt
|
||||
}
|
||||
dir.MusicBrainzId = album.MbzAlbumID
|
||||
dir.IsCompilation = album.Compilation
|
||||
dir.SortName = album.SortAlbumName
|
||||
dir.AlbumID3 = buildAlbumID3(ctx, *album)
|
||||
dir.Song = childrenFromMediaFiles(ctx, mfs)
|
||||
return dir
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
|
|
@ -97,7 +98,7 @@ func toArtist(r *http.Request, a model.Artist) responses.Artist {
|
|||
ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600),
|
||||
}
|
||||
if a.Starred {
|
||||
artist.Starred = &a.StarredAt
|
||||
artist.Starred = a.StarredAt
|
||||
}
|
||||
return artist
|
||||
}
|
||||
|
|
@ -114,7 +115,7 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 {
|
|||
SortName: a.SortArtistName,
|
||||
}
|
||||
if a.Starred {
|
||||
artist.Starred = &a.StarredAt
|
||||
artist.Starred = a.StarredAt
|
||||
}
|
||||
return artist
|
||||
}
|
||||
|
|
@ -153,7 +154,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
|
||||
|
|
@ -174,10 +175,10 @@ func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child
|
|||
child.Type = "music"
|
||||
child.PlayCount = mf.PlayCount
|
||||
if mf.PlayCount > 0 {
|
||||
child.Played = &mf.PlayDate
|
||||
child.Played = mf.PlayDate
|
||||
}
|
||||
if mf.Starred {
|
||||
child.Starred = &mf.StarredAt
|
||||
child.Starred = mf.StarredAt
|
||||
}
|
||||
child.UserRating = int32(mf.Rating)
|
||||
|
||||
|
|
@ -231,7 +232,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
|
||||
|
|
@ -239,11 +240,11 @@ func childFromAlbum(_ context.Context, al model.Album) responses.Child {
|
|||
child.Duration = int32(al.Duration)
|
||||
child.SongCount = int32(al.SongCount)
|
||||
if al.Starred {
|
||||
child.Starred = &al.StarredAt
|
||||
child.Starred = al.StarredAt
|
||||
}
|
||||
child.PlayCount = al.PlayCount
|
||||
if al.PlayCount > 0 {
|
||||
child.Played = &al.PlayDate
|
||||
child.Played = al.PlayDate
|
||||
}
|
||||
child.UserRating = int32(al.Rating)
|
||||
child.SortName = al.SortAlbumName
|
||||
|
|
@ -260,10 +261,62 @@ 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(_ context.Context, a model.Album) responses.DiscTitles {
|
||||
if len(a.Discs) == 0 {
|
||||
return nil
|
||||
}
|
||||
discTitles := responses.DiscTitles{}
|
||||
for num, title := range a.Discs {
|
||||
discTitles = append(discTitles, responses.DiscTitle{Disc: num, Title: title})
|
||||
}
|
||||
sort.Slice(discTitles, func(i, j int) bool {
|
||||
return discTitles[i].Disc < discTitles[j].Disc
|
||||
})
|
||||
return discTitles
|
||||
}
|
||||
|
||||
func buildAlbumsID3(ctx context.Context, albums model.Albums) []responses.AlbumID3 {
|
||||
res := make([]responses.AlbumID3, len(albums))
|
||||
for i, album := range albums {
|
||||
res[i] = buildAlbumID3(ctx, album)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 {
|
||||
dir := responses.AlbumID3{}
|
||||
dir.Id = album.ID
|
||||
dir.Name = album.Name
|
||||
dir.Artist = album.AlbumArtist
|
||||
dir.ArtistId = album.AlbumArtistID
|
||||
dir.CoverArt = album.CoverArtID().String()
|
||||
dir.SongCount = int32(album.SongCount)
|
||||
dir.Duration = int32(album.Duration)
|
||||
dir.PlayCount = album.PlayCount
|
||||
if album.PlayCount > 0 {
|
||||
dir.Played = album.PlayDate
|
||||
}
|
||||
dir.Year = int32(album.MaxYear)
|
||||
dir.Genre = album.Genre
|
||||
dir.Genres = buildItemGenres(album.Genres)
|
||||
dir.DiscTitles = buildDiscSubtitles(ctx, album)
|
||||
dir.UserRating = int32(album.Rating)
|
||||
if !album.CreatedAt.IsZero() {
|
||||
dir.Created = &album.CreatedAt
|
||||
}
|
||||
if album.Starred {
|
||||
dir.Starred = album.StarredAt
|
||||
}
|
||||
dir.MusicBrainzId = album.MbzAlbumID
|
||||
dir.IsCompilation = album.Compilation
|
||||
dir.SortName = album.SortAlbumName
|
||||
return dir
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,25 @@ 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[int]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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
<album id="1" name="album" artist="artist" genre="rock" userRating="0" musicBrainzId="1234" isCompilation="true" sortName="sorted album">
|
||||
<genres name="rock"></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">
|
||||
<genres name="rock"></genres>
|
||||
<genres name="progressive"></genres>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"genres": [],
|
||||
"musicBrainzId": "",
|
||||
"isCompilation": false,
|
||||
"sortName": ""
|
||||
"sortName": "",
|
||||
"discTitles": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,6 +225,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 {
|
||||
|
|
@ -276,7 +277,7 @@ type SearchResult2 struct {
|
|||
|
||||
type SearchResult3 struct {
|
||||
Artist []ArtistID3 `xml:"artist" json:"artist,omitempty"`
|
||||
Album []Child `xml:"album" json:"album,omitempty"`
|
||||
Album []AlbumID3 `xml:"album" json:"album,omitempty"`
|
||||
Song []Child `xml:"song" json:"song,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -479,12 +480,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 {
|
||||
|
|
@ -495,3 +491,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{{
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) {
|
|||
ArtistImageUrl: public.ImageURL(r, artist.CoverArtID(), 600),
|
||||
}
|
||||
if artist.Starred {
|
||||
searchResult2.Artist[i].Starred = &as[i].StarredAt
|
||||
searchResult2.Artist[i].Starred = as[i].StarredAt
|
||||
}
|
||||
}
|
||||
searchResult2.Album = childrenFromAlbums(ctx, als)
|
||||
|
|
@ -133,7 +133,7 @@ func (api *Router) Search3(r *http.Request) (*responses.Subsonic, error) {
|
|||
for i, artist := range as {
|
||||
searchResult3.Artist[i] = toArtistID3(r, artist)
|
||||
}
|
||||
searchResult3.Album = childrenFromAlbums(ctx, als)
|
||||
searchResult3.Album = buildAlbumsID3(ctx, als)
|
||||
searchResult3.Song = childrenFromMediaFiles(ctx, mfs)
|
||||
response.SearchResult3 = searchResult3
|
||||
return response, nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue