Big Refactor:

- Create model.DataStore, with provision for transactions
- Change all layers dependencies on repositories to use DataStore
- Implemented persistence.SQLStore
- Removed iTunes Bridge/Importer support
This commit is contained in:
Deluan
2020-01-19 15:37:41 -05:00
parent 40186f7e10
commit 67eeb218c4
47 changed files with 389 additions and 1621 deletions
+14 -20
View File
@@ -23,26 +23,20 @@ type Browser interface {
GetGenres() (model.Genres, error)
}
func NewBrowser(pr model.PropertyRepository, fr model.MediaFolderRepository,
ar model.ArtistRepository, alr model.AlbumRepository, mr model.MediaFileRepository, gr model.GenreRepository) Browser {
return &browser{pr, fr, ar, alr, mr, gr}
func NewBrowser(ds model.DataStore) Browser {
return &browser{ds}
}
type browser struct {
propRepo model.PropertyRepository
folderRepo model.MediaFolderRepository
artistRepo model.ArtistRepository
albumRepo model.AlbumRepository
mfileRepo model.MediaFileRepository
genreRepo model.GenreRepository
ds model.DataStore
}
func (b *browser) MediaFolders() (model.MediaFolders, error) {
return b.folderRepo.GetAll()
return b.ds.MediaFolder().GetAll()
}
func (b *browser) Indexes(ifModifiedSince time.Time) (model.ArtistIndexes, time.Time, error) {
l, err := b.propRepo.DefaultGet(model.PropLastScan, "-1")
l, err := b.ds.Property().DefaultGet(model.PropLastScan, "-1")
ms, _ := strconv.ParseInt(l, 10, 64)
lastModified := utils.ToTime(ms)
@@ -51,7 +45,7 @@ func (b *browser) Indexes(ifModifiedSince time.Time) (model.ArtistIndexes, time.
}
if lastModified.After(ifModifiedSince) {
indexes, err := b.artistRepo.GetIndex()
indexes, err := b.ds.Artist().GetIndex()
return indexes, lastModified, err
}
@@ -108,7 +102,7 @@ func (b *browser) Directory(ctx context.Context, id string) (*DirectoryInfo, err
}
func (b *browser) GetSong(id string) (*Entry, error) {
mf, err := b.mfileRepo.Get(id)
mf, err := b.ds.MediaFile().Get(id)
if err != nil {
return nil, err
}
@@ -118,7 +112,7 @@ func (b *browser) GetSong(id string) (*Entry, error) {
}
func (b *browser) GetGenres() (model.Genres, error) {
genres, err := b.genreRepo.GetAll()
genres, err := b.ds.Genre().GetAll()
for i, g := range genres {
if strings.TrimSpace(g.Name) == "" {
genres[i].Name = "<Empty>"
@@ -171,7 +165,7 @@ func (b *browser) buildAlbumDir(al *model.Album, tracks model.MediaFiles) *Direc
}
func (b *browser) isArtist(ctx context.Context, id string) bool {
found, err := b.artistRepo.Exists(id)
found, err := b.ds.Artist().Exists(id)
if err != nil {
log.Debug(ctx, "Error searching for Artist", "id", id, err)
return false
@@ -180,7 +174,7 @@ func (b *browser) isArtist(ctx context.Context, id string) bool {
}
func (b *browser) isAlbum(ctx context.Context, id string) bool {
found, err := b.albumRepo.Exists(id)
found, err := b.ds.Album().Exists(id)
if err != nil {
log.Debug(ctx, "Error searching for Album", "id", id, err)
return false
@@ -189,26 +183,26 @@ func (b *browser) isAlbum(ctx context.Context, id string) bool {
}
func (b *browser) retrieveArtist(id string) (a *model.Artist, as model.Albums, err error) {
a, err = b.artistRepo.Get(id)
a, err = b.ds.Artist().Get(id)
if err != nil {
err = fmt.Errorf("Error reading Artist %s from DB: %v", id, err)
return
}
if as, err = b.albumRepo.FindByArtist(id); err != nil {
if as, err = b.ds.Album().FindByArtist(id); err != nil {
err = fmt.Errorf("Error reading %s's albums from DB: %v", a.Name, err)
}
return
}
func (b *browser) retrieveAlbum(id string) (al *model.Album, mfs model.MediaFiles, err error) {
al, err = b.albumRepo.Get(id)
al, err = b.ds.Album().Get(id)
if err != nil {
err = fmt.Errorf("Error reading Album %s from DB: %v", id, err)
return
}
if mfs, err = b.mfileRepo.FindByAlbum(id); err != nil {
if mfs, err = b.ds.MediaFile().FindByAlbum(id); err != nil {
err = fmt.Errorf("Error reading %s's tracks from DB: %v", al.Name, err)
}
return
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"errors"
"github.com/cloudsonic/sonic-server/model"
"github.com/cloudsonic/sonic-server/persistence"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
@@ -18,7 +19,8 @@ var _ = Describe("Browser", func() {
{Name: "", SongCount: 13, AlbumCount: 13},
{Name: "Electronic", SongCount: 4000, AlbumCount: 40},
}}
b = &browser{genreRepo: repo}
var ds = &persistence.MockDataStore{MockedGenre: repo}
b = &browser{ds: ds}
})
It("returns sorted data", func() {
+5 -6
View File
@@ -20,25 +20,24 @@ type Cover interface {
}
type cover struct {
mfileRepo model.MediaFileRepository
albumRepo model.AlbumRepository
ds model.DataStore
}
func NewCover(mr model.MediaFileRepository, alr model.AlbumRepository) Cover {
return &cover{mr, alr}
func NewCover(ds model.DataStore) Cover {
return &cover{ds}
}
func (c *cover) getCoverPath(id string) (string, error) {
switch {
case strings.HasPrefix(id, "al-"):
id = id[3:]
al, err := c.albumRepo.Get(id)
al, err := c.ds.Album().Get(id)
if err != nil {
return "", err
}
return al.CoverArtPath, nil
default:
mf, err := c.mfileRepo.Get(id)
mf, err := c.ds.MediaFile().Get(id)
if err != nil {
return "", err
}
+4 -3
View File
@@ -15,10 +15,11 @@ import (
func TestCover(t *testing.T) {
Init(t, false)
mockMediaFileRepo := persistence.CreateMockMediaFileRepo()
mockAlbumRepo := persistence.CreateMockAlbumRepo()
ds := &persistence.MockDataStore{}
mockMediaFileRepo := ds.MediaFile().(*persistence.MockMediaFile)
mockAlbumRepo := ds.Album().(*persistence.MockAlbum)
cover := engine.NewCover(mockMediaFileRepo, mockAlbumRepo)
cover := engine.NewCover(ds)
out := new(bytes.Buffer)
Convey("Subject: GetCoverArt Endpoint", t, func() {
+13 -15
View File
@@ -22,22 +22,20 @@ type ListGenerator interface {
GetRandomSongs(size int) (Entries, error)
}
func NewListGenerator(arr model.ArtistRepository, alr model.AlbumRepository, mfr model.MediaFileRepository, npr NowPlayingRepository) ListGenerator {
return &listGenerator{arr, alr, mfr, npr}
func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator {
return &listGenerator{ds, npRepo}
}
type listGenerator struct {
artistRepo model.ArtistRepository
albumRepo model.AlbumRepository
mfRepository model.MediaFileRepository
npRepo NowPlayingRepository
ds model.DataStore
npRepo NowPlayingRepository
}
// TODO: Only return albums that have the SortBy field != empty
func (g *listGenerator) query(qo model.QueryOptions, offset int, size int) (Entries, error) {
qo.Offset = offset
qo.Size = size
albums, err := g.albumRepo.GetAll(qo)
albums, err := g.ds.Album().GetAll(qo)
return FromAlbums(albums), err
}
@@ -73,7 +71,7 @@ func (g *listGenerator) GetByArtist(offset int, size int) (Entries, error) {
}
func (g *listGenerator) GetRandom(offset int, size int) (Entries, error) {
ids, err := g.albumRepo.GetAllIds()
ids, err := g.ds.Album().GetAllIds()
if err != nil {
return nil, err
}
@@ -83,7 +81,7 @@ func (g *listGenerator) GetRandom(offset int, size int) (Entries, error) {
for i := 0; i < size; i++ {
v := perm[i]
al, err := g.albumRepo.Get((ids)[v])
al, err := g.ds.Album().Get((ids)[v])
if err != nil {
return nil, err
}
@@ -93,7 +91,7 @@ func (g *listGenerator) GetRandom(offset int, size int) (Entries, error) {
}
func (g *listGenerator) GetRandomSongs(size int) (Entries, error) {
ids, err := g.mfRepository.GetAllIds()
ids, err := g.ds.MediaFile().GetAllIds()
if err != nil {
return nil, err
}
@@ -103,7 +101,7 @@ func (g *listGenerator) GetRandomSongs(size int) (Entries, error) {
for i := 0; i < size; i++ {
v := perm[i]
mf, err := g.mfRepository.Get(ids[v])
mf, err := g.ds.MediaFile().Get(ids[v])
if err != nil {
return nil, err
}
@@ -114,7 +112,7 @@ func (g *listGenerator) GetRandomSongs(size int) (Entries, error) {
func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) {
qo := model.QueryOptions{Offset: offset, Size: size, SortBy: "starred_at", Desc: true}
albums, err := g.albumRepo.GetStarred(qo)
albums, err := g.ds.Album().GetStarred(qo)
if err != nil {
return nil, err
}
@@ -124,7 +122,7 @@ func (g *listGenerator) GetStarred(offset int, size int) (Entries, error) {
// TODO Return is confusing
func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) {
artists, err := g.artistRepo.GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true})
artists, err := g.ds.Artist().GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true})
if err != nil {
return nil, nil, nil, err
}
@@ -134,7 +132,7 @@ func (g *listGenerator) GetAllStarred() (Entries, Entries, Entries, error) {
return nil, nil, nil, err
}
mediaFiles, err := g.mfRepository.GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true})
mediaFiles, err := g.ds.MediaFile().GetStarred(model.QueryOptions{SortBy: "starred_at", Desc: true})
if err != nil {
return nil, nil, nil, err
}
@@ -149,7 +147,7 @@ func (g *listGenerator) GetNowPlaying() (Entries, error) {
}
entries := make(Entries, len(npInfo))
for i, np := range npInfo {
mf, err := g.mfRepository.Get(np.TrackID)
mf, err := g.ds.MediaFile().Get(np.TrackID)
if err != nil {
return nil, err
}
+10 -45
View File
@@ -2,10 +2,7 @@ package engine
import (
"context"
"sort"
"github.com/cloudsonic/sonic-server/itunesbridge"
"github.com/cloudsonic/sonic-server/log"
"github.com/cloudsonic/sonic-server/model"
)
@@ -17,18 +14,16 @@ type Playlists interface {
Update(playlistId string, name *string, idsToAdd []string, idxToRemove []int) error
}
func NewPlaylists(itunes itunesbridge.ItunesControl, pr model.PlaylistRepository, mr model.MediaFileRepository) Playlists {
return &playlists{itunes, pr, mr}
func NewPlaylists(ds model.DataStore) Playlists {
return &playlists{ds}
}
type playlists struct {
itunes itunesbridge.ItunesControl
plsRepo model.PlaylistRepository
mfileRepo model.MediaFileRepository
ds model.DataStore
}
func (p *playlists) GetAll() (model.Playlists, error) {
return p.plsRepo.GetAll(model.QueryOptions{})
return p.ds.Playlist().GetAll(model.QueryOptions{})
}
type PlaylistInfo struct {
@@ -43,52 +38,22 @@ type PlaylistInfo struct {
}
func (p *playlists) Create(ctx context.Context, name string, ids []string) error {
pid, err := p.itunes.CreatePlaylist(name, ids)
if err != nil {
return err
}
log.Info(ctx, "Created playlist", "playlist", name, "id", pid)
// TODO
return nil
}
func (p *playlists) Delete(ctx context.Context, playlistId string) error {
err := p.itunes.DeletePlaylist(playlistId)
if err != nil {
return err
}
log.Info(ctx, "Deleted playlist", "id", playlistId)
// TODO
return nil
}
func (p *playlists) Update(playlistId string, name *string, idsToAdd []string, idxToRemove []int) error {
pl, err := p.plsRepo.Get(playlistId)
if err != nil {
return err
}
if name != nil {
pl.Name = *name
err := p.itunes.RenamePlaylist(pl.ID, pl.Name)
if err != nil {
return err
}
}
if len(idsToAdd) > 0 || len(idxToRemove) > 0 {
sort.Sort(sort.Reverse(sort.IntSlice(idxToRemove)))
for _, i := range idxToRemove {
pl.Tracks, pl.Tracks[len(pl.Tracks)-1] = append(pl.Tracks[:i], pl.Tracks[i+1:]...), ""
}
pl.Tracks = append(pl.Tracks, idsToAdd...)
err := p.itunes.UpdatePlaylist(pl.ID, pl.Tracks)
if err != nil {
return err
}
}
p.plsRepo.Put(pl) // Ignores errors, as any changes will be overridden in the next scan
// TODO
return nil
}
func (p *playlists) Get(id string) (*PlaylistInfo, error) {
pl, err := p.plsRepo.Get(id)
pl, err := p.ds.Playlist().Get(id)
if err != nil {
return nil, err
}
@@ -96,7 +61,7 @@ func (p *playlists) Get(id string) (*PlaylistInfo, error) {
pinfo := &PlaylistInfo{
Id: pl.ID,
Name: pl.Name,
SongCount: len(pl.Tracks),
SongCount: len(pl.Tracks), // TODO Use model.Playlist
Duration: pl.Duration,
Public: pl.Public,
Owner: pl.Owner,
@@ -106,7 +71,7 @@ func (p *playlists) Get(id string) (*PlaylistInfo, error) {
// TODO Optimize: Get all tracks at once
for i, mfId := range pl.Tracks {
mf, err := p.mfileRepo.Get(mfId)
mf, err := p.ds.MediaFile().Get(mfId)
if err != nil {
return nil, err
}
+9 -69
View File
@@ -3,11 +3,7 @@ package engine
import (
"context"
"github.com/cloudsonic/sonic-server/conf"
"github.com/cloudsonic/sonic-server/itunesbridge"
"github.com/cloudsonic/sonic-server/log"
"github.com/cloudsonic/sonic-server/model"
"github.com/cloudsonic/sonic-server/utils"
)
type Ratings interface {
@@ -15,86 +11,30 @@ type Ratings interface {
SetRating(ctx context.Context, id string, rating int) error
}
func NewRatings(itunes itunesbridge.ItunesControl, mr model.MediaFileRepository, alr model.AlbumRepository, ar model.ArtistRepository) Ratings {
return &ratings{itunes, mr, alr, ar}
func NewRatings(ds model.DataStore) Ratings {
return &ratings{ds}
}
type ratings struct {
itunes itunesbridge.ItunesControl
mfRepo model.MediaFileRepository
albumRepo model.AlbumRepository
artistRepo model.ArtistRepository
ds model.DataStore
}
func (r ratings) SetRating(ctx context.Context, id string, rating int) error {
rating = utils.MinInt(rating, 5) * 20
isAlbum, _ := r.albumRepo.Exists(id)
if isAlbum {
mfs, _ := r.mfRepo.FindByAlbum(id)
if len(mfs) > 0 {
log.Debug(ctx, "Set Rating", "value", rating, "album", mfs[0].Album)
if err := r.itunes.SetAlbumRating(mfs[0].ID, rating); err != nil {
return err
}
}
return nil
}
mf, err := r.mfRepo.Get(id)
if err != nil {
return err
}
if mf != nil {
log.Debug(ctx, "Set Rating", "value", rating, "song", mf.Title)
if err := r.itunes.SetTrackRating(mf.ID, rating); err != nil {
return err
}
return nil
}
// TODO
return model.ErrNotFound
}
func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
if conf.Sonic.DevUseFileScanner {
err := r.mfRepo.SetStar(star, ids...)
return r.ds.WithTx(func(tx model.DataStore) error {
err := tx.MediaFile().SetStar(star, ids...)
if err != nil {
return err
}
err = r.albumRepo.SetStar(star, ids...)
err = tx.Album().SetStar(star, ids...)
if err != nil {
return err
}
err = r.artistRepo.SetStar(star, ids...)
err = tx.Artist().SetStar(star, ids...)
return err
}
for _, id := range ids {
isAlbum, _ := r.albumRepo.Exists(id)
if isAlbum {
mfs, _ := r.mfRepo.FindByAlbum(id)
if len(mfs) > 0 {
log.Debug(ctx, "Set Star", "value", star, "album", mfs[0].Album)
if err := r.itunes.SetAlbumLoved(mfs[0].ID, star); err != nil {
return err
}
}
continue
}
mf, err := r.mfRepo.Get(id)
if err != nil {
return err
}
if mf != nil {
log.Debug(ctx, "Set Star", "value", star, "song", mf.Title)
if err := r.itunes.SetTrackLoved(mf.ID, star); err != nil {
return err
}
continue
}
return model.ErrNotFound
}
return nil
})
}
+10 -69
View File
@@ -6,9 +6,6 @@ import (
"fmt"
"time"
"github.com/cloudsonic/sonic-server/conf"
"github.com/cloudsonic/sonic-server/itunesbridge"
"github.com/cloudsonic/sonic-server/log"
"github.com/cloudsonic/sonic-server/model"
)
@@ -22,87 +19,31 @@ type Scrobbler interface {
NowPlaying(ctx context.Context, playerId int, playerName, trackId, username string) (*model.MediaFile, error)
}
func NewScrobbler(itunes itunesbridge.ItunesControl, mr model.MediaFileRepository, alr model.AlbumRepository, npr NowPlayingRepository) Scrobbler {
return &scrobbler{itunes: itunes, mfRepo: mr, alRepo: alr, npRepo: npr}
func NewScrobbler(ds model.DataStore, npr NowPlayingRepository) Scrobbler {
return &scrobbler{ds: ds, npRepo: npr}
}
type scrobbler struct {
itunes itunesbridge.ItunesControl
mfRepo model.MediaFileRepository
alRepo model.AlbumRepository
ds model.DataStore
npRepo NowPlayingRepository
}
func (s *scrobbler) detectSkipped(ctx context.Context, playerId int, trackId string) {
size, _ := s.npRepo.Count(playerId)
switch size {
case 0:
return
case 1:
np, _ := s.npRepo.Tail(playerId)
if np.TrackID != trackId {
return
}
s.npRepo.Dequeue(playerId)
default:
prev, _ := s.npRepo.Dequeue(playerId)
for {
if prev.TrackID == trackId {
break
}
np, err := s.npRepo.Dequeue(playerId)
if np == nil || err != nil {
break
}
diff := np.Start.Sub(prev.Start)
if diff < minSkipped || diff > maxSkipped {
log.Debug(ctx, fmt.Sprintf("-- Playtime for track %s was %v. Not skipping.", prev.TrackID, diff))
prev = np
continue
}
err = s.itunes.MarkAsSkipped(prev.TrackID, prev.Start.Add(1*time.Minute))
if err != nil {
log.Warn(ctx, "Error skipping track", "id", prev.TrackID)
} else {
log.Debug(ctx, "-- Skipped track "+prev.TrackID)
}
}
}
}
func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string, playTime time.Time) (*model.MediaFile, error) {
s.detectSkipped(ctx, playerId, trackId)
if conf.Sonic.DevUseFileScanner {
mf, err := s.mfRepo.Get(trackId)
if err != nil {
return nil, err
}
err = s.mfRepo.MarkAsPlayed(trackId, playTime)
if err != nil {
return nil, err
}
err = s.alRepo.MarkAsPlayed(mf.AlbumID, playTime)
return mf, err
}
mf, err := s.mfRepo.Get(trackId)
// TODO Add transaction
mf, err := s.ds.MediaFile().Get(trackId)
if err != nil {
return nil, err
}
if mf == nil {
return nil, errors.New(fmt.Sprintf(`ID "%s" not found`, trackId))
}
if err := s.itunes.MarkAsPlayed(trackId, playTime); err != nil {
err = s.ds.MediaFile().MarkAsPlayed(trackId, playTime)
if err != nil {
return nil, err
}
return mf, nil
err = s.ds.Album().MarkAsPlayed(mf.AlbumID, playTime)
return mf, err
}
func (s *scrobbler) NowPlaying(ctx context.Context, playerId int, playerName, trackId, username string) (*model.MediaFile, error) {
mf, err := s.mfRepo.Get(trackId)
mf, err := s.ds.MediaFile().Get(trackId)
if err != nil {
return nil, err
}
-201
View File
@@ -1,201 +0,0 @@
package engine_test
import (
"errors"
"testing"
"time"
"github.com/cloudsonic/sonic-server/engine"
"github.com/cloudsonic/sonic-server/itunesbridge"
"github.com/cloudsonic/sonic-server/persistence"
. "github.com/cloudsonic/sonic-server/tests"
. "github.com/smartystreets/goconvey/convey"
)
func TestScrobbler(t *testing.T) {
Init(t, false)
mfRepo := persistence.CreateMockMediaFileRepo()
alRepo := persistence.CreateMockAlbumRepo()
npRepo := engine.CreateMockNowPlayingRepo()
itCtrl := &mockItunesControl{}
scrobbler := engine.NewScrobbler(itCtrl, mfRepo, alRepo, npRepo)
Convey("Given a DB with one song", t, func() {
mfRepo.SetData(`[{"ID":"2","Title":"Hands Of Time"}]`, 1)
Convey("When I scrobble an existing song", func() {
now := time.Now()
mf, err := scrobbler.Register(nil, 1, "2", now)
Convey("Then I get the scrobbled song back", func() {
So(err, ShouldBeNil)
So(mf.Title, ShouldEqual, "Hands Of Time")
})
Convey("And iTunes is notified", func() {
So(itCtrl.played, ShouldContainKey, "2")
So(itCtrl.played["2"].Equal(now), ShouldBeTrue)
})
})
Convey("When the ID is not in the DB", func() {
_, err := scrobbler.Register(nil, 1, "3", time.Now())
Convey("Then I receive an error", func() {
So(err, ShouldNotBeNil)
})
Convey("And iTunes is not notified", func() {
So(itCtrl.played, ShouldNotContainKey, "3")
})
})
Convey("When I inform the song that is now playing", func() {
mf, err := scrobbler.NowPlaying(nil, 1, "DSub", "2", "deluan")
Convey("Then I get the song for that id back", func() {
So(err, ShouldBeNil)
So(mf.Title, ShouldEqual, "Hands Of Time")
})
Convey("And it saves the song as the one current playing", func() {
info, _ := npRepo.Head(1)
So(info.TrackID, ShouldEqual, "2")
// Commenting out time sensitive test, due to flakiness
// So(info.Start, ShouldHappenBefore, time.Now())
So(info.Username, ShouldEqual, "deluan")
So(info.PlayerName, ShouldEqual, "DSub")
})
Convey("And iTunes is not notified", func() {
So(itCtrl.played, ShouldNotContainKey, "2")
})
})
Reset(func() {
itCtrl.played = make(map[string]time.Time)
itCtrl.skipped = make(map[string]time.Time)
})
})
}
var aPointInTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
func TestSkipping(t *testing.T) {
Init(t, false)
mfRepo := persistence.CreateMockMediaFileRepo()
alRepo := persistence.CreateMockAlbumRepo()
npRepo := engine.CreateMockNowPlayingRepo()
itCtrl := &mockItunesControl{}
scrobbler := engine.NewScrobbler(itCtrl, mfRepo, alRepo, npRepo)
Convey("Given a DB with three songs", t, func() {
mfRepo.SetData(`[{"ID":"1","Title":"Femme Fatale"},{"ID":"2","Title":"Here She Comes Now"},{"ID":"3","Title":"Lady Godiva's Operation"}]`, 3)
itCtrl.skipped = make(map[string]time.Time)
npRepo.ClearAll()
Convey("When I skip 2 songs", func() {
npRepo.OverrideNow(aPointInTime)
scrobbler.NowPlaying(nil, 1, "DSub", "1", "deluan")
npRepo.OverrideNow(aPointInTime.Add(2 * time.Second))
scrobbler.NowPlaying(nil, 1, "DSub", "3", "deluan")
npRepo.OverrideNow(aPointInTime.Add(3 * time.Second))
scrobbler.NowPlaying(nil, 1, "DSub", "2", "deluan")
Convey("Then the NowPlaying song should be the last one", func() {
np, err := npRepo.GetAll()
So(err, ShouldBeNil)
So(np, ShouldHaveLength, 1)
So(np[0].TrackID, ShouldEqual, "2")
})
})
Convey("When I play one song", func() {
npRepo.OverrideNow(aPointInTime)
scrobbler.NowPlaying(nil, 1, "DSub", "1", "deluan")
Convey("And I skip it before 20 seconds", func() {
npRepo.OverrideNow(aPointInTime.Add(7 * time.Second))
scrobbler.NowPlaying(nil, 1, "DSub", "2", "deluan")
Convey("Then the first song should be marked as skipped", func() {
mf, err := scrobbler.Register(nil, 1, "2", aPointInTime.Add(3*time.Minute))
So(mf.ID, ShouldEqual, "2")
So(itCtrl.skipped, ShouldContainKey, "1")
So(err, ShouldBeNil)
})
})
Convey("And I skip it before 3 seconds", func() {
npRepo.OverrideNow(aPointInTime.Add(2 * time.Second))
scrobbler.NowPlaying(nil, 1, "DSub", "2", "deluan")
Convey("Then the first song should be marked as skipped", func() {
mf, err := scrobbler.Register(nil, 1, "2", aPointInTime.Add(3*time.Minute))
So(mf.ID, ShouldEqual, "2")
So(itCtrl.skipped, ShouldBeEmpty)
So(err, ShouldBeNil)
})
})
Convey("And I skip it after 20 seconds", func() {
npRepo.OverrideNow(aPointInTime.Add(30 * time.Second))
scrobbler.NowPlaying(nil, 1, "DSub", "2", "deluan")
Convey("Then the first song should be marked as skipped", func() {
mf, err := scrobbler.Register(nil, 1, "2", aPointInTime.Add(3*time.Minute))
So(mf.ID, ShouldEqual, "2")
So(itCtrl.skipped, ShouldBeEmpty)
So(err, ShouldBeNil)
})
})
Convey("And I scrobble it before starting to play the other song", func() {
mf, err := scrobbler.Register(nil, 1, "1", time.Now())
Convey("Then the first song should NOT marked as skipped", func() {
So(mf.ID, ShouldEqual, "1")
So(itCtrl.skipped, ShouldBeEmpty)
So(err, ShouldBeNil)
})
})
})
Convey("When the NowPlaying for the next song happens before the Scrobble", func() {
npRepo.OverrideNow(aPointInTime)
scrobbler.NowPlaying(nil, 1, "DSub", "1", "deluan")
npRepo.OverrideNow(aPointInTime.Add(10 * time.Second))
scrobbler.NowPlaying(nil, 1, "DSub", "2", "deluan")
scrobbler.Register(nil, 1, "1", aPointInTime.Add(10*time.Minute))
Convey("Then the NowPlaying song should be the last one", func() {
np, _ := npRepo.GetAll()
So(np, ShouldHaveLength, 1)
So(np[0].TrackID, ShouldEqual, "2")
})
})
})
}
type mockItunesControl struct {
itunesbridge.ItunesControl
played map[string]time.Time
skipped map[string]time.Time
error bool
}
func (m *mockItunesControl) MarkAsPlayed(id string, playDate time.Time) error {
if m.error {
return errors.New("ID not found")
}
if m.played == nil {
m.played = make(map[string]time.Time)
}
m.played[id] = playDate
return nil
}
func (m *mockItunesControl) MarkAsSkipped(id string, skipDate time.Time) error {
if m.error {
return errors.New("ID not found")
}
if m.skipped == nil {
m.skipped = make(map[string]time.Time)
}
m.skipped[id] = skipDate
return nil
}
+6 -8
View File
@@ -15,19 +15,17 @@ type Search interface {
}
type search struct {
artistRepo model.ArtistRepository
albumRepo model.AlbumRepository
mfileRepo model.MediaFileRepository
ds model.DataStore
}
func NewSearch(ar model.ArtistRepository, alr model.AlbumRepository, mr model.MediaFileRepository) Search {
s := &search{artistRepo: ar, albumRepo: alr, mfileRepo: mr}
func NewSearch(ds model.DataStore) Search {
s := &search{ds}
return s
}
func (s *search) SearchArtist(ctx context.Context, q string, offset int, size int) (Entries, error) {
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
resp, err := s.artistRepo.Search(q, offset, size)
resp, err := s.ds.Artist().Search(q, offset, size)
if err != nil {
return nil, nil
}
@@ -40,7 +38,7 @@ func (s *search) SearchArtist(ctx context.Context, q string, offset int, size in
func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int) (Entries, error) {
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
resp, err := s.albumRepo.Search(q, offset, size)
resp, err := s.ds.Album().Search(q, offset, size)
if err != nil {
return nil, nil
}
@@ -53,7 +51,7 @@ func (s *search) SearchAlbum(ctx context.Context, q string, offset int, size int
func (s *search) SearchSong(ctx context.Context, q string, offset int, size int) (Entries, error) {
q = sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*")))
resp, err := s.mfileRepo.Search(q, offset, size)
resp, err := s.ds.MediaFile().Search(q, offset, size)
if err != nil {
return nil, nil
}