refactor: annotations

This commit is contained in:
Deluan 2020-01-31 21:09:23 -05:00 committed by Deluan Quintão
parent de1fea64bc
commit 88e01d05f6
16 changed files with 184 additions and 197 deletions

View File

@ -26,9 +26,9 @@ func (r ratings) SetRating(ctx context.Context, id string, rating int) error {
return err
}
if exist {
return r.ds.Annotation(ctx).SetRating(rating, model.AlbumItemType, id)
return r.ds.Album(ctx).SetRating(rating, id)
}
return r.ds.Annotation(ctx).SetRating(rating, model.MediaItemType, id)
return r.ds.MediaFile(ctx).SetRating(rating, id)
}
func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
@ -44,7 +44,7 @@ func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
return err
}
if exist {
err = tx.Annotation(ctx).SetStar(star, model.AlbumItemType, ids...)
err = tx.Album(ctx).SetStar(star, ids...)
if err != nil {
return err
}
@ -55,13 +55,13 @@ func (r ratings) SetStar(ctx context.Context, star bool, ids ...string) error {
return err
}
if exist {
err = tx.Annotation(ctx).SetStar(star, model.ArtistItemType, ids...)
err = tx.Artist(ctx).SetStar(star, ids...)
if err != nil {
return err
}
continue
}
err = tx.Annotation(ctx).SetStar(star, model.MediaItemType, ids...)
err = tx.MediaFile(ctx).SetStar(star, ids...)
if err != nil {
return err
}

View File

@ -31,11 +31,15 @@ func (s *scrobbler) Register(ctx context.Context, playerId int, trackId string,
if err != nil {
return err
}
err = s.ds.Annotation(ctx).IncPlayCount(model.MediaItemType, trackId, playTime)
err = s.ds.MediaFile(ctx).IncPlayCount(trackId, playTime)
if err != nil {
return err
}
err = s.ds.Annotation(ctx).IncPlayCount(model.AlbumItemType, mf.AlbumID, playTime)
err = s.ds.Album(ctx).IncPlayCount(mf.AlbumID, playTime)
if err != nil {
return err
}
err = s.ds.Artist(ctx).IncPlayCount(mf.ArtistID, playTime)
return err
})
return mf, err

View File

@ -40,4 +40,5 @@ type AlbumRepository interface {
Search(q string, offset int, size int) (Albums, error)
Refresh(ids ...string) error
PurgeEmpty() error
AnnotatedRepository
}

View File

@ -2,29 +2,8 @@ package model
import "time"
const (
ArtistItemType = "artist"
AlbumItemType = "album"
MediaItemType = "media_file"
)
type Annotation struct {
AnnID string `json:"annID" orm:"pk;column(ann_id)"`
UserID string `json:"userID" orm:"pk;column(user_id)"`
ItemID string `json:"itemID" orm:"pk;column(item_id)"`
ItemType string `json:"itemType"`
PlayCount int `json:"playCount"`
PlayDate time.Time `json:"playDate"`
Rating int `json:"rating"`
Starred bool `json:"starred"`
StarredAt time.Time `json:"starredAt"`
}
type AnnotationMap map[string]Annotation
type AnnotationRepository interface {
Delete(itemType string, itemID ...string) error
IncPlayCount(itemType, itemID string, ts time.Time) error
SetStar(starred bool, itemType string, ids ...string) error
SetRating(rating int, itemType, itemID string) error
type AnnotatedRepository interface {
IncPlayCount(itemID string, ts time.Time) error
SetStar(starred bool, itemIDs ...string) error
SetRating(rating int, itemID string) error
}

View File

@ -33,4 +33,5 @@ type ArtistRepository interface {
Refresh(ids ...string) error
GetIndex() (ArtistIndexes, error)
PurgeEmpty() error
AnnotatedRepository
}

View File

@ -28,7 +28,6 @@ type DataStore interface {
Playlist(ctx context.Context) PlaylistRepository
Property(ctx context.Context) PropertyRepository
User(ctx context.Context) UserRepository
Annotation(ctx context.Context) AnnotationRepository
Resource(ctx context.Context, model interface{}) ResourceRepository

View File

@ -28,11 +28,11 @@ type MediaFile struct {
UpdatedAt time.Time `json:"updatedAt"`
// Annotations
PlayCount int `json:"-" orm:"-"`
PlayDate time.Time `json:"-" orm:"-"`
Rating int `json:"-" orm:"-"`
Starred bool `json:"-" orm:"-"`
StarredAt time.Time `json:"-" orm:"-"`
PlayCount int `json:"-" orm:"-"`
PlayDate time.Time `json:"-" orm:"-"`
Rating int `json:"-" orm:"-"`
Starred bool `json:"-" orm:"-"`
StarredAt time.Time `json:"-" orm:"-"`
}
func (mf *MediaFile) ContentType() string {
@ -53,4 +53,6 @@ type MediaFileRepository interface {
Search(q string, offset int, size int) (MediaFiles, error)
Delete(id string) error
DeleteByPath(path string) error
AnnotatedRepository
}

View File

@ -42,7 +42,7 @@ func (r *albumRepository) Put(a *model.Album) error {
}
func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder {
return r.newSelectWithAnnotation(model.AlbumItemType, "id", options...).Columns("*")
return r.newSelectWithAnnotation("id", options...).Columns("*")
}
func (r *albumRepository) Get(id string) (*model.Album, error) {

97
persistence/annotation.go Normal file
View File

@ -0,0 +1,97 @@
package persistence
import (
"time"
. "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/model"
"github.com/google/uuid"
)
type annotation struct {
AnnID string `json:"annID" orm:"pk;column(ann_id)"`
UserID string `json:"userID" orm:"pk;column(user_id)"`
ItemID string `json:"itemID" orm:"pk;column(item_id)"`
ItemType string `json:"itemType"`
PlayCount int `json:"playCount"`
PlayDate time.Time `json:"playDate"`
Rating int `json:"rating"`
Starred bool `json:"starred"`
StarredAt time.Time `json:"starredAt"`
}
const annotationTable = "annotation"
func (r sqlRepository) newSelectWithAnnotation(idField string, options ...model.QueryOptions) SelectBuilder {
return r.newSelect(options...).
LeftJoin("annotation on ("+
"annotation.item_id = "+idField+
" AND annotation.item_type = '"+r.tableName+"'"+
" AND annotation.user_id = '"+userId(r.ctx)+"')").
Columns("starred", "starred_at", "play_count", "play_date", "rating")
}
func (r sqlRepository) annUpsert(values map[string]interface{}, itemIDs ...string) error {
upd := Update(annotationTable).Where(r.annId(itemIDs...))
for f, v := range values {
upd = upd.Set(f, v)
}
c, err := r.executeSQL(upd)
if c == 0 || err == orm.ErrNoRows {
for _, itemID := range itemIDs {
id, _ := uuid.NewRandom()
values["ann_id"] = id.String()
values["user_id"] = userId(r.ctx)
values["item_type"] = r.tableName
values["item_id"] = itemID
ins := Insert(annotationTable).SetMap(values)
_, err = r.executeSQL(ins)
if err != nil {
return err
}
}
}
return err
}
func (r sqlRepository) annId(itemID ...string) And {
return And{
Eq{"user_id": userId(r.ctx)},
Eq{"item_type": r.tableName},
Eq{"item_id": itemID},
}
}
func (r sqlRepository) IncPlayCount(itemID string, ts time.Time) error {
upd := Update(annotationTable).Where(r.annId(itemID)).
Set("play_count", Expr("play_count+1")).
Set("play_date", ts)
c, err := r.executeSQL(upd)
if c == 0 || err == orm.ErrNoRows {
id, _ := uuid.NewRandom()
values := map[string]interface{}{}
values["ann_id"] = id.String()
values["user_id"] = userId(r.ctx)
values["item_type"] = r.tableName
values["item_id"] = itemID
values["play_count"] = 1
values["play_date"] = ts
ins := Insert(annotationTable).SetMap(values)
_, err = r.executeSQL(ins)
if err != nil {
return err
}
}
return err
}
func (r sqlRepository) SetStar(starred bool, ids ...string) error {
starredAt := time.Now()
return r.annUpsert(map[string]interface{}{"starred": starred, "starred_at": starredAt}, ids...)
}
func (r sqlRepository) SetRating(rating int, itemID string) error {
return r.annUpsert(map[string]interface{}{"rating": rating}, itemID)
}

View File

@ -1,91 +0,0 @@
package persistence
import (
"context"
"time"
. "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/model"
"github.com/google/uuid"
)
type annotationRepository struct {
sqlRepository
}
func NewAnnotationRepository(ctx context.Context, o orm.Ormer) model.AnnotationRepository {
r := &annotationRepository{}
r.ctx = ctx
r.ormer = o
r.tableName = "annotation"
return r
}
func (r *annotationRepository) upsert(values map[string]interface{}, itemType string, itemIDs ...string) error {
upd := Update(r.tableName).Where(r.getId(itemType, itemIDs...))
for f, v := range values {
upd = upd.Set(f, v)
}
c, err := r.executeSQL(upd)
if c == 0 || err == orm.ErrNoRows {
for _, itemID := range itemIDs {
id, _ := uuid.NewRandom()
values["ann_id"] = id.String()
values["user_id"] = userId(r.ctx)
values["item_type"] = itemType
values["item_id"] = itemID
ins := Insert(r.tableName).SetMap(values)
_, err = r.executeSQL(ins)
if err != nil {
return err
}
}
}
return err
}
func (r *annotationRepository) IncPlayCount(itemType, itemID string, ts time.Time) error {
upd := Update(r.tableName).Where(r.getId(itemType, itemID)).
Set("play_count", Expr("play_count+1")).
Set("play_date", ts)
c, err := r.executeSQL(upd)
if c == 0 || err == orm.ErrNoRows {
id, _ := uuid.NewRandom()
values := map[string]interface{}{}
values["ann_id"] = id.String()
values["user_id"] = userId(r.ctx)
values["item_type"] = itemType
values["item_id"] = itemID
values["play_count"] = 1
values["play_date"] = ts
ins := Insert(r.tableName).SetMap(values)
_, err = r.executeSQL(ins)
if err != nil {
return err
}
}
return err
}
func (r *annotationRepository) getId(itemType string, itemID ...string) And {
return And{
Eq{"user_id": userId(r.ctx)},
Eq{"item_type": itemType},
Eq{"item_id": itemID},
}
}
func (r *annotationRepository) SetStar(starred bool, itemType string, ids ...string) error {
starredAt := time.Now()
return r.upsert(map[string]interface{}{"starred": starred, "starred_at": starredAt}, itemType, ids...)
}
func (r *annotationRepository) SetRating(rating int, itemType, itemID string) error {
return r.upsert(map[string]interface{}{"rating": rating}, itemType, itemID)
}
func (r *annotationRepository) Delete(itemType string, itemIDs ...string) error {
return r.delete(r.getId(itemType, itemIDs...))
}

View File

@ -30,7 +30,7 @@ func NewArtistRepository(ctx context.Context, o orm.Ormer) model.ArtistRepositor
}
func (r *artistRepository) selectArtist(options ...model.QueryOptions) SelectBuilder {
return r.newSelectWithAnnotation(model.ArtistItemType, "id", options...).Columns("*")
return r.newSelectWithAnnotation("id", options...).Columns("*")
}
func (r *artistRepository) CountAll(options ...model.QueryOptions) (int64, error) {

View File

@ -41,7 +41,7 @@ func (r mediaFileRepository) Put(m *model.MediaFile) error {
}
func (r mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
return r.newSelectWithAnnotation(model.MediaItemType, "media_file.id", options...).Columns("media_file.*")
return r.newSelectWithAnnotation("media_file.id", options...).Columns("media_file.*")
}
func (r mediaFileRepository) Get(id string) (*model.MediaFile, error) {

View File

@ -61,10 +61,6 @@ func (db *MockDataStore) User(context.Context) model.UserRepository {
return db.MockedUser
}
func (db *MockDataStore) Annotation(context.Context) model.AnnotationRepository {
return struct{ model.AnnotationRepository }{}
}
func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error {
return block(db)
}

View File

@ -69,10 +69,6 @@ func (db *NewSQLStore) User(ctx context.Context) model.UserRepository {
return NewUserRepository(ctx, db.getOrmer())
}
func (db *NewSQLStore) Annotation(ctx context.Context) model.AnnotationRepository {
return NewAnnotationRepository(ctx, db.getOrmer())
}
func (db *NewSQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository {
switch m.(type) {
case model.User:

View File

@ -1,11 +1,11 @@
package persistence
import (
"context"
"os"
"strings"
"testing"
"github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/db"
@ -30,41 +30,38 @@ func TestPersistence(t *testing.T) {
RunSpecs(t, "Persistence Suite")
}
var artistKraftwerk = model.Artist{ID: "2", Name: "Kraftwerk", AlbumCount: 1}
var artistBeatles = model.Artist{ID: "3", Name: "The Beatles", AlbumCount: 2, Starred: true}
var testArtists = model.Artists{
artistKraftwerk,
artistBeatles,
}
var (
artistKraftwerk = model.Artist{ID: "2", Name: "Kraftwerk", AlbumCount: 1}
artistBeatles = model.Artist{ID: "3", Name: "The Beatles", AlbumCount: 2}
testArtists = model.Artists{
artistKraftwerk,
artistBeatles,
}
)
var albumSgtPeppers = model.Album{ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, Year: 1967}
var albumAbbeyRoad = model.Album{ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, Year: 1969}
var albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, Starred: true}
var testAlbums = model.Albums{
albumSgtPeppers,
albumAbbeyRoad,
albumRadioactivity,
}
var (
albumSgtPeppers = model.Album{ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", ArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, Year: 1967}
albumAbbeyRoad = model.Album{ID: "2", Name: "Abbey Road", Artist: "The Beatles", ArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, Year: 1969}
albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", ArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2}
testAlbums = model.Albums{
albumSgtPeppers,
albumAbbeyRoad,
albumRadioactivity,
}
)
var songDayInALife = model.MediaFile{ID: "1", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "1", Album: "Sgt Peppers", Genre: "Rock", Path: P("/beatles/1/sgt/a day.mp3")}
var songComeTogether = model.MediaFile{ID: "2", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "2", Album: "Abbey Road", Genre: "Rock", Path: P("/beatles/1/come together.mp3"), Starred: true}
var songRadioactivity = model.MediaFile{ID: "3", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "3", Album: "Radioactivity", Genre: "Electronic", Path: P("/kraft/radio/radio.mp3")}
var songAntenna = model.MediaFile{ID: "4", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "3", Genre: "Electronic", Path: P("/kraft/radio/antenna.mp3")}
var testSongs = model.MediaFiles{
songDayInALife,
songComeTogether,
songRadioactivity,
songAntenna,
}
var annArtistBeatles = model.Annotation{AnnID: "3", UserID: "userid", ItemType: model.ArtistItemType, ItemID: artistBeatles.ID, Starred: true}
var annAlbumRadioactivity = model.Annotation{AnnID: "1", UserID: "userid", ItemType: model.AlbumItemType, ItemID: albumRadioactivity.ID, Starred: true}
var annSongComeTogether = model.Annotation{AnnID: "2", UserID: "userid", ItemType: model.MediaItemType, ItemID: songComeTogether.ID, Starred: true}
var testAnnotations = []model.Annotation{
annArtistBeatles,
annAlbumRadioactivity,
annSongComeTogether,
}
var (
songDayInALife = model.MediaFile{ID: "1", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "1", Album: "Sgt Peppers", Genre: "Rock", Path: P("/beatles/1/sgt/a day.mp3")}
songComeTogether = model.MediaFile{ID: "2", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "2", Album: "Abbey Road", Genre: "Rock", Path: P("/beatles/1/come together.mp3")}
songRadioactivity = model.MediaFile{ID: "3", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "3", Album: "Radioactivity", Genre: "Electronic", Path: P("/kraft/radio/radio.mp3")}
songAntenna = model.MediaFile{ID: "4", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "3", Genre: "Electronic", Path: P("/kraft/radio/antenna.mp3")}
testSongs = model.MediaFiles{
songDayInALife,
songComeTogether,
songRadioactivity,
songAntenna,
}
)
var (
plsBest = model.Playlist{
@ -85,9 +82,11 @@ func P(path string) string {
}
var _ = Describe("Initialize test DB", func() {
// TODO Load this data setup from file(s)
BeforeSuite(func() {
o := orm.NewOrm()
ctx := log.NewContext(nil)
ctx := context.WithValue(log.NewContext(nil), "user", &model.User{ID: "userid"})
mr := NewMediaFileRepository(ctx, o)
for _, s := range testSongs {
err := mr.Put(&s)
@ -112,19 +111,6 @@ var _ = Describe("Initialize test DB", func() {
}
}
for _, a := range testAnnotations {
values, _ := toSqlArgs(a)
ins := squirrel.Insert("annotation").SetMap(values)
query, args, err := ins.ToSql()
if err != nil {
panic(err)
}
_, err = o.Raw(query, args...).Exec()
if err != nil {
panic(err)
}
}
pr := NewPlaylistRepository(ctx, o)
for _, pls := range testPlaylists {
err := pr.Put(&pls)
@ -132,5 +118,31 @@ var _ = Describe("Initialize test DB", func() {
panic(err)
}
}
// Prepare annotations
if err := arr.SetStar(true, artistBeatles.ID); err != nil {
panic(err)
}
ar, _ := arr.Get(artistBeatles.ID)
artistBeatles.Starred = true
artistBeatles.StarredAt = ar.StarredAt
testArtists[1] = artistBeatles
if err := alr.SetStar(true, albumRadioactivity.ID); err != nil {
panic(err)
}
al, _ := alr.Get(albumRadioactivity.ID)
albumRadioactivity.Starred = true
albumRadioactivity.StarredAt = al.StarredAt
testAlbums[2] = albumRadioactivity
if err := mr.SetStar(true, songComeTogether.ID); err != nil {
panic(err)
}
mf, _ := mr.Get(songComeTogether.ID)
songComeTogether.Starred = true
songComeTogether.StarredAt = mf.StarredAt
testSongs[1] = songComeTogether
})
})

View File

@ -30,15 +30,6 @@ func userId(ctx context.Context) string {
return usr.ID
}
func (r sqlRepository) newSelectWithAnnotation(itemType, idField string, options ...model.QueryOptions) SelectBuilder {
return r.newSelect(options...).
LeftJoin("annotation on ("+
"annotation.item_id = "+idField+
" AND annotation.item_type = '"+itemType+"'"+
" AND annotation.user_id = '"+userId(r.ctx)+"')").
Columns("starred", "starred_at", "play_count", "play_date", "rating")
}
func (r sqlRepository) newSelect(options ...model.QueryOptions) SelectBuilder {
sq := Select().From(r.tableName)
sq = r.applyOptions(sq, options...)