Skip to content

Commit

Permalink
feat: transcoding and player datastores and configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
deluan committed Mar 17, 2020
1 parent a0e0fba commit 8ec7890
Show file tree
Hide file tree
Showing 32 changed files with 783 additions and 29 deletions.
17 changes: 17 additions & 0 deletions consts/consts.go
Expand Up @@ -20,3 +20,20 @@ const (
DevInitialUserName = "admin"
DevInitialName = "Dev Admin"
)

var (
DefaultTranscodings = []map[string]interface{}{
{
"name": "mp3 audio",
"targetFormat": "mp3",
"defaultBitRate": 192,
"command": "ffmpeg -i %s -ab %bk -v 0 -f mp3 -",
},
{
"name": "opus audio",
"targetFormat": "oga",
"defaultBitRate": 128,
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
}
)
49 changes: 49 additions & 0 deletions db/migration/20200310181627_add_transcoding_and_player_tables.go
@@ -0,0 +1,49 @@
package migration

import (
"database/sql"

"github.com/pressly/goose"
)

func init() {
goose.AddMigration(Up20200310181627, Down20200310181627)
}

func Up20200310181627(tx *sql.Tx) error {
_, err := tx.Exec(`
create table transcoding
(
id varchar(255) not null primary key,
name varchar(255) not null,
target_format varchar(255) not null,
command varchar(255) default '' not null,
default_bit_rate int default 192,
unique (name),
unique (target_format)
);
create table player
(
id varchar(255) not null primary key,
name varchar not null,
type varchar,
user_name varchar not null,
client varchar not null,
ip_address varchar,
last_seen timestamp,
transcoding_id varchar, -- todo foreign key
max_bit_rate int default 0,
unique (name)
);
`)
return err
}

func Down20200310181627(tx *sql.Tx) error {
_, err := tx.Exec(`
drop table transcoding;
drop table player;
`)
return err
}
57 changes: 57 additions & 0 deletions engine/players.go
@@ -0,0 +1,57 @@
package engine

import (
"context"
"fmt"
"time"

"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/google/uuid"
)

type Players interface {
Get(ctx context.Context, playerId string) (*model.Player, error)
Register(ctx context.Context, id, client, typ, ip string) (*model.Player, error)
}

func NewPlayers(ds model.DataStore) Players {
return &players{ds}
}

type players struct {
ds model.DataStore
}

func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, error) {
var plr *model.Player
var err error
userName := ctx.Value("username").(string)
if id != "" {
plr, err = p.ds.Player(ctx).Get(id)
}
if err != nil || id == "" {
plr, err = p.ds.Player(ctx).FindByName(client, userName)
if err == nil {
log.Trace("Found player by name", "id", plr.ID, "client", client, "userName", userName)
} else {
r, _ := uuid.NewRandom()
plr = &model.Player{
ID: r.String(),
Name: fmt.Sprintf("%s (%s)", client, userName),
UserName: userName,
Client: client,
}
log.Trace("Create new player", "id", plr.ID, "client", client, "userName", userName)
}
}
plr.LastSeen = time.Now()
plr.Type = typ
plr.IPAddress = ip
err = p.ds.Player(ctx).Put(plr)
return plr, err
}

func (p *players) Get(ctx context.Context, playerId string) (*model.Player, error) {
return p.ds.Player(ctx).Get(playerId)
}
112 changes: 112 additions & 0 deletions engine/players_test.go
@@ -0,0 +1,112 @@
package engine

import (
"context"
"time"

"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/persistence"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Players", func() {
var players Players
var repo *mockPlayerRepository
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid", UserName: "johndoe"})
ctx = context.WithValue(ctx, "username", "johndoe")
var beforeRegister time.Time

BeforeEach(func() {
repo = &mockPlayerRepository{}
ds := &persistence.MockDataStore{MockedPlayer: repo}
players = NewPlayers(ds)
beforeRegister = time.Now()
})

Describe("Register", func() {
It("creates a new player when no ID is specified", func() {
p, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).ToNot(BeEmpty())
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(p.Client).To(Equal("client"))
Expect(p.UserName).To(Equal("johndoe"))
Expect(p.Type).To(Equal("chrome"))
Expect(repo.lastSaved).To(Equal(p))
})

It("creates a new player if it cannot find any matching player", func() {
p, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).ToNot(BeEmpty())
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})

It("finds players by ID", func() {
plr := &model.Player{ID: "123", Name: "A Player", LastSeen: time.Time{}}
repo.add(plr)
p, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})

It("finds player by client and user names when ID is not found", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserName: "johndoe", LastSeen: time.Time{}}
repo.add(plr)
p, err := players.Register(ctx, "999", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})

It("finds player by client and user names when not ID is provided", func() {
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserName: "johndoe", LastSeen: time.Time{}}
repo.add(plr)
p, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
Expect(err).ToNot(HaveOccurred())
Expect(p.ID).To(Equal("123"))
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
Expect(repo.lastSaved).To(Equal(p))
})
})
})

type mockPlayerRepository struct {
model.PlayerRepository
lastSaved *model.Player
data map[string]model.Player
}

func (m *mockPlayerRepository) add(p *model.Player) {
if m.data == nil {
m.data = make(map[string]model.Player)
}
m.data[p.ID] = *p
}

func (m *mockPlayerRepository) Get(id string) (*model.Player, error) {
if p, ok := m.data[id]; ok {
return &p, nil
}
return nil, model.ErrNotFound
}

func (m *mockPlayerRepository) FindByName(client, userName string) (*model.Player, error) {
for _, p := range m.data {
if p.Client == client && p.UserName == userName {
return &p, nil
}
}
return nil, model.ErrNotFound
}

func (m *mockPlayerRepository) Put(p *model.Player) error {
m.lastSaved = p
return nil
}
2 changes: 1 addition & 1 deletion engine/playlists.go
Expand Up @@ -51,7 +51,7 @@ func (p *playlists) Create(ctx context.Context, playlistId, name string, ids []s
}

func (p *playlists) getUser(ctx context.Context) string {
user, ok := ctx.Value("user").(*model.User)
user, ok := ctx.Value("user").(model.User)
if ok {
return user.UserName
}
Expand Down
1 change: 1 addition & 0 deletions engine/wire_providers.go
Expand Up @@ -18,4 +18,5 @@ var Set = wire.NewSet(
NewMediaStreamer,
transcoder.New,
NewTranscodingCache,
NewPlayers,
)
2 changes: 2 additions & 0 deletions model/datastore.go
Expand Up @@ -28,6 +28,8 @@ type DataStore interface {
Playlist(ctx context.Context) PlaylistRepository
Property(ctx context.Context) PropertyRepository
User(ctx context.Context) UserRepository
Transcoding(ctx context.Context) TranscodingRepository
Player(ctx context.Context) PlayerRepository

Resource(ctx context.Context, model interface{}) ResourceRepository

Expand Down
25 changes: 25 additions & 0 deletions model/player.go
@@ -0,0 +1,25 @@
package model

import (
"time"
)

type Player struct {
ID string `json:"id" orm:"column(id)"`
Name string `json:"name"`
Type string `json:"type"`
UserName string `json:"userName"`
Client string `json:"client"`
IPAddress string `json:"ipAddress"`
LastSeen time.Time `json:"lastSeen"`
TranscodingId string `json:"transcodingId"`
MaxBitRate int `json:"maxBitRate"`
}

type Players []Player

type PlayerRepository interface {
Get(id string) (*Player, error)
FindByName(client, userName string) (*Player, error)
Put(p *Player) error
}
15 changes: 15 additions & 0 deletions model/transcoding.go
@@ -0,0 +1,15 @@
package model

type Transcoding struct {
ID string `json:"id" orm:"column(id)"`
Name string `json:"name"`
TargetFormat string `json:"targetFormat"`
Command string `json:"command"`
DefaultBitRate int `json:"defaultBitRate"`
}

type Transcodings []Transcoding

type TranscodingRepository interface {
Put(*Transcoding) error
}
2 changes: 1 addition & 1 deletion persistence/album_repository_test.go
Expand Up @@ -14,7 +14,7 @@ var _ = Describe("AlbumRepository", func() {
var repo model.AlbumRepository

BeforeEach(func() {
ctx := context.WithValue(log.NewContext(nil), "user", &model.User{ID: "userid"})
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
repo = NewAlbumRepository(ctx, orm.NewOrm())
})

Expand Down
2 changes: 1 addition & 1 deletion persistence/artist_repository_test.go
Expand Up @@ -14,7 +14,7 @@ var _ = Describe("ArtistRepository", func() {
var repo model.ArtistRepository

BeforeEach(func() {
ctx := context.WithValue(log.NewContext(nil), "user", &model.User{ID: "userid"})
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
repo = NewArtistRepository(ctx, orm.NewOrm())
})

Expand Down
2 changes: 1 addition & 1 deletion persistence/mediafile_repository_test.go
Expand Up @@ -16,7 +16,7 @@ var _ = Describe("MediaRepository", func() {
var mr model.MediaFileRepository

BeforeEach(func() {
ctx := context.WithValue(log.NewContext(nil), "user", &model.User{ID: "userid"})
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
mr = NewMediaFileRepository(ctx, orm.NewOrm())
})

Expand Down
12 changes: 12 additions & 0 deletions persistence/mock_persistence.go
Expand Up @@ -12,6 +12,7 @@ type MockDataStore struct {
MockedArtist model.ArtistRepository
MockedMediaFile model.MediaFileRepository
MockedUser model.UserRepository
MockedPlayer model.PlayerRepository
}

func (db *MockDataStore) Album(context.Context) model.AlbumRepository {
Expand Down Expand Up @@ -61,6 +62,17 @@ func (db *MockDataStore) User(context.Context) model.UserRepository {
return db.MockedUser
}

func (db *MockDataStore) Transcoding(context.Context) model.TranscodingRepository {
return struct{ model.TranscodingRepository }{}
}

func (db *MockDataStore) Player(context.Context) model.PlayerRepository {
if db.MockedPlayer != nil {
return db.MockedPlayer
}
return struct{ model.PlayerRepository }{}
}

func (db *MockDataStore) WithTx(block func(db model.DataStore) error) error {
return block(db)
}
Expand Down
12 changes: 12 additions & 0 deletions persistence/persistence.go
Expand Up @@ -55,10 +55,22 @@ func (s *SQLStore) User(ctx context.Context) model.UserRepository {
return NewUserRepository(ctx, s.getOrmer())
}

func (s *SQLStore) Transcoding(ctx context.Context) model.TranscodingRepository {
return NewTranscodingRepository(ctx, s.getOrmer())
}

func (s *SQLStore) Player(ctx context.Context) model.PlayerRepository {
return NewPlayerRepository(ctx, s.getOrmer())
}

func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRepository {
switch m.(type) {
case model.User:
return s.User(ctx).(model.ResourceRepository)
case model.Transcoding:
return s.Transcoding(ctx).(model.ResourceRepository)
case model.Player:
return s.Player(ctx).(model.ResourceRepository)
case model.Artist:
return s.Artist(ctx).(model.ResourceRepository)
case model.Album:
Expand Down
2 changes: 1 addition & 1 deletion persistence/persistence_suite_test.go
Expand Up @@ -86,7 +86,7 @@ var _ = Describe("Initialize test DB", func() {
// TODO Load this data setup from file(s)
BeforeSuite(func() {
o := orm.NewOrm()
ctx := context.WithValue(log.NewContext(nil), "user", &model.User{ID: "userid"})
ctx := context.WithValue(log.NewContext(nil), "user", model.User{ID: "userid"})
mr := NewMediaFileRepository(ctx, o)
for _, s := range testSongs {
err := mr.Put(&s)
Expand Down

0 comments on commit 8ec7890

Please sign in to comment.