diff --git a/engine/players.go b/engine/players.go index e84fef490..11d3f7083 100644 --- a/engine/players.go +++ b/engine/players.go @@ -12,7 +12,7 @@ import ( type Players interface { Get(ctx context.Context, playerId string) (*model.Player, error) - Register(ctx context.Context, id, client, typ, ip string) (*model.Player, error) + Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error) } func NewPlayers(ds model.DataStore) Players { @@ -23,8 +23,9 @@ type players struct { ds model.DataStore } -func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, error) { +func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error) { var plr *model.Player + var trc *model.Transcoding var err error userName := ctx.Value("username").(string) if id != "" { @@ -49,7 +50,13 @@ func (p *players) Register(ctx context.Context, id, client, typ, ip string) (*mo plr.Type = typ plr.IPAddress = ip err = p.ds.Player(ctx).Put(plr) - return plr, err + if err != nil { + return nil, nil, err + } + if plr.TranscodingId != "" { + trc, err = p.ds.Transcoding(ctx).Get(plr.TranscodingId) + } + return plr, trc, err } func (p *players) Get(ctx context.Context, playerId string) (*model.Player, error) { diff --git a/engine/players_test.go b/engine/players_test.go index addb31275..1adfa866b 100644 --- a/engine/players_test.go +++ b/engine/players_test.go @@ -20,14 +20,14 @@ var _ = Describe("Players", func() { BeforeEach(func() { repo = &mockPlayerRepository{} - ds := &persistence.MockDataStore{MockedPlayer: repo} + ds := &persistence.MockDataStore{MockedPlayer: repo, MockedTranscoding: &mockTranscodingRepository{}} 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") + p, trc, 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)) @@ -35,30 +35,33 @@ var _ = Describe("Players", func() { Expect(p.UserName).To(Equal("johndoe")) Expect(p.Type).To(Equal("chrome")) Expect(repo.lastSaved).To(Equal(p)) + Expect(trc).To(BeNil()) }) 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") + p, trc, 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)) + Expect(trc).To(BeNil()) }) 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") + p, trc, 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)) + Expect(trc).To(BeNil()) }) 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") + 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)) @@ -68,15 +71,34 @@ var _ = Describe("Players", func() { 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") + 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)) }) + + It("finds player by ID and return its transcoding", func() { + plr := &model.Player{ID: "123", Name: "A Player", LastSeen: time.Time{}, TranscodingId: "1"} + repo.add(plr) + p, trc, 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)) + Expect(trc.ID).To(Equal("1")) + }) }) }) +type mockTranscodingRepository struct { + model.TranscodingRepository +} + +func (m *mockTranscodingRepository) Get(id string) (*model.Transcoding, error) { + return &model.Transcoding{ID: id, TargetFormat: "mp3"}, nil +} + type mockPlayerRepository struct { model.PlayerRepository lastSaved *model.Player diff --git a/model/transcoding.go b/model/transcoding.go index 9bcce0379..243badedc 100644 --- a/model/transcoding.go +++ b/model/transcoding.go @@ -11,5 +11,6 @@ type Transcoding struct { type Transcodings []Transcoding type TranscodingRepository interface { + Get(id string) (*Transcoding, error) Put(*Transcoding) error } diff --git a/persistence/mock_persistence.go b/persistence/mock_persistence.go index 850e1bd74..d18243cb0 100644 --- a/persistence/mock_persistence.go +++ b/persistence/mock_persistence.go @@ -7,12 +7,13 @@ import ( ) type MockDataStore struct { - MockedGenre model.GenreRepository - MockedAlbum model.AlbumRepository - MockedArtist model.ArtistRepository - MockedMediaFile model.MediaFileRepository - MockedUser model.UserRepository - MockedPlayer model.PlayerRepository + MockedGenre model.GenreRepository + MockedAlbum model.AlbumRepository + MockedArtist model.ArtistRepository + MockedMediaFile model.MediaFileRepository + MockedUser model.UserRepository + MockedPlayer model.PlayerRepository + MockedTranscoding model.TranscodingRepository } func (db *MockDataStore) Album(context.Context) model.AlbumRepository { @@ -63,6 +64,9 @@ func (db *MockDataStore) User(context.Context) model.UserRepository { } func (db *MockDataStore) Transcoding(context.Context) model.TranscodingRepository { + if db.MockedTranscoding != nil { + return db.MockedTranscoding + } return struct{ model.TranscodingRepository }{} } diff --git a/persistence/transcoding_repository.go b/persistence/transcoding_repository.go index 7e21bc708..06c2b4928 100644 --- a/persistence/transcoding_repository.go +++ b/persistence/transcoding_repository.go @@ -21,6 +21,13 @@ func NewTranscodingRepository(ctx context.Context, o orm.Ormer) model.Transcodin return r } +func (r *transcodingRepository) Get(id string) (*model.Transcoding, error) { + sel := r.newSelect().Columns("*").Where(Eq{"id": id}) + var res model.Transcoding + err := r.queryOne(sel, &res) + return &res, err +} + func (r *transcodingRepository) Put(t *model.Transcoding) error { _, err := r.put(t.ID, t) return err @@ -31,11 +38,7 @@ func (r *transcodingRepository) Count(options ...rest.QueryOptions) (int64, erro } 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 - + return r.Get(id) } func (r *transcodingRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) { diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index dafdc898c..51d256dbb 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -66,7 +66,7 @@ func (c *AlbumListController) GetAlbumList(w http.ResponseWriter, r *http.Reques } response := NewResponse() - response.AlbumList = &responses.AlbumList{Album: ToChildren(albums)} + response.AlbumList = &responses.AlbumList{Album: ToChildren(r.Context(), albums)} return response, nil } @@ -77,7 +77,7 @@ func (c *AlbumListController) GetAlbumList2(w http.ResponseWriter, r *http.Reque } response := NewResponse() - response.AlbumList2 = &responses.AlbumList{Album: ToAlbums(albums)} + response.AlbumList2 = &responses.AlbumList{Album: ToAlbums(r.Context(), albums)} return response, nil } @@ -91,8 +91,8 @@ func (c *AlbumListController) GetStarred(w http.ResponseWriter, r *http.Request) response := NewResponse() response.Starred = &responses.Starred{} response.Starred.Artist = ToArtists(artists) - response.Starred.Album = ToChildren(albums) - response.Starred.Song = ToChildren(mediaFiles) + response.Starred.Album = ToChildren(r.Context(), albums) + response.Starred.Song = ToChildren(r.Context(), mediaFiles) return response, nil } @@ -106,8 +106,8 @@ func (c *AlbumListController) GetStarred2(w http.ResponseWriter, r *http.Request response := NewResponse() response.Starred2 = &responses.Starred{} response.Starred2.Artist = ToArtists(artists) - response.Starred2.Album = ToAlbums(albums) - response.Starred2.Song = ToChildren(mediaFiles) + response.Starred2.Album = ToAlbums(r.Context(), albums) + response.Starred2.Song = ToChildren(r.Context(), mediaFiles) return response, nil } @@ -122,7 +122,7 @@ func (c *AlbumListController) GetNowPlaying(w http.ResponseWriter, r *http.Reque response.NowPlaying = &responses.NowPlaying{} response.NowPlaying.Entry = make([]responses.NowPlayingEntry, len(npInfos)) for i, entry := range npInfos { - response.NowPlaying.Entry[i].Child = ToChild(entry) + response.NowPlaying.Entry[i].Child = ToChild(r.Context(), entry) response.NowPlaying.Entry[i].UserName = entry.UserName response.NowPlaying.Entry[i].MinutesAgo = entry.MinutesAgo response.NowPlaying.Entry[i].PlayerId = entry.PlayerId @@ -143,9 +143,6 @@ func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Requ response := NewResponse() response.RandomSongs = &responses.Songs{} - response.RandomSongs.Songs = make([]responses.Child, len(songs)) - for i, entry := range songs { - response.RandomSongs.Songs[i] = ToChild(entry) - } + response.RandomSongs.Songs = ToChildren(r.Context(), songs) return response, nil } diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index 2650439e2..6e4ec138f 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "fmt" "net/http" "time" @@ -97,7 +98,7 @@ func (c *BrowsingController) GetMusicDirectory(w http.ResponseWriter, r *http.Re } response := NewResponse() - response.Directory = c.buildDirectory(dir) + response.Directory = c.buildDirectory(r.Context(), dir) return response, nil } @@ -114,7 +115,7 @@ func (c *BrowsingController) GetArtist(w http.ResponseWriter, r *http.Request) ( } response := NewResponse() - response.ArtistWithAlbumsID3 = c.buildArtist(dir) + response.ArtistWithAlbumsID3 = c.buildArtist(r.Context(), dir) return response, nil } @@ -131,7 +132,7 @@ func (c *BrowsingController) GetAlbum(w http.ResponseWriter, r *http.Request) (* } response := NewResponse() - response.AlbumWithSongsID3 = c.buildAlbum(dir) + response.AlbumWithSongsID3 = c.buildAlbum(r.Context(), dir) return response, nil } @@ -148,7 +149,7 @@ func (c *BrowsingController) GetSong(w http.ResponseWriter, r *http.Request) (*r } response := NewResponse() - child := ToChild(*song) + child := ToChild(r.Context(), *song) response.Song = &child return response, nil } @@ -189,7 +190,7 @@ func (c *BrowsingController) GetArtistInfo2(w http.ResponseWriter, r *http.Reque return response, nil } -func (c *BrowsingController) buildDirectory(d *engine.DirectoryInfo) *responses.Directory { +func (c *BrowsingController) buildDirectory(ctx context.Context, d *engine.DirectoryInfo) *responses.Directory { dir := &responses.Directory{ Id: d.Id, Name: d.Name, @@ -202,11 +203,11 @@ func (c *BrowsingController) buildDirectory(d *engine.DirectoryInfo) *responses. dir.Starred = &d.Starred } - dir.Child = ToChildren(d.Entries) + dir.Child = ToChildren(ctx, d.Entries) return dir } -func (c *BrowsingController) buildArtist(d *engine.DirectoryInfo) *responses.ArtistWithAlbumsID3 { +func (c *BrowsingController) buildArtist(ctx context.Context, d *engine.DirectoryInfo) *responses.ArtistWithAlbumsID3 { dir := &responses.ArtistWithAlbumsID3{} dir.Id = d.Id dir.Name = d.Name @@ -216,11 +217,11 @@ func (c *BrowsingController) buildArtist(d *engine.DirectoryInfo) *responses.Art dir.Starred = &d.Starred } - dir.Album = ToAlbums(d.Entries) + dir.Album = ToAlbums(ctx, d.Entries) return dir } -func (c *BrowsingController) buildAlbum(d *engine.DirectoryInfo) *responses.AlbumWithSongsID3 { +func (c *BrowsingController) buildAlbum(ctx context.Context, d *engine.DirectoryInfo) *responses.AlbumWithSongsID3 { dir := &responses.AlbumWithSongsID3{} dir.Id = d.Id dir.Name = d.Name @@ -239,6 +240,6 @@ func (c *BrowsingController) buildAlbum(d *engine.DirectoryInfo) *responses.Albu dir.Starred = &d.Starred } - dir.Song = ToChildren(d.Entries) + dir.Song = ToChildren(ctx, d.Entries) return dir } diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index b9329b1eb..9639b1be3 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "fmt" "mime" "net/http" @@ -63,16 +64,16 @@ func (e SubsonicError) Error() string { return msg } -func ToAlbums(entries engine.Entries) []responses.Child { +func ToAlbums(ctx context.Context, entries engine.Entries) []responses.Child { children := make([]responses.Child, len(entries)) for i, entry := range entries { - children[i] = ToAlbum(entry) + children[i] = ToAlbum(ctx, entry) } return children } -func ToAlbum(entry engine.Entry) responses.Child { - album := ToChild(entry) +func ToAlbum(ctx context.Context, entry engine.Entry) responses.Child { + album := ToChild(ctx, entry) album.Name = album.Title album.Title = "" album.Parent = "" @@ -96,15 +97,15 @@ func ToArtists(entries engine.Entries) []responses.Artist { return artists } -func ToChildren(entries engine.Entries) []responses.Child { +func ToChildren(ctx context.Context, entries engine.Entries) []responses.Child { children := make([]responses.Child, len(entries)) for i, entry := range entries { - children[i] = ToChild(entry) + children[i] = ToChild(ctx, entry) } return children } -func ToChild(entry engine.Entry) responses.Child { +func ToChild(ctx context.Context, entry engine.Entry) responses.Child { child := responses.Child{} child.Id = entry.Id child.Title = entry.Title @@ -136,9 +137,11 @@ func ToChild(entry engine.Entry) responses.Child { child.IsVideo = false child.UserRating = entry.UserRating child.SongCount = entry.SongCount - // TODO Must be dynamic, based on player/transcoding config - child.TranscodedSuffix = "mp3" - child.TranscodedContentType = mime.TypeByExtension(".mp3") + format, _ := getTranscoding(ctx) + if entry.Suffix != "" && format != "" && entry.Suffix != format { + child.TranscodedSuffix = format + child.TranscodedContentType = mime.TypeByExtension("." + format) + } return child } @@ -149,3 +152,13 @@ func ToGenres(genres model.Genres) *responses.Genres { } return &responses.Genres{Genre: response} } + +func getTranscoding(ctx context.Context) (format string, bitRate int) { + if trc, ok := ctx.Value("transcoding").(model.Transcoding); ok { + format = trc.TargetFormat + } + if plr, ok := ctx.Value("player").(model.Player); ok { + bitRate = plr.MaxBitRate + } + return +} diff --git a/server/subsonic/middlewares.go b/server/subsonic/middlewares.go index aa03b5472..bbee7583d 100644 --- a/server/subsonic/middlewares.go +++ b/server/subsonic/middlewares.go @@ -103,11 +103,14 @@ func getPlayer(players engine.Players) func(next http.Handler) http.Handler { 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) + player, trc, 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) } else { ctx = context.WithValue(ctx, "player", *player) + if trc != nil { + ctx = context.WithValue(ctx, "transcoding", *trc) + } r = r.WithContext(ctx) } diff --git a/server/subsonic/middlewares_test.go b/server/subsonic/middlewares_test.go index 66ead808b..07b565aed 100644 --- a/server/subsonic/middlewares_test.go +++ b/server/subsonic/middlewares_test.go @@ -173,6 +173,7 @@ var _ = Describe("Middlewares", func() { Expect(next.called).To(BeTrue()) player := next.req.Context().Value("player").(model.Player) Expect(player.ID).To(Equal("123")) + Expect(next.req.Context().Value("transcoding")).To(BeNil()) }) It("returns the playerId in the cookie", func() { @@ -180,6 +181,27 @@ var _ = Describe("Middlewares", func() { Expect(cookieStr).To(ContainSubstring(playerIDCookieName("someone") + "=123")) }) }) + + Context("Player has transcoding configured", func() { + BeforeEach(func() { + cookie := &http.Cookie{ + Name: playerIDCookieName("someone"), + Value: "123", + MaxAge: cookieExpiry, + } + r.AddCookie(cookie) + mockedPlayers.transcoding = &model.Transcoding{ID: "12"} + gp := getPlayer(mockedPlayers)(next) + gp.ServeHTTP(w, r) + }) + + It("stores the player in the context", func() { + player := next.req.Context().Value("player").(model.Player) + Expect(player.ID).To(Equal("123")) + transcoding := next.req.Context().Value("transcoding").(model.Transcoding) + Expect(transcoding.ID).To(Equal("12")) + }) + }) }) }) @@ -212,12 +234,13 @@ func (m *mockUsers) Authenticate(ctx context.Context, username, password, token, type mockPlayers struct { engine.Players + transcoding *model.Transcoding } 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 +func (mp *mockPlayers) Register(ctx context.Context, id, client, typ, ip string) (*model.Player, *model.Transcoding, error) { + return &model.Player{ID: id}, mp.transcoding, nil } diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go index e64f97a76..dc6a52228 100644 --- a/server/subsonic/playlists.go +++ b/server/subsonic/playlists.go @@ -1,6 +1,7 @@ package subsonic import ( + "context" "errors" "fmt" "net/http" @@ -57,7 +58,7 @@ func (c *PlaylistsController) GetPlaylist(w http.ResponseWriter, r *http.Request } response := NewResponse() - response.Playlist = c.buildPlaylist(pinfo) + response.Playlist = c.buildPlaylist(r.Context(), pinfo) return response, nil } @@ -124,7 +125,7 @@ func (c *PlaylistsController) UpdatePlaylist(w http.ResponseWriter, r *http.Requ return NewResponse(), nil } -func (c *PlaylistsController) buildPlaylist(d *engine.PlaylistInfo) *responses.PlaylistWithSongs { +func (c *PlaylistsController) buildPlaylist(ctx context.Context, d *engine.PlaylistInfo) *responses.PlaylistWithSongs { pls := &responses.PlaylistWithSongs{} pls.Id = d.Id pls.Name = d.Name @@ -133,6 +134,6 @@ func (c *PlaylistsController) buildPlaylist(d *engine.PlaylistInfo) *responses.P pls.Duration = d.Duration pls.Public = d.Public - pls.Entry = ToChildren(d.Entries) + pls.Entry = ToChildren(ctx, d.Entries) return pls } diff --git a/server/subsonic/searching.go b/server/subsonic/searching.go index d95de3d38..c2218ad53 100644 --- a/server/subsonic/searching.go +++ b/server/subsonic/searching.go @@ -72,8 +72,8 @@ func (c *SearchingController) Search2(w http.ResponseWriter, r *http.Request) (* response := NewResponse() searchResult2 := &responses.SearchResult2{} searchResult2.Artist = ToArtists(as) - searchResult2.Album = ToChildren(als) - searchResult2.Song = ToChildren(mfs) + searchResult2.Album = ToChildren(r.Context(), als) + searchResult2.Song = ToChildren(r.Context(), mfs) response.SearchResult2 = searchResult2 return response, nil } @@ -99,8 +99,8 @@ func (c *SearchingController) Search3(w http.ResponseWriter, r *http.Request) (* searchResult3.Artist[i].Starred = &e.Starred } } - searchResult3.Album = ToAlbums(als) - searchResult3.Song = ToChildren(mfs) + searchResult3.Album = ToAlbums(r.Context(), als) + searchResult3.Song = ToChildren(r.Context(), mfs) response.SearchResult3 = searchResult3 return response, nil }