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:
+14
-20
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user