Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create playqueue table and repository
- Loading branch information
Showing
4 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
} |