Removed more layers of indirection from the engine package

This commit is contained in:
Deluan 2020-10-27 10:03:10 -04:00
parent acba4b16ee
commit 3037ea01e2
7 changed files with 182 additions and 307 deletions

View File

@ -1,66 +1,71 @@
package subsonic
import (
"context"
"errors"
"net/http"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/server/subsonic/engine"
"github.com/deluan/navidrome/server/subsonic/filter"
"github.com/deluan/navidrome/server/subsonic/responses"
"github.com/deluan/navidrome/utils"
)
type AlbumListController struct {
ds model.DataStore
listGen engine.ListGenerator
}
func NewAlbumListController(listGen engine.ListGenerator) *AlbumListController {
func NewAlbumListController(ds model.DataStore, listGen engine.ListGenerator) *AlbumListController {
c := &AlbumListController{
ds: ds,
listGen: listGen,
}
return c
}
func (c *AlbumListController) getAlbumList(r *http.Request) (engine.Entries, error) {
func (c *AlbumListController) getAlbumList(r *http.Request) (model.Albums, error) {
typ, err := requiredParamString(r, "type", "Required string parameter 'type' is not present")
if err != nil {
return nil, err
}
var filter engine.ListFilter
var opts filter.Options
switch typ {
case "newest":
filter = engine.ByNewest()
opts = filter.AlbumsByNewest()
case "recent":
filter = engine.ByRecent()
opts = filter.AlbumsByRecent()
case "random":
filter = engine.ByRandom()
opts = filter.AlbumsByRandom()
case "alphabeticalByName":
filter = engine.ByName()
opts = filter.AlbumsByName()
case "alphabeticalByArtist":
filter = engine.ByArtist()
opts = filter.AlbumsByArtist()
case "frequent":
filter = engine.ByFrequent()
opts = filter.AlbumsByFrequent()
case "starred":
filter = engine.ByStarred()
opts = filter.AlbumsByStarred()
case "highest":
filter = engine.ByRating()
opts = filter.AlbumsByRating()
case "byGenre":
filter = engine.ByGenre(utils.ParamString(r, "genre"))
opts = filter.AlbumsByGenre(utils.ParamString(r, "genre"))
case "byYear":
filter = engine.ByYear(utils.ParamInt(r, "fromYear", 0), utils.ParamInt(r, "toYear", 0))
opts = filter.AlbumsByYear(utils.ParamInt(r, "fromYear", 0), utils.ParamInt(r, "toYear", 0))
default:
log.Error(r, "albumList type not implemented", "type", typ)
return nil, errors.New("Not implemented!")
return nil, errors.New("not implemented")
}
offset := utils.ParamInt(r, "offset", 0)
size := utils.MinInt(utils.ParamInt(r, "size", 10), 500)
opts.Offset = utils.ParamInt(r, "offset", 0)
opts.Max = utils.MinInt(utils.ParamInt(r, "size", 10), 500)
albums, err := c.ds.Album(r.Context()).GetAll(model.QueryOptions(opts))
albums, err := c.listGen.GetAlbums(r.Context(), offset, size, filter)
if err != nil {
log.Error(r, "Error retrieving albums", "error", err)
return nil, errors.New("Internal Error")
return nil, errors.New("internal Error")
}
return albums, nil
@ -73,7 +78,7 @@ func (c *AlbumListController) GetAlbumList(w http.ResponseWriter, r *http.Reques
}
response := newResponse()
response.AlbumList = &responses.AlbumList{Album: toChildren(r.Context(), albums)}
response.AlbumList = &responses.AlbumList{Album: childrenFromAlbums(r.Context(), albums)}
return response, nil
}
@ -84,37 +89,45 @@ func (c *AlbumListController) GetAlbumList2(w http.ResponseWriter, r *http.Reque
}
response := newResponse()
response.AlbumList2 = &responses.AlbumList{Album: toAlbums(r.Context(), albums)}
response.AlbumList2 = &responses.AlbumList{Album: childrenFromAlbums(r.Context(), albums)}
return response, nil
}
func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
artists, albums, mediaFiles, err := c.listGen.GetAllStarred(r.Context())
ctx := r.Context()
options := model.QueryOptions{Sort: "starred_at", Order: "desc"}
artists, err := c.ds.Artist(ctx).GetStarred(options)
if err != nil {
log.Error(r, "Error retrieving starred media", "error", err)
log.Error(r, "Error retrieving starred artists", "error", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
}
albums, err := c.ds.Album(ctx).GetStarred(options)
if err != nil {
log.Error(r, "Error retrieving starred albums", "error", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
}
mediaFiles, err := c.ds.MediaFile(ctx).GetStarred(options)
if err != nil {
log.Error(r, "Error retrieving starred mediaFiles", "error", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
}
response := newResponse()
response.Starred = &responses.Starred{}
response.Starred.Artist = toArtists(artists)
response.Starred.Album = toChildren(r.Context(), albums)
response.Starred.Song = toChildren(r.Context(), mediaFiles)
response.Starred.Artist = toArtists(ctx, artists)
response.Starred.Album = childrenFromAlbums(r.Context(), albums)
response.Starred.Song = childrenFromMediaFiles(r.Context(), mediaFiles)
return response, nil
}
func (c *AlbumListController) GetStarred2(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
artists, albums, mediaFiles, err := c.listGen.GetAllStarred(r.Context())
resp, err := c.GetStarred(w, r)
if err != nil {
log.Error(r, "Error retrieving starred media", "error", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
return nil, err
}
response := newResponse()
response.Starred2 = &responses.Starred{}
response.Starred2.Artist = toArtists(artists)
response.Starred2.Album = toAlbums(r.Context(), albums)
response.Starred2.Song = toChildren(r.Context(), mediaFiles)
response.Starred2 = resp.Starred
return response, nil
}
@ -144,7 +157,7 @@ func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Requ
fromYear := utils.ParamInt(r, "fromYear", 0)
toYear := utils.ParamInt(r, "toYear", 0)
songs, err := c.listGen.GetSongs(r.Context(), 0, size, engine.SongsByRandom(genre, fromYear, toYear))
songs, err := c.getSongs(r.Context(), 0, size, filter.SongsByRandom(genre, fromYear, toYear))
if err != nil {
log.Error(r, "Error retrieving random songs", "error", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
@ -152,7 +165,7 @@ func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Requ
response := newResponse()
response.RandomSongs = &responses.Songs{}
response.RandomSongs.Songs = toChildren(r.Context(), songs)
response.RandomSongs.Songs = childrenFromMediaFiles(r.Context(), songs)
return response, nil
}
@ -161,7 +174,7 @@ func (c *AlbumListController) GetSongsByGenre(w http.ResponseWriter, r *http.Req
offset := utils.MinInt(utils.ParamInt(r, "offset", 0), 500)
genre := utils.ParamString(r, "genre")
songs, err := c.listGen.GetSongs(r.Context(), offset, count, engine.SongsByGenre(genre))
songs, err := c.getSongs(r.Context(), offset, count, filter.SongsByGenre(genre))
if err != nil {
log.Error(r, "Error retrieving random songs", "error", err)
return nil, newError(responses.ErrorGeneric, "Internal Error")
@ -169,6 +182,12 @@ func (c *AlbumListController) GetSongsByGenre(w http.ResponseWriter, r *http.Req
response := newResponse()
response.SongsByGenre = &responses.Songs{}
response.SongsByGenre.Songs = toChildren(r.Context(), songs)
response.SongsByGenre.Songs = childrenFromMediaFiles(r.Context(), songs)
return response, nil
}
func (c *AlbumListController) getSongs(ctx context.Context, offset, size int, opts filter.Options) (model.MediaFiles, error) {
opts.Offset = offset
opts.Max = size
return c.ds.MediaFile(ctx).GetAll(model.QueryOptions(opts))
}

View File

@ -2,55 +2,40 @@ package subsonic
import (
"context"
"errors"
"net/http/httptest"
"github.com/deluan/navidrome/server/subsonic/engine"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/persistence"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
type fakeListGen struct {
engine.ListGenerator
data engine.Entries
err error
recvOffset int
recvSize int
}
func (lg *fakeListGen) GetAlbums(ctx context.Context, offset int, size int, filter engine.ListFilter) (engine.Entries, error) {
if lg.err != nil {
return nil, lg.err
}
lg.recvOffset = offset
lg.recvSize = size
return lg.data, nil
}
var _ = Describe("AlbumListController", func() {
var controller *AlbumListController
var listGen *fakeListGen
var ds model.DataStore
var mockRepo *persistence.MockAlbum
var w *httptest.ResponseRecorder
ctx := log.NewContext(context.TODO())
BeforeEach(func() {
listGen = &fakeListGen{}
controller = NewAlbumListController(listGen)
ds = &persistence.MockDataStore{}
mockRepo = ds.Album(ctx).(*persistence.MockAlbum)
controller = NewAlbumListController(ds, nil)
w = httptest.NewRecorder()
})
Describe("GetAlbumList", func() {
It("should return list of the type specified", func() {
r := newGetRequest("type=newest", "offset=10", "size=20")
listGen.data = engine.Entries{
{Id: "1"}, {Id: "2"},
}
mockRepo.SetData(`[{"id": "1"},{"id": "2"}]`)
resp, err := controller.GetAlbumList(w, r)
Expect(err).To(BeNil())
Expect(resp.AlbumList.Album[0].Id).To(Equal("1"))
Expect(resp.AlbumList.Album[1].Id).To(Equal("2"))
Expect(listGen.recvOffset).To(Equal(10))
Expect(listGen.recvSize).To(Equal(20))
Expect(mockRepo.Options.Offset).To(Equal(10))
Expect(mockRepo.Options.Max).To(Equal(20))
})
It("should fail if missing type parameter", func() {
@ -61,28 +46,26 @@ var _ = Describe("AlbumListController", func() {
})
It("should return error if call fails", func() {
listGen.err = errors.New("some issue")
mockRepo.SetError(true)
r := newGetRequest("type=newest")
_, err := controller.GetAlbumList(w, r)
Expect(err).To(MatchError("Internal Error"))
Expect(err).ToNot(BeNil())
})
})
Describe("GetAlbumList2", func() {
It("should return list of the type specified", func() {
r := newGetRequest("type=newest", "offset=10", "size=20")
listGen.data = engine.Entries{
{Id: "1"}, {Id: "2"},
}
mockRepo.SetData(`[{"id": "1"},{"id": "2"}]`)
resp, err := controller.GetAlbumList2(w, r)
Expect(err).To(BeNil())
Expect(resp.AlbumList2.Album[0].Id).To(Equal("1"))
Expect(resp.AlbumList2.Album[1].Id).To(Equal("2"))
Expect(listGen.recvOffset).To(Equal(10))
Expect(listGen.recvSize).To(Equal(20))
Expect(mockRepo.Options.Offset).To(Equal(10))
Expect(mockRepo.Options.Max).To(Equal(20))
})
It("should fail if missing type parameter", func() {
@ -93,12 +76,12 @@ var _ = Describe("AlbumListController", func() {
})
It("should return error if call fails", func() {
listGen.err = errors.New("some issue")
mockRepo.SetError(true)
r := newGetRequest("type=newest")
_, err := controller.GetAlbumList2(w, r)
Expect(err).To(MatchError("Internal Error"))
Expect(err).ToNot(BeNil())
})
})
})

View File

@ -45,43 +45,6 @@ type Entry struct {
type Entries []Entry
func FromArtist(ar *model.Artist) Entry {
e := Entry{}
e.Id = ar.ID
e.Title = ar.Name
e.AlbumCount = ar.AlbumCount
e.IsDir = true
e.UserRating = ar.Rating
if ar.Starred {
e.Starred = ar.StarredAt
}
return e
}
func FromAlbum(al *model.Album) Entry {
e := Entry{}
e.Id = al.ID
e.Title = al.Name
e.IsDir = true
e.Parent = al.AlbumArtistID
e.Album = al.Name
e.Year = al.MaxYear
e.Artist = al.AlbumArtist
e.Genre = al.Genre
e.CoverArt = al.CoverArtId
e.Created = al.CreatedAt
e.AlbumId = al.ID
e.ArtistId = al.AlbumArtistID
e.Duration = int(al.Duration)
e.SongCount = al.SongCount
if al.Starred {
e.Starred = al.StarredAt
}
e.PlayCount = int32(al.PlayCount)
e.UserRating = al.Rating
return e
}
func FromMediaFile(mf *model.MediaFile) Entry {
e := Entry{}
e.Id = mf.ID
@ -133,15 +96,6 @@ func realArtistName(mf *model.MediaFile) string {
return mf.Artist
}
func FromAlbums(albums model.Albums) Entries {
entries := make(Entries, len(albums))
for i := range albums {
al := albums[i]
entries[i] = FromAlbum(&al)
}
return entries
}
func FromMediaFiles(mfs model.MediaFiles) Entries {
entries := make(Entries, len(mfs))
for i := range mfs {
@ -150,12 +104,3 @@ func FromMediaFiles(mfs model.MediaFiles) Entries {
}
return entries
}
func FromArtists(ars model.Artists) Entries {
entries := make(Entries, len(ars))
for i := range ars {
ar := ars[i]
entries[i] = FromArtist(&ar)
}
return entries
}

View File

@ -4,172 +4,22 @@ import (
"context"
"time"
"github.com/Masterminds/squirrel"
"github.com/deluan/navidrome/model"
)
type ListGenerator interface {
GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error)
GetNowPlaying(ctx context.Context) (Entries, error)
GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error)
GetAlbums(ctx context.Context, offset, size int, filter ListFilter) (Entries, error)
}
func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator {
return &listGenerator{ds, npRepo}
}
type ListFilter model.QueryOptions
func ByNewest() ListFilter {
return ListFilter{Sort: "createdAt", Order: "desc"}
}
func ByRecent() ListFilter {
return ListFilter{Sort: "playDate", Order: "desc", Filters: squirrel.Gt{"play_date": time.Time{}}}
}
func ByFrequent() ListFilter {
return ListFilter{Sort: "playCount", Order: "desc", Filters: squirrel.Gt{"play_count": 0}}
}
func ByRandom() ListFilter {
return ListFilter{Sort: "random()"}
}
func ByName() ListFilter {
return ListFilter{Sort: "name"}
}
func ByArtist() ListFilter {
return ListFilter{Sort: "artist"}
}
func ByStarred() ListFilter {
return ListFilter{Sort: "starred_at", Order: "desc", Filters: squirrel.Eq{"starred": true}}
}
func ByRating() ListFilter {
return ListFilter{Sort: "Rating", Order: "desc", Filters: squirrel.Gt{"rating": 0}}
}
func ByGenre(genre string) ListFilter {
return ListFilter{
Sort: "genre asc, name asc",
Filters: squirrel.Eq{"genre": genre},
}
}
func ByYear(fromYear, toYear int) ListFilter {
sortOption := "max_year, name"
if fromYear > toYear {
fromYear, toYear = toYear, fromYear
sortOption = "max_year desc, name"
}
return ListFilter{
Sort: sortOption,
Filters: squirrel.Or{
squirrel.And{
squirrel.GtOrEq{"min_year": fromYear},
squirrel.LtOrEq{"min_year": toYear},
},
squirrel.And{
squirrel.GtOrEq{"max_year": fromYear},
squirrel.LtOrEq{"max_year": toYear},
},
},
}
}
func SongsByGenre(genre string) ListFilter {
return ListFilter{
Sort: "genre asc, title asc",
Filters: squirrel.Eq{"genre": genre},
}
}
func SongsByRandom(genre string, fromYear, toYear int) ListFilter {
options := ListFilter{
Sort: "random()",
}
ff := squirrel.And{}
if genre != "" {
ff = append(ff, squirrel.Eq{"genre": genre})
}
if fromYear != 0 {
ff = append(ff, squirrel.GtOrEq{"year": fromYear})
}
if toYear != 0 {
ff = append(ff, squirrel.LtOrEq{"year": toYear})
}
options.Filters = ff
return options
}
type listGenerator struct {
ds model.DataStore
npRepo NowPlayingRepository
}
func (g *listGenerator) GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
qo := model.QueryOptions(filter)
qo.Offset = offset
qo.Max = size
mediaFiles, err := g.ds.MediaFile(ctx).GetAll(qo)
if err != nil {
return nil, err
}
return FromMediaFiles(mediaFiles), nil
}
func (g *listGenerator) GetAlbums(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
qo := model.QueryOptions(filter)
qo.Offset = offset
qo.Max = size
albums, err := g.ds.Album(ctx).GetAll(qo)
if err != nil {
return nil, err
}
return FromAlbums(albums), nil
}
func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (Entries, error) {
qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"}
albums, err := g.ds.Album(ctx).GetStarred(qo)
if err != nil {
return nil, err
}
return FromAlbums(albums), nil
}
func (g *listGenerator) GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error) {
options := model.QueryOptions{Sort: "starred_at", Order: "desc"}
ars, err := g.ds.Artist(ctx).GetStarred(options)
if err != nil {
return nil, nil, nil, err
}
als, err := g.ds.Album(ctx).GetStarred(options)
if err != nil {
return nil, nil, nil, err
}
mfs, err := g.ds.MediaFile(ctx).GetStarred(options)
if err != nil {
return nil, nil, nil, err
}
artists = FromArtists(ars)
albums = FromAlbums(als)
mediaFiles = FromMediaFiles(mfs)
return
}
func (g *listGenerator) GetNowPlaying(ctx context.Context) (Entries, error) {
npInfo, err := g.npRepo.GetAll()
if err != nil {

View File

@ -0,0 +1,95 @@
package filter
import (
"time"
"github.com/Masterminds/squirrel"
"github.com/deluan/navidrome/model"
)
type Options model.QueryOptions
func AlbumsByNewest() Options {
return Options{Sort: "createdAt", Order: "desc"}
}
func AlbumsByRecent() Options {
return Options{Sort: "playDate", Order: "desc", Filters: squirrel.Gt{"play_date": time.Time{}}}
}
func AlbumsByFrequent() Options {
return Options{Sort: "playCount", Order: "desc", Filters: squirrel.Gt{"play_count": 0}}
}
func AlbumsByRandom() Options {
return Options{Sort: "random()"}
}
func AlbumsByName() Options {
return Options{Sort: "name"}
}
func AlbumsByArtist() Options {
return Options{Sort: "artist"}
}
func AlbumsByStarred() Options {
return Options{Sort: "starred_at", Order: "desc", Filters: squirrel.Eq{"starred": true}}
}
func AlbumsByRating() Options {
return Options{Sort: "Rating", Order: "desc", Filters: squirrel.Gt{"rating": 0}}
}
func AlbumsByGenre(genre string) Options {
return Options{
Sort: "genre asc, name asc",
Filters: squirrel.Eq{"genre": genre},
}
}
func AlbumsByYear(fromYear, toYear int) Options {
sortOption := "max_year, name"
if fromYear > toYear {
fromYear, toYear = toYear, fromYear
sortOption = "max_year desc, name"
}
return Options{
Sort: sortOption,
Filters: squirrel.Or{
squirrel.And{
squirrel.GtOrEq{"min_year": fromYear},
squirrel.LtOrEq{"min_year": toYear},
},
squirrel.And{
squirrel.GtOrEq{"max_year": fromYear},
squirrel.LtOrEq{"max_year": toYear},
},
},
}
}
func SongsByGenre(genre string) Options {
return Options{
Sort: "genre asc, title asc",
Filters: squirrel.Eq{"genre": genre},
}
}
func SongsByRandom(genre string, fromYear, toYear int) Options {
options := Options{
Sort: "random()",
}
ff := squirrel.And{}
if genre != "" {
ff = append(ff, squirrel.Eq{"genre": genre})
}
if fromYear != 0 {
ff = append(ff, squirrel.GtOrEq{"year": fromYear})
}
if toYear != 0 {
ff = append(ff, squirrel.LtOrEq{"year": toYear})
}
options.Filters = ff
return options
}

View File

@ -64,38 +64,20 @@ func (e SubsonicError) Error() string {
return msg
}
func toAlbums(ctx context.Context, entries engine.Entries) []responses.Child {
children := make([]responses.Child, len(entries))
for i, entry := range entries {
children[i] = toAlbum(ctx, entry)
}
return children
}
func toAlbum(ctx context.Context, entry engine.Entry) responses.Child {
album := toChild(ctx, entry)
album.Name = album.Title
album.Title = ""
album.Parent = ""
album.Album = ""
album.AlbumId = ""
return album
}
func toArtists(entries engine.Entries) []responses.Artist {
artists := make([]responses.Artist, len(entries))
for i, entry := range entries {
artists[i] = responses.Artist{
Id: entry.Id,
Name: entry.Title,
AlbumCount: entry.AlbumCount,
UserRating: entry.UserRating,
func toArtists(ctx context.Context, artists model.Artists) []responses.Artist {
as := make([]responses.Artist, len(artists))
for i, artist := range artists {
as[i] = responses.Artist{
Id: artist.ID,
Name: artist.Name,
AlbumCount: artist.AlbumCount,
UserRating: artist.Rating,
}
if !entry.Starred.IsZero() {
artists[i].Starred = &entry.Starred
if artist.Starred {
as[i].Starred = &artist.StarredAt
}
}
return artists
return as
}
func toChildren(ctx context.Context, entries engine.Entries) []responses.Child {

View File

@ -25,8 +25,9 @@ func initBrowsingController(router *Router) *BrowsingController {
}
func initAlbumListController(router *Router) *AlbumListController {
dataStore := router.DataStore
listGenerator := router.ListGenerator
albumListController := NewAlbumListController(listGenerator)
albumListController := NewAlbumListController(dataStore, listGenerator)
return albumListController
}