diff --git a/API_COMPATIBILITY.md b/API_COMPATIBILITY.md index ca0d11e99..62106782c 100644 --- a/API_COMPATIBILITY.md +++ b/API_COMPATIBILITY.md @@ -32,10 +32,10 @@ CloudSonic and Subsonic: | _ALBUM/SONGS LISTS_ || | `getAlbumList` | `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 | -| `getStarred2` | Doesn't return any artists, as iTunes does not support starred (loved) artists | +| `getStarred` | | +| `getStarred2` | | | `getNowPlaying` | | -| `getRandomSongs` | Ignores `genre` and `year` parameters | +| `getRandomSongs` | Ignores `year` parameter | | || | _SEARCHING_ || | `search2` | Doesn't support Lucene queries, only simple auto complete queries | diff --git a/engine/list_generator.go b/engine/list_generator.go index 09e994553..b7468f65d 100644 --- a/engine/list_generator.go +++ b/engine/list_generator.go @@ -1,11 +1,9 @@ package engine import ( - "math/rand" "time" "github.com/cloudsonic/sonic-server/model" - "github.com/cloudsonic/sonic-server/utils" ) type ListGenerator interface { @@ -19,7 +17,7 @@ type ListGenerator interface { GetStarred(offset int, size int) (Entries, error) GetAllStarred() (artists Entries, albums Entries, mediaFiles Entries, err error) GetNowPlaying() (Entries, error) - GetRandomSongs(size int) (Entries, error) + GetRandomSongs(size int, genre string) (Entries, error) } 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) { - ids, err := g.ds.Album().GetAllIds() + albums, err := g.ds.Album().GetRandom(model.QueryOptions{Max: size, Offset: offset}) if err != nil { return nil, err } - size = utils.MinInt(size, len(ids)) - perm := rand.Perm(size) - r := make(Entries, size) - for i := 0; i < size; i++ { - v := perm[i] - al, err := g.ds.Album().Get((ids)[v]) - if err != nil { - return nil, err - } - r[i] = FromAlbum(al) + r := make(Entries, len(albums)) + for i, al := range albums { + r[i] = FromAlbum(&al) } return r, nil } -func (g *listGenerator) GetRandomSongs(size int) (Entries, error) { - ids, err := g.ds.MediaFile().GetAllIds() +func (g *listGenerator) GetRandomSongs(size int, genre string) (Entries, error) { + options := model.QueryOptions{Max: size} + if genre != "" { + options.Filters = map[string]interface{}{"genre": genre} + } + mediaFiles, err := g.ds.MediaFile().GetRandom(options) if err != nil { return nil, err } - size = utils.MinInt(size, len(ids)) - perm := rand.Perm(size) - r := make(Entries, size) - for i := 0; i < size; i++ { - v := perm[i] - mf, err := g.ds.MediaFile().Get(ids[v]) - if err != nil { - return nil, err - } - r[i] = FromMediaFile(mf) + r := make(Entries, len(mediaFiles)) + for i, mf := range mediaFiles { + r[i] = FromMediaFile(&mf) } return r, nil } diff --git a/model/album.go b/model/album.go index bb9bb3041..22c420552 100644 --- a/model/album.go +++ b/model/album.go @@ -33,7 +33,7 @@ type AlbumRepository interface { Get(id string) (*Album, error) FindByArtist(artistId string) (Albums, error) GetAll(...QueryOptions) (Albums, error) - GetAllIds() ([]string, error) + GetRandom(...QueryOptions) (Albums, error) GetStarred(...QueryOptions) (Albums, error) Search(q string, offset int, size int) (Albums, error) Refresh(ids ...string) error diff --git a/model/mediafile.go b/model/mediafile.go index 0f6fa4321..8e3d15e93 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -47,7 +47,7 @@ type MediaFileRepository interface { FindByAlbum(albumId string) (MediaFiles, error) FindByPath(path string) (MediaFiles, error) GetStarred(options ...QueryOptions) (MediaFiles, error) - GetAllIds() ([]string, error) + GetRandom(options ...QueryOptions) (MediaFiles, error) Search(q string, offset int, size int) (MediaFiles, error) Delete(id string) error DeleteByPath(path string) error diff --git a/persistence/album_repository.go b/persistence/album_repository.go index f5e402e88..2f17ff5b7 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -79,6 +79,24 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e 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 { result := make(model.Albums, len(all)) for i, a := range all { diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go index fa3e0d55e..59a37e660 100644 --- a/persistence/album_repository_test.go +++ b/persistence/album_repository_test.go @@ -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() { It("returns all starred records", func() { Expect(repo.GetStarred(model.QueryOptions{})).To(Equal(model.Albums{ diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index a38f4c522..39756f091 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -127,6 +127,23 @@ func (r *mediaFileRepository) DeleteByPath(path string) error { 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) { var starred []mediaFile _, err := r.newQuery(options...).Filter("starred", true).All(&starred) diff --git a/persistence/sql_repository.go b/persistence/sql_repository.go index c8a976343..c88de2ef1 100644 --- a/persistence/sql_repository.go +++ b/persistence/sql_repository.go @@ -1,6 +1,7 @@ package persistence import ( + "github.com/Masterminds/squirrel" "github.com/astaxie/beego/orm" "github.com/cloudsonic/sonic-server/model" ) @@ -29,6 +30,29 @@ func (r *sqlRepository) newQuery(options ...model.QueryOptions) orm.QuerySeter { 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) { return r.newQuery().Count() } @@ -38,22 +62,6 @@ func (r *sqlRepository) Exists(id string) (bool, error) { 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 func (r *sqlRepository) insert(record interface{}) error { _, err := r.ormer.Insert(record) diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index b553feddf..f5f18924d 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -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) { 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 { log.Error(r, "Error retrieving random songs", "error", err) return nil, NewError(responses.ErrorGeneric, "Internal Error")