feat: add transcodedSuffix to Subsonic API responses

This commit is contained in:
Deluan 2020-03-16 12:56:15 -04:00 committed by Deluan Quintão
parent 45180115a6
commit 39993810b3
12 changed files with 136 additions and 61 deletions

View file

@ -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) {

View file

@ -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

View file

@ -11,5 +11,6 @@ type Transcoding struct {
type Transcodings []Transcoding
type TranscodingRepository interface {
Get(id string) (*Transcoding, error)
Put(*Transcoding) error
}

View file

@ -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 }{}
}

View file

@ -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) {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}