Skip to content

Commit

Permalink
Create playqueue table and repository
Browse files Browse the repository at this point in the history
  • Loading branch information
deluan committed Jul 31, 2020
1 parent 3c2b14d commit 721a959
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 0 deletions.
36 changes: 36 additions & 0 deletions db/migration/20200731095603_create_play_queues_table.go
@@ -0,0 +1,36 @@
package migration

import (
"database/sql"

"github.com/pressly/goose"
)

func init() {
goose.AddMigration(upCreatePlayQueuesTable, downCreatePlayQueuesTable)
}

func upCreatePlayQueuesTable(tx *sql.Tx) error {
_, err := tx.Exec(`
create table playqueue
(
id varchar(255) not null,
user_id varchar(255) not null
references user (id)
on update cascade on delete cascade,
comment varchar(255),
current varchar(255),
position real,
changed_by varchar(255),
items varchar(255),
created_at datetime,
updated_at datetime
);
`)

return err
}

func downCreatePlayQueuesTable(tx *sql.Tx) error {
return nil
}
24 changes: 24 additions & 0 deletions model/playqueue.go
@@ -0,0 +1,24 @@
package model

import (
"time"
)

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 float32 `json:"position"`
ChangedBy string `json:"changedBy"`
Items MediaFiles `json:"items,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

type PlayQueues []PlayQueue

type PlayQueueRepository interface {
Store(queue *PlayQueue) error
Retrieve(userId string) (*PlayQueue, error)
}
151 changes: 151 additions & 0 deletions persistence/playqueue_repository.go
@@ -0,0 +1,151 @@
package persistence

import (
"context"
"strings"
"time"

. "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
)

type playQueueRepository struct {
sqlRepository
sqlRestful
}

func NewPlayQueueRepository(ctx context.Context, o orm.Ormer) model.PlayQueueRepository {
r := &playQueueRepository{}
r.ctx = ctx
r.ormer = o
r.tableName = "playqueue"
return r
}

type playQueue struct {
ID string `orm:"column(id)"`
UserID string `orm:"column(user_id)"`
Comment string
Current string
Position float32
ChangedBy string
Items string
CreatedAt time.Time
UpdatedAt time.Time
}

func (r *playQueueRepository) Store(q *model.PlayQueue) error {
err := r.clearPlayQueue(q.UserID)
if err != nil {
return err
}
pq := r.fromModel(q)
if pq.ID == "" {
pq.CreatedAt = time.Now()
}
pq.UpdatedAt = time.Now()
_, err = r.put(pq.ID, pq)
return err
}

func (r *playQueueRepository) Retrieve(userId string) (*model.PlayQueue, error) {
sel := r.newSelect().Columns("*").Where(Eq{"user_id": userId})
var res playQueue
err := r.queryOne(sel, &res)
pls := r.toModel(&res)
return &pls, err

}

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,
CreatedAt: q.CreatedAt,
UpdatedAt: q.UpdatedAt,
}
var itemIDs []string
for _, t := range q.Items {
itemIDs = append(itemIDs, t.ID)
}
pq.Items = strings.Join(itemIDs, ",")
return pq
}

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,
CreatedAt: pq.CreatedAt,
UpdatedAt: pq.UpdatedAt,
}
if strings.TrimSpace(pq.Items) != "" {
tracks := strings.Split(pq.Items, ",")
for _, t := range tracks {
q.Items = append(q.Items, model.MediaFile{ID: t})
}
}
q.Items = r.loadTracks(&q)
return q
}

func (r *playQueueRepository) loadTracks(p *model.PlayQueue) model.MediaFiles {
if len(p.Items) == 0 {
return nil
}

// Collect all ids
ids := make([]string, len(p.Items))
for i, t := range p.Items {
ids[i] = t.ID
}

// Break the list in chunks, up to 50 items, to avoid hitting SQLITE_MAX_FUNCTION_ARG limit
const chunkSize = 50
var chunks [][]string
for i := 0; i < len(ids); i += chunkSize {
end := i + chunkSize
if end > len(ids) {
end = len(ids)
}

chunks = append(chunks, ids[i:end])
}

// Query each chunk of media_file ids and store results in a map
mfRepo := NewMediaFileRepository(r.ctx, r.ormer)
trackMap := map[string]model.MediaFile{}
for i := range chunks {
idsFilter := Eq{"id": chunks[i]}
tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: idsFilter})
if err != nil {
log.Error(r.ctx, "Could not load playqueue's tracks", "userId", p.UserID, err)
}
for _, t := range tracks {
trackMap[t.ID] = t
}
}

// Create a new list of tracks with the same order as the original
newTracks := make(model.MediaFiles, len(p.Items))
for i, t := range p.Items {
newTracks[i] = trackMap[t.ID]
}
return newTracks
}

func (r *playQueueRepository) clearPlayQueue(userId string) error {
return r.delete(Eq{"user_id": userId})
}

var _ model.PlayQueueRepository = (*playQueueRepository)(nil)
92 changes: 92 additions & 0 deletions persistence/playqueue_repository_test.go
@@ -0,0 +1,92 @@
package persistence

import (
"context"
"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"
"github.com/google/uuid"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("PlayQueueRepository", func() {
var repo model.PlayQueueRepository

BeforeEach(func() {
ctx := log.NewContext(context.TODO())
ctx = request.WithUser(ctx, model.User{ID: "user1", UserName: "user1", IsAdmin: true})
repo = NewPlayQueueRepository(ctx, orm.NewOrm())
})

It("returns notfound error if there's no playqueue for the user", func() {
_, err := repo.Retrieve("user999")
Expect(err).To(MatchError(model.ErrNotFound))
})

It("stores and retrieves the playqueue for the user", func() {
By("Storing a playqueue for the user")

expected := aPlayQueue("user1", songDayInALife.ID, 123, songComeTogether, songDayInALife)
Expect(repo.Store(expected)).To(BeNil())

actual, err := repo.Retrieve("user1")
Expect(err).To(BeNil())

AssertPlayQueue(expected, actual)

By("Storing a new playqueue for the same user")

new := aPlayQueue("user1", songRadioactivity.ID, 321, songAntenna, songRadioactivity)
Expect(repo.Store(new)).To(BeNil())

actual, err = repo.Retrieve("user1")
Expect(err).To(BeNil())

AssertPlayQueue(new, actual)
Expect(countPlayQueues(repo, "user1")).To(Equal(1))
})
})

func countPlayQueues(repo model.PlayQueueRepository, userId string) int {
r := repo.(*playQueueRepository)
c, err := r.count(squirrel.Select().Where(squirrel.Eq{"user_id": userId}))
if err != nil {
panic(err)
}
return int(c)
}

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))
Expect(actual.Items).To(HaveLen(len(expected.Items)))
for i, item := range actual.Items {
Expect(item.Title).To(Equal(expected.Items[i].Title))
}
}

func aPlayQueue(userId, current string, position float32, items ...model.MediaFile) *model.PlayQueue {
createdAt := time.Now()
updatedAt := createdAt.Add(time.Minute)
id, _ := uuid.NewRandom()
return &model.PlayQueue{
ID: id.String(),
UserID: userId,
Comment: "no_comments",
Current: current,
Position: position,
ChangedBy: "test",
Items: items,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}

0 comments on commit 721a959

Please sign in to comment.