diff --git a/api/wire_gen.go b/api/wire_gen.go index 82b092972..e1149dd79 100644 --- a/api/wire_gen.go +++ b/api/wire_gen.go @@ -27,7 +27,7 @@ func initBrowsingController() *BrowsingController { mediaFolderRepository := ledis.NewMediaFolderRepository() artistIndexRepository := ledis.NewArtistIndexRepository() artistRepository := storm.NewArtistRepository() - albumRepository := ledis.NewAlbumRepository() + albumRepository := storm.NewAlbumRepository() mediaFileRepository := ledis.NewMediaFileRepository() browser := engine.NewBrowser(propertyRepository, mediaFolderRepository, artistIndexRepository, artistRepository, albumRepository, mediaFileRepository) browsingController := NewBrowsingController(browser) @@ -35,7 +35,7 @@ func initBrowsingController() *BrowsingController { } func initAlbumListController() *AlbumListController { - albumRepository := ledis.NewAlbumRepository() + albumRepository := storm.NewAlbumRepository() mediaFileRepository := ledis.NewMediaFileRepository() nowPlayingRepository := ledis.NewNowPlayingRepository() listGenerator := engine.NewListGenerator(albumRepository, mediaFileRepository, nowPlayingRepository) @@ -48,7 +48,7 @@ func initMediaAnnotationController() *MediaAnnotationController { mediaFileRepository := ledis.NewMediaFileRepository() nowPlayingRepository := ledis.NewNowPlayingRepository() scrobbler := engine.NewScrobbler(itunesControl, mediaFileRepository, nowPlayingRepository) - albumRepository := ledis.NewAlbumRepository() + albumRepository := storm.NewAlbumRepository() artistRepository := storm.NewArtistRepository() ratings := engine.NewRatings(itunesControl, mediaFileRepository, albumRepository, artistRepository) mediaAnnotationController := NewMediaAnnotationController(scrobbler, ratings) @@ -66,7 +66,7 @@ func initPlaylistsController() *PlaylistsController { func initSearchingController() *SearchingController { artistRepository := storm.NewArtistRepository() - albumRepository := ledis.NewAlbumRepository() + albumRepository := storm.NewAlbumRepository() mediaFileRepository := ledis.NewMediaFileRepository() db := newDB() search := engine.NewSearch(artistRepository, albumRepository, mediaFileRepository, db) @@ -81,7 +81,7 @@ func initUsersController() *UsersController { func initMediaRetrievalController() *MediaRetrievalController { mediaFileRepository := ledis.NewMediaFileRepository() - albumRepository := ledis.NewAlbumRepository() + albumRepository := storm.NewAlbumRepository() cover := engine.NewCover(mediaFileRepository, albumRepository) mediaRetrievalController := NewMediaRetrievalController(cover) return mediaRetrievalController diff --git a/persistence/ledis/wire_providers.go b/persistence/ledis/wire_providers.go index 4cf7a5999..adb3ccc9a 100644 --- a/persistence/ledis/wire_providers.go +++ b/persistence/ledis/wire_providers.go @@ -3,7 +3,6 @@ package ledis import "github.com/google/wire" var Set = wire.NewSet( - NewAlbumRepository, NewCheckSumRepository, NewArtistIndexRepository, NewMediaFileRepository, diff --git a/persistence/storm/album_repository.go b/persistence/storm/album_repository.go new file mode 100644 index 000000000..6193ec564 --- /dev/null +++ b/persistence/storm/album_repository.go @@ -0,0 +1,122 @@ +package storm + +import ( + "time" + + "github.com/asdine/storm/q" + "github.com/cloudsonic/sonic-server/domain" +) + +type _Album struct { + ID string `` + Name string `storm:"index"` + ArtistID string `storm:"index"` + CoverArtPath string `` + CoverArtId string `` + Artist string `storm:"index"` + AlbumArtist string `` + Year int `storm:"index"` + Compilation bool `` + Starred bool `storm:"index"` + PlayCount int `storm:"index"` + PlayDate time.Time `storm:"index"` + SongCount int `` + Duration int `` + Rating int `storm:"index"` + Genre string `` + StarredAt time.Time `storm:"index"` + CreatedAt time.Time `storm:"index"` + UpdatedAt time.Time `` +} + +type albumRepository struct { + stormRepository +} + +func NewAlbumRepository() domain.AlbumRepository { + r := &albumRepository{} + r.init(&_Album{}) + return r +} + +func (r *albumRepository) Put(a *domain.Album) error { + ta := _Album(*a) + return Db().Save(&ta) +} + +func (r *albumRepository) Get(id string) (*domain.Album, error) { + ta := &_Album{} + err := r.getByID(id, ta) + if err != nil { + return nil, err + } + a := domain.Album(*ta) + return &a, err +} + +func (r *albumRepository) FindByArtist(artistId string) (domain.Albums, error) { + var albums []_Album + err := r.execute(q.Eq("ArtistID", artistId), &albums) + if err != nil { + return nil, err + } + return r.toDomainList(albums) +} + +func (r *albumRepository) GetAll(options domain.QueryOptions) (domain.Albums, error) { + all, err := r.getAll(&options) + if err != nil { + return nil, err + } + return r.toDomainList(all) +} + +func (r *albumRepository) toDomainList(all []_Album) (domain.Albums, error) { + result := make(domain.Albums, len(all)) + for i, a := range all { + result[i] = domain.Album(a) + } + return result, nil +} + +func (r *albumRepository) GetAllIds() ([]string, error) { + all, err := r.getAll(&domain.QueryOptions{}) + if err != nil { + return nil, err + } + result := make([]string, len(all)) + for i, a := range all { + result[i] = domain.Album(a).ID + } + return result, nil +} + +func (r *albumRepository) getAll(options *domain.QueryOptions) (all []_Album, err error) { + if options.SortBy != "" { + err = Db().AllByIndex(options.SortBy, &all, stormOptions(options)) + } else { + err = Db().All(&all, stormOptions(options)) + } + return +} + +func (r *albumRepository) PurgeInactive(active domain.Albums) ([]string, error) { + activeIDs := make([]string, len(active)) + for i, album := range active { + activeIDs[i] = album.ID + } + + return r.purgeInactive(activeIDs) +} + +func (r *albumRepository) GetStarred(options domain.QueryOptions) (domain.Albums, error) { + var starred []_Album + err := r.execute(q.Eq("Starred", true), &starred, &options) + if err != nil { + return nil, err + } + return r.toDomainList(starred) +} + +var _ domain.AlbumRepository = (*albumRepository)(nil) +var _ = domain.Album(_Album{}) diff --git a/persistence/storm/album_repository_test.go b/persistence/storm/album_repository_test.go new file mode 100644 index 000000000..f4c1937da --- /dev/null +++ b/persistence/storm/album_repository_test.go @@ -0,0 +1,77 @@ +package storm + +import ( + "github.com/cloudsonic/sonic-server/domain" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AlbumRepository", func() { + var repo domain.AlbumRepository + var data domain.Albums + + BeforeEach(func() { + Db().Drop(&_Album{}) + repo = NewAlbumRepository() + data = domain.Albums{ + {ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "1"}, + {ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "1"}, + {ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Starred: true}, + } + for _, a := range data { + repo.Put(&a) + } + }) + + Describe("GetAll", func() { + It("returns all records", func() { + Expect(repo.GetAll(domain.QueryOptions{})).To(Equal(data)) + }) + + It("returns all records sorted", func() { + Expect(repo.GetAll(domain.QueryOptions{SortBy: "Name"})).To(Equal(domain.Albums{ + {ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "1"}, + {ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Starred: true}, + {ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "1"}, + })) + }) + + It("returns all records sorted desc", func() { + Expect(repo.GetAll(domain.QueryOptions{SortBy: "Name", Desc: true})).To(Equal(domain.Albums{ + {ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "1"}, + {ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Starred: true}, + {ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "1"}, + })) + }) + + It("paginates the result", func() { + Expect(repo.GetAll(domain.QueryOptions{Offset: 1, Size: 1})).To(Equal(domain.Albums{ + {ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "1"}, + })) + }) + }) + + Describe("GetAllIds", func() { + It("returns all records", func() { + Expect(repo.GetAllIds()).To(Equal([]string{"1", "2", "3"})) + }) + }) + + Describe("GetStarred", func() { + It("returns all starred records", func() { + Expect(repo.GetStarred(domain.QueryOptions{})).To(Equal(domain.Albums{ + {ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Starred: true}, + })) + }) + }) + + Describe("FindByArtist", func() { + It("returns all records from a given ArtistID", func() { + Expect(repo.FindByArtist("1")).To(Equal(domain.Albums{ + {ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "1"}, + {ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "1"}, + })) + }) + }) + +}) diff --git a/persistence/storm/artist_repository.go b/persistence/storm/artist_repository.go index 647dd7ca0..9dfb86284 100644 --- a/persistence/storm/artist_repository.go +++ b/persistence/storm/artist_repository.go @@ -28,10 +28,12 @@ func (r *artistRepository) Put(a *domain.Artist) error { func (r *artistRepository) Get(id string) (*domain.Artist, error) { ta := &_Artist{} - - err := Db().One("ID", id, ta) + err := r.getByID(id, ta) + if err != nil { + return nil, err + } a := domain.Artist(*ta) - return &a, err + return &a, nil } func (r *artistRepository) PurgeInactive(active domain.Artists) ([]string, error) { diff --git a/persistence/storm/artist_repository_test.go b/persistence/storm/artist_repository_test.go index c6f0843dd..8f9738afc 100644 --- a/persistence/storm/artist_repository_test.go +++ b/persistence/storm/artist_repository_test.go @@ -25,6 +25,11 @@ var _ = Describe("ArtistRepository", func() { Expect(repo.Get("1")).To(Equal(artist)) }) + It("returns ErrNotFound when the ID does not exist", func() { + _, err := repo.Get("999") + Expect(err).To(MatchError(domain.ErrNotFound)) + }) + Describe("PurgeInactive", func() { var data domain.Artists diff --git a/persistence/storm/property_repository.go b/persistence/storm/property_repository.go index 5d90c1178..73cea84f9 100644 --- a/persistence/storm/property_repository.go +++ b/persistence/storm/property_repository.go @@ -22,12 +22,15 @@ func (r *propertyRepository) Put(id string, value string) error { func (r *propertyRepository) Get(id string) (string, error) { var value string err := Db().Get(propertyBucket, id, &value) + if err == storm.ErrNotFound { + return value, domain.ErrNotFound + } return value, err } func (r *propertyRepository) DefaultGet(id string, defaultValue string) (string, error) { value, err := r.Get(id) - if err == storm.ErrNotFound { + if err == domain.ErrNotFound { return defaultValue, nil } if err != nil { diff --git a/persistence/storm/storm_repository.go b/persistence/storm/storm_repository.go index 26e3a97c2..d7ee76071 100644 --- a/persistence/storm/storm_repository.go +++ b/persistence/storm/storm_repository.go @@ -4,7 +4,9 @@ import ( "reflect" "github.com/asdine/storm" + "github.com/asdine/storm/index" "github.com/asdine/storm/q" + "github.com/cloudsonic/sonic-server/domain" ) type stormRepository struct { @@ -34,18 +36,26 @@ func (r *stormRepository) Exists(id string) (bool, error) { return err != storm.ErrNotFound, nil } -func (r *stormRepository) getID(record interface{}) string { +func (r *stormRepository) extractID(record interface{}) string { v := reflect.ValueOf(record).Elem() id := v.FieldByName("ID").String() return id } +func (r *stormRepository) getByID(id string, ta interface{}) error { + err := Db().One("ID", id, ta) + if err == storm.ErrNotFound { + return domain.ErrNotFound + } + return nil +} + func (r *stormRepository) purgeInactive(ids []string) (deleted []string, err error) { query := Db().Select(q.Not(q.In("ID", ids))) // Collect IDs that will be deleted err = query.Each(r.bucket, func(record interface{}) error { - id := r.getID(record) + id := r.extractID(record) deleted = append(deleted, id) return nil }) @@ -63,3 +73,38 @@ func (r *stormRepository) purgeInactive(ids []string) (deleted []string, err err } return deleted, nil } + +func (r *stormRepository) execute(matcher q.Matcher, result *[]_Album, options ...*domain.QueryOptions) error { + query := Db().Select(matcher) + if len(options) > 0 { + query = addQueryOptions(query, options[0]) + } + err := query.Find(result) + if err == storm.ErrNotFound { + return nil + } + return err +} + +func stormOptions(options *domain.QueryOptions) func(*index.Options) { + return func(opts *index.Options) { + opts.Reverse = options.Desc + opts.Skip = options.Offset + if options.Size > 0 { + opts.Limit = options.Size + } + } +} + +func addQueryOptions(q storm.Query, o *domain.QueryOptions) storm.Query { + if o.SortBy != "" { + q = q.OrderBy(o.SortBy) + } + if o.Desc { + q = q.Reverse() + } + if o.Size > 0 { + q = q.Limit(o.Size) + } + return q.Skip(o.Offset) +} diff --git a/persistence/storm/wire_providers.go b/persistence/storm/wire_providers.go index 30bfaf499..a8a3d638b 100644 --- a/persistence/storm/wire_providers.go +++ b/persistence/storm/wire_providers.go @@ -5,4 +5,5 @@ import "github.com/google/wire" var Set = wire.NewSet( NewPropertyRepository, NewArtistRepository, + NewAlbumRepository, ) diff --git a/wire_gen.go b/wire_gen.go index 4a7818ee2..3eb72cd90 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -22,7 +22,7 @@ func initImporter(musicFolder string) *scanner.Importer { checkSumRepository := ledis.NewCheckSumRepository() itunesScanner := scanner.NewItunesScanner(checkSumRepository) mediaFileRepository := ledis.NewMediaFileRepository() - albumRepository := ledis.NewAlbumRepository() + albumRepository := storm.NewAlbumRepository() artistRepository := storm.NewArtistRepository() artistIndexRepository := ledis.NewArtistIndexRepository() playlistRepository := ledis.NewPlaylistRepository()