diff --git a/db/migration/20200801101355_create_bookmark_table.go b/db/migration/20200801101355_create_bookmark_table.go
new file mode 100644
index 00000000..3f41f18a
--- /dev/null
+++ b/db/migration/20200801101355_create_bookmark_table.go
@@ -0,0 +1,53 @@
+package migration
+
+import (
+ "database/sql"
+
+ "github.com/pressly/goose"
+)
+
+func init() {
+ goose.AddMigration(upCreateBookmarkTable, downCreateBookmarkTable)
+}
+
+func upCreateBookmarkTable(tx *sql.Tx) error {
+ _, err := tx.Exec(`
+create table bookmark
+(
+ user_id varchar(255) not null
+ references user
+ on update cascade on delete cascade,
+ item_id varchar(255) not null,
+ item_type varchar(255) not null,
+ comment varchar(255),
+ position integer,
+ changed_by varchar(255),
+ created_at datetime,
+ updated_at datetime,
+ constraint bookmark_pk
+ unique (user_id, item_id, item_type)
+);
+
+create table playqueue_dg_tmp
+(
+ id varchar(255) not null,
+ user_id varchar(255) not null
+ references user
+ on update cascade on delete cascade,
+ current varchar(255),
+ position real,
+ changed_by varchar(255),
+ items varchar(255),
+ created_at datetime,
+ updated_at datetime
+);
+drop table playqueue;
+alter table playqueue_dg_tmp rename to playqueue;
+`)
+
+ return err
+}
+
+func downCreateBookmarkTable(tx *sql.Tx) error {
+ return nil
+}
diff --git a/engine/common.go b/engine/common.go
index 47e47f9c..d8bb92dc 100644
--- a/engine/common.go
+++ b/engine/common.go
@@ -9,37 +9,37 @@ import (
)
type Entry struct {
- Id string
- Title string
- IsDir bool
- Parent string
- Album string
- Year int
- Artist string
- Genre string
- CoverArt string
- Starred time.Time
- Track int
- Duration int
- Size int64
- Suffix string
- BitRate int
- ContentType string
- Path string
- PlayCount int32
- DiscNumber int
- Created time.Time
- AlbumId string
- ArtistId string
- Type string
- UserRating int
- SongCount int
-
- UserName string
- MinutesAgo int
- PlayerId int
- PlayerName string
- AlbumCount int
+ Id string
+ Title string
+ IsDir bool
+ Parent string
+ Album string
+ Year int
+ Artist string
+ Genre string
+ CoverArt string
+ Starred time.Time
+ Track int
+ Duration int
+ Size int64
+ Suffix string
+ BitRate int
+ ContentType string
+ Path string
+ PlayCount int32
+ DiscNumber int
+ Created time.Time
+ AlbumId string
+ ArtistId string
+ Type string
+ UserRating int
+ SongCount int
+ UserName string
+ MinutesAgo int
+ PlayerId int
+ PlayerName string
+ AlbumCount int
+ BookmarkPosition int64
}
type Entries []Entry
@@ -112,6 +112,7 @@ func FromMediaFile(mf *model.MediaFile) Entry {
e.Starred = mf.StarredAt
}
e.UserRating = mf.Rating
+ e.BookmarkPosition = mf.BookmarkPosition
return e
}
diff --git a/model/bookmark.go b/model/bookmark.go
new file mode 100644
index 00000000..5ae223e1
--- /dev/null
+++ b/model/bookmark.go
@@ -0,0 +1,28 @@
+package model
+
+import "time"
+
+type Bookmarkable struct {
+ BookmarkPosition int64 `json:"bookmarkPosition"`
+}
+
+type BookmarkableRepository interface {
+ AddBookmark(id, comment string, position int64) error
+ DeleteBookmark(id string) error
+ GetBookmarks() (Bookmarks, error)
+}
+
+type Bookmark struct {
+ Item MediaFile `json:"item"`
+ Comment string `json:"comment"`
+ Position int64 `json:"position"`
+ ChangedBy string `json:"changed_by"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+type Bookmarks []Bookmark
+
+// While I can't find a better way to make these fields optional in the models, I keep this list here
+// to be used in other packages
+var BookmarkFields = []string{"bookmarkPosition"}
diff --git a/model/mediafile.go b/model/mediafile.go
index 2cd34b69..6c7c508b 100644
--- a/model/mediafile.go
+++ b/model/mediafile.go
@@ -7,6 +7,7 @@ import (
type MediaFile struct {
Annotations
+ Bookmarkable
ID string `json:"id" orm:"pk;column(id)"`
Path string `json:"path"`
@@ -63,6 +64,7 @@ type MediaFileRepository interface {
DeleteByPath(path string) (int64, error)
AnnotatedRepository
+ BookmarkableRepository
}
func (mf MediaFile) GetAnnotations() Annotations {
diff --git a/model/playqueue.go b/model/playqueue.go
index 1d0ab099..d699f5d5 100644
--- a/model/playqueue.go
+++ b/model/playqueue.go
@@ -7,7 +7,6 @@ import (
type PlayQueue struct {
ID string `json:"id" orm:"column(id)"`
UserID string `json:"userId" orm:"column(user_id)"`
- Comment string `json:"comment"`
Current string `json:"current"`
Position int64 `json:"position"`
ChangedBy string `json:"changedBy"`
@@ -21,17 +20,4 @@ type PlayQueues []PlayQueue
type PlayQueueRepository interface {
Store(queue *PlayQueue) error
Retrieve(userId string) (*PlayQueue, error)
- AddBookmark(userId, id, comment string, position int64) error
- GetBookmarks(userId string) (Bookmarks, error)
- DeleteBookmark(userId, id string) error
}
-
-type Bookmark struct {
- Item MediaFile `json:"item"`
- Comment string `json:"comment"`
- Position int64 `json:"position"`
- CreatedAt time.Time `json:"createdAt"`
- UpdatedAt time.Time `json:"updatedAt"`
-}
-
-type Bookmarks []Bookmark
diff --git a/persistence/helpers.go b/persistence/helpers.go
index 597841f6..6bec5e98 100644
--- a/persistence/helpers.go
+++ b/persistence/helpers.go
@@ -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
}
}
diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go
index 16b07980..8abf30e5 100644
--- a/persistence/mediafile_repository.go
+++ b/persistence/mediafile_repository.go
@@ -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) {
diff --git a/persistence/persistence.go b/persistence/persistence.go
index 4cf676d5..fac59e71 100644
--- a/persistence/persistence.go
+++ b/persistence/persistence.go
@@ -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)
diff --git a/persistence/playqueue_repository.go b/persistence/playqueue_repository.go
index d092a774..d27d52a0 100644
--- a/persistence/playqueue_repository.go
+++ b/persistence/playqueue_repository.go
@@ -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)
diff --git a/persistence/playqueue_repository_test.go b/persistence/playqueue_repository_test.go
index 26433a45..3eec460f 100644
--- a/persistence/playqueue_repository_test.go
+++ b/persistence/playqueue_repository_test.go
@@ -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",
diff --git a/persistence/sql_annotations.go b/persistence/sql_annotations.go
index 4b8f9801..30b549ae 100644
--- a/persistence/sql_annotations.go
+++ b/persistence/sql_annotations.go
@@ -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},
}
}
diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go
index fc42f554..10428632 100644
--- a/persistence/sql_base_repository.go
+++ b/persistence/sql_base_repository.go
@@ -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()
diff --git a/persistence/sql_bookmarks.go b/persistence/sql_bookmarks.go
new file mode 100644
index 00000000..897270d0
--- /dev/null
+++ b/persistence/sql_bookmarks.go
@@ -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
+}
diff --git a/persistence/sql_bookmarks_test.go b/persistence/sql_bookmarks_test.go
new file mode 100644
index 00000000..ce35610e
--- /dev/null
+++ b/persistence/sql_bookmarks_test.go
@@ -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))
+ })
+ })
+})
diff --git a/server/subsonic/bookmarks.go b/server/subsonic/bookmarks.go
index 1c1b54dd..8411529b 100644
--- a/server/subsonic/bookmarks.go
+++ b/server/subsonic/bookmarks.go
@@ -21,8 +21,8 @@ func NewBookmarksController(ds model.DataStore) *BookmarksController {
func (c *BookmarksController) GetBookmarks(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
user, _ := request.UserFrom(r.Context())
- repo := c.ds.PlayQueue(r.Context())
- bmks, err := repo.GetBookmarks(user.ID)
+ repo := c.ds.MediaFile(r.Context())
+ bmks, err := repo.GetBookmarks()
if err != nil {
return nil, NewError(responses.ErrorGeneric, "Internal Error")
}
@@ -52,10 +52,8 @@ func (c *BookmarksController) CreateBookmark(w http.ResponseWriter, r *http.Requ
comment := utils.ParamString(r, "comment")
position := utils.ParamInt64(r, "position", 0)
- user, _ := request.UserFrom(r.Context())
-
- repo := c.ds.PlayQueue(r.Context())
- err = repo.AddBookmark(user.ID, id, comment, position)
+ repo := c.ds.MediaFile(r.Context())
+ err = repo.AddBookmark(id, comment, position)
if err != nil {
return nil, NewError(responses.ErrorGeneric, "Internal Error")
}
@@ -68,10 +66,8 @@ func (c *BookmarksController) DeleteBookmark(w http.ResponseWriter, r *http.Requ
return nil, err
}
- user, _ := request.UserFrom(r.Context())
-
- repo := c.ds.PlayQueue(r.Context())
- err = repo.DeleteBookmark(user.ID, id)
+ repo := c.ds.MediaFile(r.Context())
+ err = repo.DeleteBookmark(id)
if err != nil {
return nil, NewError(responses.ErrorGeneric, "Internal Error")
}
diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go
index c4051ac3..f391fbaa 100644
--- a/server/subsonic/helpers.go
+++ b/server/subsonic/helpers.go
@@ -142,6 +142,7 @@ func ToChild(ctx context.Context, entry engine.Entry) responses.Child {
child.TranscodedSuffix = format
child.TranscodedContentType = mime.TypeByExtension("." + format)
}
+ child.BookmarkPosition = entry.BookmarkPosition
return child
}
@@ -203,6 +204,7 @@ func ChildFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child
child.TranscodedSuffix = format
child.TranscodedContentType = mime.TypeByExtension("." + format)
}
+ child.BookmarkPosition = mf.BookmarkPosition
return child
}
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index 447da51a..3013d9cc 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -117,9 +117,9 @@ type Child struct {
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
+ BookmarkPosition int64 `xml:"bookmarkPosition,attr,omitempty" json:"bookmarkPosition,omitempty"`
/*
-
*/