Reimplemented GetAlbumList&type=random and GetRandomSongs (now with filter by genres)

This commit is contained in:
Deluan 2020-01-21 08:49:43 -05:00
parent 6cd758faa0
commit de0816da67
9 changed files with 80 additions and 54 deletions

View File

@ -32,10 +32,10 @@ CloudSonic and Subsonic:
| _ALBUM/SONGS LISTS_ || | _ALBUM/SONGS LISTS_ ||
| `getAlbumList` | `byYear` and `byGenre` are not implemented | | `getAlbumList` | `byYear` and `byGenre` are not implemented |
| `getAlbumList2` | `byYear` and `byGenre` are not implemented | | `getAlbumList2` | `byYear` and `byGenre` are not implemented |
| `getStarred` | Doesn't return any artists, as iTunes does not support starred (loved) artists | | `getStarred` | |
| `getStarred2` | Doesn't return any artists, as iTunes does not support starred (loved) artists | | `getStarred2` | |
| `getNowPlaying` | | | `getNowPlaying` | |
| `getRandomSongs` | Ignores `genre` and `year` parameters | | `getRandomSongs` | Ignores `year` parameter |
| || | ||
| _SEARCHING_ || | _SEARCHING_ ||
| `search2` | Doesn't support Lucene queries, only simple auto complete queries | | `search2` | Doesn't support Lucene queries, only simple auto complete queries |

View File

@ -1,11 +1,9 @@
package engine package engine
import ( import (
"math/rand"
"time" "time"
"github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/model"
"github.com/cloudsonic/sonic-server/utils"
) )
type ListGenerator interface { type ListGenerator interface {
@ -19,7 +17,7 @@ type ListGenerator interface {
GetStarred(offset int, size int) (Entries, error) GetStarred(offset int, size int) (Entries, error)
GetAllStarred() (artists Entries, albums Entries, mediaFiles Entries, err error) GetAllStarred() (artists Entries, albums Entries, mediaFiles Entries, err error)
GetNowPlaying() (Entries, error) GetNowPlaying() (Entries, error)
GetRandomSongs(size int) (Entries, error) GetRandomSongs(size int, genre string) (Entries, error)
} }
func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator { func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator {
@ -71,41 +69,31 @@ func (g *listGenerator) GetByArtist(offset int, size int) (Entries, error) {
} }
func (g *listGenerator) GetRandom(offset int, size int) (Entries, error) { func (g *listGenerator) GetRandom(offset int, size int) (Entries, error) {
ids, err := g.ds.Album().GetAllIds() albums, err := g.ds.Album().GetRandom(model.QueryOptions{Max: size, Offset: offset})
if err != nil { if err != nil {
return nil, err return nil, err
} }
size = utils.MinInt(size, len(ids))
perm := rand.Perm(size)
r := make(Entries, size)
for i := 0; i < size; i++ { r := make(Entries, len(albums))
v := perm[i] for i, al := range albums {
al, err := g.ds.Album().Get((ids)[v]) r[i] = FromAlbum(&al)
if err != nil {
return nil, err
}
r[i] = FromAlbum(al)
} }
return r, nil return r, nil
} }
func (g *listGenerator) GetRandomSongs(size int) (Entries, error) { func (g *listGenerator) GetRandomSongs(size int, genre string) (Entries, error) {
ids, err := g.ds.MediaFile().GetAllIds() options := model.QueryOptions{Max: size}
if genre != "" {
options.Filters = map[string]interface{}{"genre": genre}
}
mediaFiles, err := g.ds.MediaFile().GetRandom(options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
size = utils.MinInt(size, len(ids))
perm := rand.Perm(size)
r := make(Entries, size)
for i := 0; i < size; i++ { r := make(Entries, len(mediaFiles))
v := perm[i] for i, mf := range mediaFiles {
mf, err := g.ds.MediaFile().Get(ids[v]) r[i] = FromMediaFile(&mf)
if err != nil {
return nil, err
}
r[i] = FromMediaFile(mf)
} }
return r, nil return r, nil
} }

View File

@ -33,7 +33,7 @@ type AlbumRepository interface {
Get(id string) (*Album, error) Get(id string) (*Album, error)
FindByArtist(artistId string) (Albums, error) FindByArtist(artistId string) (Albums, error)
GetAll(...QueryOptions) (Albums, error) GetAll(...QueryOptions) (Albums, error)
GetAllIds() ([]string, error) GetRandom(...QueryOptions) (Albums, error)
GetStarred(...QueryOptions) (Albums, error) GetStarred(...QueryOptions) (Albums, error)
Search(q string, offset int, size int) (Albums, error) Search(q string, offset int, size int) (Albums, error)
Refresh(ids ...string) error Refresh(ids ...string) error

View File

@ -47,7 +47,7 @@ type MediaFileRepository interface {
FindByAlbum(albumId string) (MediaFiles, error) FindByAlbum(albumId string) (MediaFiles, error)
FindByPath(path string) (MediaFiles, error) FindByPath(path string) (MediaFiles, error)
GetStarred(options ...QueryOptions) (MediaFiles, error) GetStarred(options ...QueryOptions) (MediaFiles, error)
GetAllIds() ([]string, error) GetRandom(options ...QueryOptions) (MediaFiles, error)
Search(q string, offset int, size int) (MediaFiles, error) Search(q string, offset int, size int) (MediaFiles, error)
Delete(id string) error Delete(id string) error
DeleteByPath(path string) error DeleteByPath(path string) error

View File

@ -79,6 +79,24 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e
return r.toAlbums(all), nil return r.toAlbums(all), nil
} }
// TODO Keep order when paginating
func (r *albumRepository) GetRandom(options ...model.QueryOptions) (model.Albums, error) {
sq := r.newRawQuery(options...)
switch r.ormer.Driver().Type() {
case orm.DRMySQL:
sq = sq.OrderBy("RAND()")
default:
sq = sq.OrderBy("RANDOM()")
}
sql, args, err := sq.ToSql()
if err != nil {
return nil, err
}
var results []album
_, err = r.ormer.Raw(sql, args...).QueryRows(&results)
return r.toAlbums(results), err
}
func (r *albumRepository) toAlbums(all []album) model.Albums { func (r *albumRepository) toAlbums(all []album) model.Albums {
result := make(model.Albums, len(all)) result := make(model.Albums, len(all))
for i, a := range all { for i, a := range all {

View File

@ -42,12 +42,6 @@ var _ = Describe("AlbumRepository", func() {
}) })
}) })
Describe("GetAllIds", func() {
It("returns all records", func() {
Expect(repo.GetAllIds()).To(ConsistOf("1", "2", "3"))
})
})
Describe("GetStarred", func() { Describe("GetStarred", func() {
It("returns all starred records", func() { It("returns all starred records", func() {
Expect(repo.GetStarred(model.QueryOptions{})).To(Equal(model.Albums{ Expect(repo.GetStarred(model.QueryOptions{})).To(Equal(model.Albums{

View File

@ -127,6 +127,23 @@ func (r *mediaFileRepository) DeleteByPath(path string) error {
return err return err
} }
func (r *mediaFileRepository) GetRandom(options ...model.QueryOptions) (model.MediaFiles, error) {
sq := r.newRawQuery(options...)
switch r.ormer.Driver().Type() {
case orm.DRMySQL:
sq = sq.OrderBy("RAND()")
default:
sq = sq.OrderBy("RANDOM()")
}
sql, args, err := sq.ToSql()
if err != nil {
return nil, err
}
var results []mediaFile
_, err = r.ormer.Raw(sql, args...).QueryRows(&results)
return r.toMediaFiles(results), err
}
func (r *mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.MediaFiles, error) { func (r *mediaFileRepository) GetStarred(options ...model.QueryOptions) (model.MediaFiles, error) {
var starred []mediaFile var starred []mediaFile
_, err := r.newQuery(options...).Filter("starred", true).All(&starred) _, err := r.newQuery(options...).Filter("starred", true).All(&starred)

View File

@ -1,6 +1,7 @@
package persistence package persistence
import ( import (
"github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
"github.com/cloudsonic/sonic-server/model" "github.com/cloudsonic/sonic-server/model"
) )
@ -29,6 +30,29 @@ func (r *sqlRepository) newQuery(options ...model.QueryOptions) orm.QuerySeter {
return q return q
} }
func (r *sqlRepository) newRawQuery(options ...model.QueryOptions) squirrel.SelectBuilder {
sq := squirrel.Select("*").From(r.tableName)
if len(options) > 0 {
if options[0].Max > 0 {
sq = sq.Limit(uint64(options[0].Max))
}
if options[0].Offset > 0 {
sq = sq.Offset(uint64(options[0].Max))
}
if options[0].Sort != "" {
if options[0].Order == "desc" {
sq = sq.OrderBy(options[0].Sort + " desc")
} else {
sq = sq.OrderBy(options[0].Sort)
}
}
for field, value := range options[0].Filters {
sq = sq.Where(squirrel.Like{field: value.(string) + "%"})
}
}
return sq
}
func (r *sqlRepository) CountAll() (int64, error) { func (r *sqlRepository) CountAll() (int64, error) {
return r.newQuery().Count() return r.newQuery().Count()
} }
@ -38,22 +62,6 @@ func (r *sqlRepository) Exists(id string) (bool, error) {
return c == 1, err return c == 1, err
} }
// TODO This is used to generate random lists. Can be optimized in SQL: https://stackoverflow.com/a/19419
func (r *sqlRepository) GetAllIds() ([]string, error) {
qs := r.newQuery()
var values []orm.Params
num, err := qs.Values(&values, "id")
if num == 0 {
return nil, err
}
result := collectField(values, func(item interface{}) string {
return item.(orm.Params)["ID"].(string)
})
return result, nil
}
// "Hack" to bypass Postgres driver limitation // "Hack" to bypass Postgres driver limitation
func (r *sqlRepository) insert(record interface{}) error { func (r *sqlRepository) insert(record interface{}) error {
_, err := r.ormer.Insert(record) _, err := r.ormer.Insert(record)

View File

@ -132,8 +132,9 @@ func (c *AlbumListController) GetNowPlaying(w http.ResponseWriter, r *http.Reque
func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
size := utils.MinInt(ParamInt(r, "size", 10), 500) size := utils.MinInt(ParamInt(r, "size", 10), 500)
genre := ParamString(r, "genre")
songs, err := c.listGen.GetRandomSongs(size) songs, err := c.listGen.GetRandomSongs(size, genre)
if err != nil { if err != nil {
log.Error(r, "Error retrieving random songs", "error", err) log.Error(r, "Error retrieving random songs", "error", err)
return nil, NewError(responses.ErrorGeneric, "Internal Error") return nil, NewError(responses.ErrorGeneric, "Internal Error")