Better implementation of Bookmarks, using its own table

This commit is contained in:
Deluan
2020-08-01 12:17:06 -04:00
parent 23d69d26e0
commit ed726c2126
17 changed files with 362 additions and 186 deletions
+3 -1
View File
@@ -23,7 +23,9 @@ func toSqlArgs(rec interface{}) (map[string]interface{}, error) {
err = json.Unmarshal(b, &m)
r := make(map[string]interface{}, len(m))
for f, v := range m {
if !utils.StringInSlice(f, model.AnnotationFields) && v != nil {
isAnnotationField := utils.StringInSlice(f, model.AnnotationFields)
isBookmarkField := utils.StringInSlice(f, model.BookmarkFields)
if !isAnnotationField && !isBookmarkField && v != nil {
r[toSnakeCase(f)] = v
}
}
+2 -1
View File
@@ -52,7 +52,8 @@ func (r mediaFileRepository) Put(m *model.MediaFile) error {
}
func (r mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
return r.newSelectWithAnnotation("media_file.id", options...).Columns("media_file.*")
sql := r.newSelectWithAnnotation("media_file.id", options...).Columns("media_file.*")
return r.withBookmark(sql, "media_file.id")
}
func (r mediaFileRepository) Get(id string) (*model.MediaFile, error) {
+5
View File
@@ -137,6 +137,11 @@ func (s *SQLStore) GC(ctx context.Context) error {
log.Error(ctx, "Error removing orphan artist annotations", err)
return err
}
err = s.MediaFile(ctx).(*mediaFileRepository).cleanBookmarks()
if err != nil {
log.Error(ctx, "Error removing orphan bookmarks", err)
return err
}
err = s.Playlist(ctx).(*playlistRepository).removeOrphans()
if err != nil {
log.Error(ctx, "Error tidying up playlists", err)
+1 -73
View File
@@ -9,7 +9,6 @@ import (
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
)
type playQueueRepository struct {
@@ -27,7 +26,6 @@ func NewPlayQueueRepository(ctx context.Context, o orm.Ormer) model.PlayQueueRep
type playQueue struct {
ID string `orm:"column(id)"`
UserID string `orm:"column(user_id)"`
Comment string
Current string
Position int64
ChangedBy string
@@ -64,79 +62,10 @@ func (r *playQueueRepository) Retrieve(userId string) (*model.PlayQueue, error)
return &pls, err
}
func (r *playQueueRepository) AddBookmark(userId, id, comment string, position int64) error {
u := loggedUser(r.ctx)
client, _ := request.ClientFrom(r.ctx)
bm := &playQueue{
UserID: userId,
Comment: comment,
Current: id,
Position: position,
ChangedBy: client,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
sel := r.newSelect().Column("*").Where(And{
Eq{"user_id": userId},
Eq{"items": ""},
Eq{"current": id},
})
var prev model.PlayQueue
err := r.queryOne(sel, &prev)
if err != nil && err != model.ErrNotFound {
log.Error(r.ctx, "Error retrieving previous bookmark", "user", u.UserName, err, "mediaFileId", id, err)
return err
}
// If there is a previous bookmark, override
if prev.ID != "" {
bm.ID = prev.ID
bm.CreatedAt = prev.CreatedAt
}
_, err = r.put(bm.ID, bm)
if err != nil {
log.Error(r.ctx, "Error saving bookmark", "user", u.UserName, err, "mediaFileId", id, err)
return err
}
return nil
}
func (r *playQueueRepository) GetBookmarks(userId string) (model.Bookmarks, error) {
u := loggedUser(r.ctx)
sel := r.newSelect().Column("*").Where(And{Eq{"user_id": userId}, Eq{"items": ""}})
var pqs model.PlayQueues
err := r.queryAll(sel, &pqs)
if err != nil {
log.Error(r.ctx, "Error retrieving bookmarks", "user", u.UserName, err)
return nil, err
}
bms := make(model.Bookmarks, len(pqs))
for i := range pqs {
items := r.loadTracks(model.MediaFiles{{ID: pqs[i].Current}})
bms[i].Item = items[0]
bms[i].Comment = pqs[i].Comment
bms[i].Position = pqs[i].Position
bms[i].CreatedAt = pqs[i].CreatedAt
bms[i].UpdatedAt = pqs[i].UpdatedAt
}
return bms, nil
}
func (r *playQueueRepository) DeleteBookmark(userId, id string) error {
return r.delete(And{
Eq{"user_id": userId},
Eq{"items": ""},
Eq{"current": id},
})
}
func (r *playQueueRepository) fromModel(q *model.PlayQueue) playQueue {
pq := playQueue{
ID: q.ID,
UserID: q.UserID,
Comment: q.Comment,
Current: q.Current,
Position: q.Position,
ChangedBy: q.ChangedBy,
@@ -155,7 +84,6 @@ func (r *playQueueRepository) toModel(pq *playQueue) model.PlayQueue {
q := model.PlayQueue{
ID: pq.ID,
UserID: pq.UserID,
Comment: pq.Comment,
Current: pq.Current,
Position: pq.Position,
ChangedBy: pq.ChangedBy,
@@ -219,7 +147,7 @@ func (r *playQueueRepository) loadTracks(tracks model.MediaFiles) model.MediaFil
}
func (r *playQueueRepository) clearPlayQueue(userId string) error {
return r.delete(And{Eq{"user_id": userId}, NotEq{"items": ""}})
return r.delete(Eq{"user_id": userId})
}
var _ model.PlayQueueRepository = (*playQueueRepository)(nil)
-51
View File
@@ -52,55 +52,6 @@ var _ = Describe("PlayQueueRepository", func() {
Expect(countPlayQueues(repo, "user1")).To(Equal(1))
})
})
Describe("Bookmarks", func() {
It("returns an empty collection if there are no bookmarks", func() {
Expect(repo.GetBookmarks("user999")).To(BeEmpty())
})
It("saves and overrides bookmarks", func() {
By("Saving the bookmark")
Expect(repo.AddBookmark("user5", songAntenna.ID, "this is a comment", 123)).To(BeNil())
bms, err := repo.GetBookmarks("user5")
Expect(err).To(BeNil())
Expect(bms).To(HaveLen(1))
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
Expect(bms[0].Item.Title).To(Equal(songAntenna.Title))
Expect(bms[0].Comment).To(Equal("this is a comment"))
Expect(bms[0].Position).To(Equal(int64(123)))
created := bms[0].CreatedAt
updated := bms[0].UpdatedAt
By("Overriding the bookmark")
Expect(repo.AddBookmark("user5", songAntenna.ID, "another comment", 333)).To(BeNil())
bms, err = repo.GetBookmarks("user5")
Expect(err).To(BeNil())
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
Expect(bms[0].Comment).To(Equal("another comment"))
Expect(bms[0].Position).To(Equal(int64(333)))
Expect(bms[0].CreatedAt).To(Equal(created))
Expect(bms[0].UpdatedAt).To(BeTemporally(">", updated))
By("Saving another bookmark")
Expect(repo.AddBookmark("user5", songComeTogether.ID, "one more comment", 444)).To(BeNil())
bms, err = repo.GetBookmarks("user5")
Expect(err).To(BeNil())
Expect(bms).To(HaveLen(2))
By("Delete bookmark")
Expect(repo.DeleteBookmark("user5", songAntenna.ID))
bms, err = repo.GetBookmarks("user5")
Expect(err).To(BeNil())
Expect(bms).To(HaveLen(1))
Expect(bms[0].Item.ID).To(Equal(songComeTogether.ID))
Expect(bms[0].Item.Title).To(Equal(songComeTogether.Title))
})
})
})
func countPlayQueues(repo model.PlayQueueRepository, userId string) int {
@@ -115,7 +66,6 @@ func countPlayQueues(repo model.PlayQueueRepository, userId string) int {
func AssertPlayQueue(expected, actual *model.PlayQueue) {
Expect(actual.ID).To(Equal(expected.ID))
Expect(actual.UserID).To(Equal(expected.UserID))
Expect(actual.Comment).To(Equal(expected.Comment))
Expect(actual.Current).To(Equal(expected.Current))
Expect(actual.Position).To(Equal(expected.Position))
Expect(actual.ChangedBy).To(Equal(expected.ChangedBy))
@@ -132,7 +82,6 @@ func aPlayQueue(userId, current string, position int64, items ...model.MediaFile
return &model.PlayQueue{
ID: id.String(),
UserID: userId,
Comment: "no_comments",
Current: current,
Position: position,
ChangedBy: "test",
+3 -3
View File
@@ -23,9 +23,9 @@ func (r sqlRepository) newSelectWithAnnotation(idField string, options ...model.
func (r sqlRepository) annId(itemID ...string) And {
return And{
Eq{"user_id": userId(r.ctx)},
Eq{"item_type": r.tableName},
Eq{"item_id": itemID},
Eq{annotationTable + ".user_id": userId(r.ctx)},
Eq{annotationTable + ".item_type": r.tableName},
Eq{annotationTable + ".item_id": itemID},
}
}
+1 -1
View File
@@ -112,7 +112,7 @@ func (r sqlRepository) executeSQL(sq Sqlizer) (int64, error) {
return res.RowsAffected()
}
// Note: Due to a bug in the QueryRow, this method does not map any embedded structs (ex: annotations)
// Note: Due to a bug in the QueryRow method, this function does not map any embedded structs (ex: annotations)
// In this case, use the queryAll method and get the first item of the returned list
func (r sqlRepository) queryOne(sq Sqlizer, response interface{}) error {
query, args, err := sq.ToSql()
+151
View File
@@ -0,0 +1,151 @@
package persistence
import (
"time"
. "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
)
const bookmarkTable = "bookmark"
func (r sqlRepository) withBookmark(sql SelectBuilder, idField string) SelectBuilder {
return sql.
LeftJoin("bookmark on (" +
"bookmark.item_id = " + idField +
" AND bookmark.item_type = '" + r.tableName + "'" +
" AND bookmark.user_id = '" + userId(r.ctx) + "')").
Columns("position as bookmark_position")
}
func (r sqlRepository) bmkID(itemID ...string) And {
return And{
Eq{bookmarkTable + ".user_id": userId(r.ctx)},
Eq{bookmarkTable + ".item_type": r.tableName},
Eq{bookmarkTable + ".item_id": itemID},
}
}
func (r sqlRepository) bmkUpsert(itemID, comment string, position int64) error {
client, _ := request.ClientFrom(r.ctx)
user, _ := request.UserFrom(r.ctx)
values := map[string]interface{}{
"comment": comment,
"position": position,
"updated_at": time.Now(),
"changed_by": client,
}
upd := Update(bookmarkTable).Where(r.bmkID(itemID)).SetMap(values)
c, err := r.executeSQL(upd)
if err == nil {
log.Debug(r.ctx, "Updated bookmark", "id", itemID, "user", user.UserName, "position", position, "comment", comment)
}
if c == 0 || err == orm.ErrNoRows {
values["user_id"] = user.ID
values["item_type"] = r.tableName
values["item_id"] = itemID
values["created_at"] = time.Now()
values["updated_at"] = time.Now()
ins := Insert(bookmarkTable).SetMap(values)
_, err = r.executeSQL(ins)
if err != nil {
return err
}
log.Debug(r.ctx, "Added bookmark", "id", itemID, "user", user.UserName, "position", position, "comment", comment)
}
return err
}
func (r sqlRepository) AddBookmark(id, comment string, position int64) error {
user, _ := request.UserFrom(r.ctx)
err := r.bmkUpsert(id, comment, position)
if err != nil {
log.Error(r.ctx, "Error adding bookmark", "id", id, "user", user.UserName, "position", position, "comment", comment)
}
return err
}
func (r sqlRepository) DeleteBookmark(id string) error {
user, _ := request.UserFrom(r.ctx)
del := Delete(bookmarkTable).Where(r.bmkID(id))
_, err := r.executeSQL(del)
if err != nil {
log.Error(r.ctx, "Error removing bookmark", "id", id, "user", user.UserName)
}
return err
}
type bookmark struct {
UserID string `json:"user_id" orm:"column(user_id)"`
ItemID string `json:"item_id" orm:"column(item_id)"`
ItemType string `json:"item_type"`
Comment string `json:"comment"`
Position int64 `json:"position"`
ChangedBy string `json:"changed_by"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (r sqlRepository) GetBookmarks() (model.Bookmarks, error) {
user, _ := request.UserFrom(r.ctx)
idField := r.tableName + ".id"
sql := r.newSelectWithAnnotation(idField).Columns("*")
sql = r.withBookmark(sql, idField).Where(NotEq{bookmarkTable + ".item_id": nil})
var mfs model.MediaFiles
err := r.queryAll(sql, &mfs)
if err != nil {
log.Error(r.ctx, "Error getting mediafiles with bookmarks", "user", user.UserName, err)
return nil, err
}
ids := make([]string, len(mfs))
mfMap := make(map[string]int)
for i, mf := range mfs {
ids[i] = mf.ID
mfMap[mf.ID] = i
}
sql = Select("*").From(bookmarkTable).Where(r.bmkID(ids...))
var bmks []bookmark
err = r.queryAll(sql, &bmks)
if err != nil {
log.Error(r.ctx, "Error getting bookmarks", "user", user.UserName, "ids", ids, err)
return nil, err
}
resp := make(model.Bookmarks, len(bmks))
for i, bmk := range bmks {
if itemIdx, ok := mfMap[bmk.ItemID]; !ok {
log.Debug(r.ctx, "Invalid bookmark", "id", bmk.ItemID, "user", user.UserName)
continue
} else {
resp[i] = model.Bookmark{
Comment: bmk.Comment,
Position: bmk.Position,
CreatedAt: bmk.CreatedAt,
UpdatedAt: bmk.UpdatedAt,
ChangedBy: bmk.ChangedBy,
Item: mfs[itemIdx],
}
}
}
return resp, nil
}
func (r sqlRepository) cleanBookmarks() error {
del := Delete(bookmarkTable).Where(Eq{"item_type": r.tableName}).Where("item_id not in (select id from " + r.tableName + ")")
c, err := r.executeSQL(del)
if err != nil {
return err
}
if c > 0 {
log.Debug(r.ctx, "Clean-up bookmarks", "totalDeleted", c)
}
return nil
}
+72
View File
@@ -0,0 +1,72 @@
package persistence
import (
"context"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("sqlBookmarks", func() {
var mr model.MediaFileRepository
BeforeEach(func() {
ctx := log.NewContext(context.TODO())
ctx = request.WithUser(ctx, model.User{ID: "user1"})
mr = NewMediaFileRepository(ctx, orm.NewOrm())
})
Describe("Bookmarks", func() {
It("returns an empty collection if there are no bookmarks", func() {
Expect(mr.GetBookmarks()).To(BeEmpty())
})
It("saves and overrides bookmarks", func() {
By("Saving the bookmark")
Expect(mr.AddBookmark(songAntenna.ID, "this is a comment", 123)).To(BeNil())
bms, err := mr.GetBookmarks()
Expect(err).To(BeNil())
Expect(bms).To(HaveLen(1))
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
Expect(bms[0].Item.Title).To(Equal(songAntenna.Title))
Expect(bms[0].Comment).To(Equal("this is a comment"))
Expect(bms[0].Position).To(Equal(int64(123)))
created := bms[0].CreatedAt
updated := bms[0].UpdatedAt
Expect(created.IsZero()).To(BeFalse())
Expect(updated).To(BeTemporally(">=", created))
By("Overriding the bookmark")
Expect(mr.AddBookmark(songAntenna.ID, "another comment", 333)).To(BeNil())
bms, err = mr.GetBookmarks()
Expect(err).To(BeNil())
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
Expect(bms[0].Comment).To(Equal("another comment"))
Expect(bms[0].Position).To(Equal(int64(333)))
Expect(bms[0].CreatedAt).To(Equal(created))
Expect(bms[0].UpdatedAt).To(BeTemporally(">=", updated))
By("Saving another bookmark")
Expect(mr.AddBookmark(songComeTogether.ID, "one more comment", 444)).To(BeNil())
bms, err = mr.GetBookmarks()
Expect(err).To(BeNil())
Expect(bms).To(HaveLen(2))
By("Delete bookmark")
Expect(mr.DeleteBookmark(songAntenna.ID))
bms, err = mr.GetBookmarks()
Expect(err).To(BeNil())
Expect(bms).To(HaveLen(1))
Expect(bms[0].Item.ID).To(Equal(songComeTogether.ID))
Expect(bms[0].Item.Title).To(Equal(songComeTogether.Title))
})
})
})