diff --git a/consts/consts.go b/consts/consts.go
index 5b8481791..a8ef159ac 100644
--- a/consts/consts.go
+++ b/consts/consts.go
@@ -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 -",
+ },
+ }
+)
diff --git a/db/migration/20200310181627_add_transcoding_and_player_tables.go b/db/migration/20200310181627_add_transcoding_and_player_tables.go
new file mode 100644
index 000000000..5de1422cc
--- /dev/null
+++ b/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
+}
diff --git a/engine/players.go b/engine/players.go
new file mode 100644
index 000000000..e84fef490
--- /dev/null
+++ b/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)
+}
diff --git a/engine/players_test.go b/engine/players_test.go
new file mode 100644
index 000000000..addb31275
--- /dev/null
+++ b/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
+}
diff --git a/engine/playlists.go b/engine/playlists.go
index 97828d22c..f4a0a0ae4 100644
--- a/engine/playlists.go
+++ b/engine/playlists.go
@@ -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
}
diff --git a/engine/wire_providers.go b/engine/wire_providers.go
index 547e19bbd..17d76f8ec 100644
--- a/engine/wire_providers.go
+++ b/engine/wire_providers.go
@@ -18,4 +18,5 @@ var Set = wire.NewSet(
NewMediaStreamer,
transcoder.New,
NewTranscodingCache,
+ NewPlayers,
)
diff --git a/model/datastore.go b/model/datastore.go
index 96cf1ed9d..a2fa14bdd 100644
--- a/model/datastore.go
+++ b/model/datastore.go
@@ -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
diff --git a/model/player.go b/model/player.go
new file mode 100644
index 000000000..3fc855102
--- /dev/null
+++ b/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
+}
diff --git a/model/transcoding.go b/model/transcoding.go
new file mode 100644
index 000000000..9bcce0379
--- /dev/null
+++ b/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
+}
diff --git a/persistence/album_repository_test.go b/persistence/album_repository_test.go
index d0dab2d4c..98222ee3a 100644
--- a/persistence/album_repository_test.go
+++ b/persistence/album_repository_test.go
@@ -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())
})
diff --git a/persistence/artist_repository_test.go b/persistence/artist_repository_test.go
index f7e272c94..b5ae5e43e 100644
--- a/persistence/artist_repository_test.go
+++ b/persistence/artist_repository_test.go
@@ -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())
})
diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go
index 5bff8310f..20af87f46 100644
--- a/persistence/mediafile_repository_test.go
+++ b/persistence/mediafile_repository_test.go
@@ -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())
})
diff --git a/persistence/mock_persistence.go b/persistence/mock_persistence.go
index 91b3bd7fd..850e1bd74 100644
--- a/persistence/mock_persistence.go
+++ b/persistence/mock_persistence.go
@@ -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 {
@@ -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)
}
diff --git a/persistence/persistence.go b/persistence/persistence.go
index f2de43669..e292e3b29 100644
--- a/persistence/persistence.go
+++ b/persistence/persistence.go
@@ -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:
diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go
index fd1d379f0..0e5e4e958 100644
--- a/persistence/persistence_suite_test.go
+++ b/persistence/persistence_suite_test.go
@@ -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)
diff --git a/persistence/player_repository.go b/persistence/player_repository.go
new file mode 100644
index 000000000..a9bf4d4be
--- /dev/null
+++ b/persistence/player_repository.go
@@ -0,0 +1,94 @@
+package persistence
+
+import (
+ "context"
+
+ . "github.com/Masterminds/squirrel"
+ "github.com/astaxie/beego/orm"
+ "github.com/deluan/navidrome/model"
+ "github.com/deluan/rest"
+)
+
+type playerRepository struct {
+ sqlRepository
+}
+
+func NewPlayerRepository(ctx context.Context, o orm.Ormer) model.PlayerRepository {
+ r := &playerRepository{}
+ r.ctx = ctx
+ r.ormer = o
+ r.tableName = "player"
+ return r
+}
+
+func (r *playerRepository) Put(p *model.Player) error {
+ _, err := r.put(p.ID, p)
+ return err
+}
+
+func (r *playerRepository) Get(id string) (*model.Player, error) {
+ sel := r.newSelect().Columns("*").Where(Eq{"id": id})
+ var res model.Player
+ err := r.queryOne(sel, &res)
+ return &res, err
+}
+
+func (r *playerRepository) FindByName(client, userName string) (*model.Player, error) {
+ sel := r.newSelect().Columns("*").Where(And{Eq{"client": client}, Eq{"user_name": userName}})
+ var res model.Player
+ err := r.queryOne(sel, &res)
+ return &res, err
+}
+
+func (r *playerRepository) Count(options ...rest.QueryOptions) (int64, error) {
+ return r.count(Select(), r.parseRestOptions(options...))
+}
+
+func (r *playerRepository) Read(id string) (interface{}, error) {
+ return r.Get(id)
+}
+
+func (r *playerRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
+ sel := r.newSelect(r.parseRestOptions(options...)).Columns("*")
+ res := model.Players{}
+ err := r.queryAll(sel, &res)
+ return res, err
+}
+
+func (r *playerRepository) EntityName() string {
+ return "player"
+}
+
+func (r *playerRepository) NewInstance() interface{} {
+ return &model.Player{}
+}
+
+func (r *playerRepository) Save(entity interface{}) (string, error) {
+ t := entity.(*model.Player)
+ id, err := r.put(t.ID, t)
+ if err == model.ErrNotFound {
+ return "", rest.ErrNotFound
+ }
+ return id, err
+}
+
+func (r *playerRepository) Update(entity interface{}, cols ...string) error {
+ t := entity.(*model.Player)
+ _, err := r.put(t.ID, t)
+ if err == model.ErrNotFound {
+ return rest.ErrNotFound
+ }
+ return err
+}
+
+func (r *playerRepository) Delete(id string) error {
+ err := r.delete(Eq{"id": id})
+ if err == model.ErrNotFound {
+ return rest.ErrNotFound
+ }
+ return err
+}
+
+var _ model.PlayerRepository = (*playerRepository)(nil)
+var _ rest.Repository = (*playerRepository)(nil)
+var _ rest.Persistable = (*playerRepository)(nil)
diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go
index f906ce0bb..9b21db352 100644
--- a/persistence/sql_base_repository.go
+++ b/persistence/sql_base_repository.go
@@ -29,7 +29,7 @@ func userId(ctx context.Context) string {
if user == nil {
return invalidUserId
}
- usr := user.(*model.User)
+ usr := user.(model.User)
return usr.ID
}
@@ -38,7 +38,8 @@ func loggedUser(ctx context.Context) *model.User {
if user == nil {
return &model.User{}
}
- return user.(*model.User)
+ u := user.(model.User)
+ return &u
}
func (r sqlRepository) newSelect(options ...model.QueryOptions) SelectBuilder {
diff --git a/persistence/transcoding_repository.go b/persistence/transcoding_repository.go
new file mode 100644
index 000000000..7e21bc708
--- /dev/null
+++ b/persistence/transcoding_repository.go
@@ -0,0 +1,84 @@
+package persistence
+
+import (
+ "context"
+
+ . "github.com/Masterminds/squirrel"
+ "github.com/astaxie/beego/orm"
+ "github.com/deluan/navidrome/model"
+ "github.com/deluan/rest"
+)
+
+type transcodingRepository struct {
+ sqlRepository
+}
+
+func NewTranscodingRepository(ctx context.Context, o orm.Ormer) model.TranscodingRepository {
+ r := &transcodingRepository{}
+ r.ctx = ctx
+ r.ormer = o
+ r.tableName = "transcoding"
+ return r
+}
+
+func (r *transcodingRepository) Put(t *model.Transcoding) error {
+ _, err := r.put(t.ID, t)
+ return err
+}
+
+func (r *transcodingRepository) Count(options ...rest.QueryOptions) (int64, error) {
+ return r.count(Select(), r.parseRestOptions(options...))
+}
+
+func (r *transcodingRepository) Read(id string) (interface{}, error) {
+ sel := r.newSelect().Columns("*").Where(Eq{"id": id})
+ var res model.Transcoding
+ err := r.queryOne(sel, &res)
+ return &res, err
+
+}
+
+func (r *transcodingRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
+ sel := r.newSelect(r.parseRestOptions(options...)).Columns("*")
+ res := model.Transcodings{}
+ err := r.queryAll(sel, &res)
+ return res, err
+}
+
+func (r *transcodingRepository) EntityName() string {
+ return "transcoding"
+}
+
+func (r *transcodingRepository) NewInstance() interface{} {
+ return &model.Transcoding{}
+}
+
+func (r *transcodingRepository) Save(entity interface{}) (string, error) {
+ t := entity.(*model.Transcoding)
+ id, err := r.put(t.ID, t)
+ if err == model.ErrNotFound {
+ return "", rest.ErrNotFound
+ }
+ return id, err
+}
+
+func (r *transcodingRepository) Update(entity interface{}, cols ...string) error {
+ t := entity.(*model.Transcoding)
+ _, err := r.put(t.ID, t)
+ if err == model.ErrNotFound {
+ return rest.ErrNotFound
+ }
+ return err
+}
+
+func (r *transcodingRepository) Delete(id string) error {
+ err := r.delete(Eq{"id": id})
+ if err == model.ErrNotFound {
+ return rest.ErrNotFound
+ }
+ return err
+}
+
+var _ model.TranscodingRepository = (*transcodingRepository)(nil)
+var _ rest.Repository = (*transcodingRepository)(nil)
+var _ rest.Persistable = (*transcodingRepository)(nil)
diff --git a/server/app/app.go b/server/app/app.go
index 749f7dd97..aa57e3413 100644
--- a/server/app/app.go
+++ b/server/app/app.go
@@ -43,6 +43,8 @@ func (app *Router) routes() http.Handler {
app.R(r, "/song", model.MediaFile{})
app.R(r, "/album", model.Album{})
app.R(r, "/artist", model.Artist{})
+ app.R(r, "/transcoding", model.Transcoding{})
+ app.R(r, "/player", model.Player{})
// Keepalive endpoint to be used to keep the session valid (ex: while playing songs)
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"response":"ok"}`)) })
diff --git a/server/app/auth.go b/server/app/auth.go
index 1eb00d683..cc5403af5 100644
--- a/server/app/auth.go
+++ b/server/app/auth.go
@@ -149,7 +149,7 @@ func validateLogin(userRepo model.UserRepository, userName, password string) (*m
func contextWithUser(ctx context.Context, ds model.DataStore, claims jwt.MapClaims) context.Context {
userName := claims["sub"].(string)
user, _ := ds.User(ctx).FindByUsername(userName)
- return context.WithValue(ctx, "user", user)
+ return context.WithValue(ctx, "user", *user)
}
func getToken(ds model.DataStore, ctx context.Context) (*jwt.Token, error) {
diff --git a/server/initial_setup.go b/server/initial_setup.go
index 6661cf07b..72347400c 100644
--- a/server/initial_setup.go
+++ b/server/initial_setup.go
@@ -1,7 +1,7 @@
package server
import (
- "context"
+ "encoding/json"
"fmt"
"time"
@@ -29,14 +29,17 @@ func initialSetup(ds model.DataStore) {
}
}
+ if err = createDefaultTranscodings(ds); err != nil {
+ return err
+ }
+
err = ds.Property(nil).Put(consts.InitialSetupFlagKey, time.Now().String())
return err
})
}
func createInitialAdminUser(ds model.DataStore) error {
- ctx := context.Background()
- c, err := ds.User(ctx).CountAll()
+ c, err := ds.User(nil).CountAll()
if err != nil {
panic(fmt.Sprintf("Could not access User table: %s", err))
}
@@ -56,7 +59,7 @@ func createInitialAdminUser(ds model.DataStore) error {
Password: initialPassword,
IsAdmin: true,
}
- err := ds.User(ctx).Put(&initialUser)
+ err := ds.User(nil).Put(&initialUser)
if err != nil {
log.Error("Could not create initial admin user", "user", initialUser, err)
}
@@ -77,3 +80,23 @@ func createJWTSecret(ds model.DataStore) error {
}
return err
}
+
+func createDefaultTranscodings(ds model.DataStore) error {
+ repo := ds.Transcoding(nil)
+ for _, d := range consts.DefaultTranscodings {
+ var j []byte
+ var err error
+ if j, err = json.Marshal(d); err != nil {
+ return err
+ }
+ var t model.Transcoding
+ if err = json.Unmarshal(j, &t); err != nil {
+ return err
+ }
+ log.Info("Creating default transcoding config", "name", t.Name)
+ if err = repo.Put(&t); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/server/subsonic/api.go b/server/subsonic/api.go
index d969d6600..cea352472 100644
--- a/server/subsonic/api.go
+++ b/server/subsonic/api.go
@@ -27,16 +27,17 @@ type Router struct {
Search engine.Search
Users engine.Users
Streamer engine.MediaStreamer
+ Players engine.Players
mux http.Handler
}
func New(browser engine.Browser, cover engine.Cover, listGenerator engine.ListGenerator, users engine.Users,
playlists engine.Playlists, ratings engine.Ratings, scrobbler engine.Scrobbler, search engine.Search,
- streamer engine.MediaStreamer) *Router {
+ streamer engine.MediaStreamer, players engine.Players) *Router {
r := &Router{Browser: browser, Cover: cover, ListGenerator: listGenerator, Playlists: playlists,
- Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer}
+ Ratings: ratings, Scrobbler: scrobbler, Search: search, Users: users, Streamer: streamer, Players: players}
r.mux = r.routes()
return r
}
@@ -50,6 +51,7 @@ func (api *Router) routes() http.Handler {
r.Use(postFormToQueryParams)
r.Use(checkRequiredParameters)
+ r.Use(getPlayer(api.Players))
// Add validation middleware
r.Use(authenticate(api.Users))
diff --git a/server/subsonic/middlewares.go b/server/subsonic/middlewares.go
index 32b612cb6..dc8470894 100644
--- a/server/subsonic/middlewares.go
+++ b/server/subsonic/middlewares.go
@@ -3,6 +3,7 @@ package subsonic
import (
"context"
"fmt"
+ "net"
"net/http"
"net/url"
"strings"
@@ -14,6 +15,10 @@ import (
"github.com/deluan/navidrome/utils"
)
+const (
+ cookieExpiry = 365 * 24 * 3600 // One year
+)
+
func postFormToQueryParams(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
@@ -82,10 +87,54 @@ func authenticate(users engine.Users) func(next http.Handler) http.Handler {
}
ctx := r.Context()
- ctx = context.WithValue(ctx, "user", usr)
+ ctx = context.WithValue(ctx, "user", *usr)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
+
+func getPlayer(players engine.Players) func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ userName := ctx.Value("username").(string)
+ client := ctx.Value("client").(string)
+ playerId := playerIDFromCookie(r, userName)
+ ip, _, _ := net.SplitHostPort(r.RemoteAddr)
+ player, err := players.Register(ctx, playerId, client, r.Header.Get("user-agent"), ip)
+ if err != nil {
+ log.Error("Could not register player", "userName", userName, "client", client)
+ }
+
+ ctx = context.WithValue(ctx, "player", *player)
+ r = r.WithContext(ctx)
+
+ cookie := &http.Cookie{
+ Name: playerIDCookieName(userName),
+ Value: player.ID,
+ MaxAge: cookieExpiry,
+ HttpOnly: true,
+ Path: "/",
+ }
+ http.SetCookie(w, cookie)
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+func playerIDFromCookie(r *http.Request, userName string) string {
+ cookieName := playerIDCookieName(userName)
+ var playerId string
+ if c, err := r.Cookie(cookieName); err == nil {
+ playerId = c.Value
+ log.Trace(r, "playerId found in cookies", "playerId", playerId)
+ }
+ return playerId
+}
+
+func playerIDCookieName(userName string) string {
+ cookieName := fmt.Sprintf("nd-player-%x", userName)
+ return cookieName
+}
diff --git a/server/subsonic/middlewares_test.go b/server/subsonic/middlewares_test.go
index e5573e18e..66ead808b 100644
--- a/server/subsonic/middlewares_test.go
+++ b/server/subsonic/middlewares_test.go
@@ -107,35 +107,80 @@ var _ = Describe("Middlewares", func() {
})
Describe("Authenticate", func() {
- var mockedUser *mockUsers
+ var mockedUsers *mockUsers
BeforeEach(func() {
- mockedUser = &mockUsers{}
+ mockedUsers = &mockUsers{}
})
It("passes all parameters to users.Authenticate ", func() {
r := newGetRequest("u=valid", "p=password", "t=token", "s=salt", "jwt=jwt")
- cp := authenticate(mockedUser)(next)
+ cp := authenticate(mockedUsers)(next)
cp.ServeHTTP(w, r)
- Expect(mockedUser.username).To(Equal("valid"))
- Expect(mockedUser.password).To(Equal("password"))
- Expect(mockedUser.token).To(Equal("token"))
- Expect(mockedUser.salt).To(Equal("salt"))
- Expect(mockedUser.jwt).To(Equal("jwt"))
+ Expect(mockedUsers.username).To(Equal("valid"))
+ Expect(mockedUsers.password).To(Equal("password"))
+ Expect(mockedUsers.token).To(Equal("token"))
+ Expect(mockedUsers.salt).To(Equal("salt"))
+ Expect(mockedUsers.jwt).To(Equal("jwt"))
Expect(next.called).To(BeTrue())
- user := next.req.Context().Value("user").(*model.User)
+ user := next.req.Context().Value("user").(model.User)
Expect(user.UserName).To(Equal("valid"))
})
It("fails authentication with wrong password", func() {
r := newGetRequest("u=invalid", "", "", "")
- cp := authenticate(mockedUser)(next)
+ cp := authenticate(mockedUsers)(next)
cp.ServeHTTP(w, r)
Expect(w.Body.String()).To(ContainSubstring(`code="40"`))
Expect(next.called).To(BeFalse())
})
})
+
+ Describe("GetPlayer", func() {
+ var mockedPlayers *mockPlayers
+ var r *http.Request
+ BeforeEach(func() {
+ mockedPlayers = &mockPlayers{}
+ r = newGetRequest()
+ ctx := context.WithValue(r.Context(), "username", "someone")
+ ctx = context.WithValue(ctx, "client", "client")
+ r = r.WithContext(ctx)
+ })
+
+ It("returns a new player in the cookies when none is specified", func() {
+ gp := getPlayer(mockedPlayers)(next)
+ gp.ServeHTTP(w, r)
+
+ cookieStr := w.Header().Get("Set-Cookie")
+ Expect(cookieStr).To(ContainSubstring(playerIDCookieName("someone")))
+ })
+
+ Context("PlayerId specified in Cookies", func() {
+ BeforeEach(func() {
+ cookie := &http.Cookie{
+ Name: playerIDCookieName("someone"),
+ Value: "123",
+ MaxAge: cookieExpiry,
+ }
+ r.AddCookie(cookie)
+
+ gp := getPlayer(mockedPlayers)(next)
+ gp.ServeHTTP(w, r)
+ })
+
+ It("stores the player in the context", func() {
+ Expect(next.called).To(BeTrue())
+ player := next.req.Context().Value("player").(model.Player)
+ Expect(player.ID).To(Equal("123"))
+ })
+
+ It("returns the playerId in the cookie", func() {
+ cookieStr := w.Header().Get("Set-Cookie")
+ Expect(cookieStr).To(ContainSubstring(playerIDCookieName("someone") + "=123"))
+ })
+ })
+ })
})
type mockHandler struct {
@@ -164,3 +209,15 @@ func (m *mockUsers) Authenticate(ctx context.Context, username, password, token,
}
return nil, model.ErrInvalidAuth
}
+
+type mockPlayers struct {
+ engine.Players
+}
+
+func (mp *mockPlayers) Get(ctx context.Context, playerId string) (*model.Player, error) {
+ return &model.Player{ID: playerId}, nil
+}
+
+func (mp *mockPlayers) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, error) {
+ return &model.Player{ID: id}, nil
+}
diff --git a/ui/src/App.js b/ui/src/App.js
index ca39a1d75..485efaa1a 100644
--- a/ui/src/App.js
+++ b/ui/src/App.js
@@ -5,6 +5,7 @@ import authProvider from './authProvider'
import polyglotI18nProvider from 'ra-i18n-polyglot'
import messages from './i18n'
import { DarkTheme, Layout, Login } from './layout'
+import transcoding from './transcoding'
import user from './user'
import song from './song'
import album from './album'
@@ -44,7 +45,16 @@ const App = () => {