First version of getAlbumList.view working.

- It still misses almost all type options
- Introduced "parent" in Child subresponse, as it was breaking DSub
This commit is contained in:
Deluan 2016-03-04 09:09:16 -05:00
parent 87e012f3bf
commit 9a246b5432
12 changed files with 239 additions and 48 deletions

67
api/get_album_list.go Normal file
View File

@ -0,0 +1,67 @@
package api
import (
"github.com/astaxie/beego"
"github.com/deluan/gosonic/api/responses"
"github.com/deluan/gosonic/domain"
"github.com/deluan/gosonic/utils"
"github.com/karlkfi/inject"
"time"
)
type GetAlbumListController struct {
BaseAPIController
albumRepo domain.AlbumRepository
types map[string]domain.QueryOptions
}
func (c *GetAlbumListController) Prepare() {
inject.ExtractAssignable(utils.Graph, &c.albumRepo)
// TODO To implement other types, we need to fix album data at import time
c.types = map[string]domain.QueryOptions{
"newest": domain.QueryOptions{SortBy: "CreatedAt", Desc: true, Alpha: true},
}
}
func (c *GetAlbumListController) Get() {
typ := c.GetParameter("type", "Required string parameter 'type' is not present")
qo, found := c.types[typ]
if !found {
beego.Error("getAlbumList type", typ, "not implemented!")
c.SendError(responses.ERROR_GENERIC, "Not implemented yet!")
}
qo.Size = 10
c.Ctx.Input.Bind(&qo.Size, "size")
c.Ctx.Input.Bind(&qo.Offset, "offset")
albums, err := c.albumRepo.GetAll(qo)
if err != nil {
beego.Error("Error retrieving albums:", err)
c.SendError(responses.ERROR_GENERIC, "Internal Error")
}
albumList := make([]responses.Child, len(albums))
for i, al := range albums {
albumList[i].Id = al.Id
albumList[i].Title = al.Name
albumList[i].Parent = al.ArtistId
albumList[i].IsDir = true
albumList[i].Album = al.Name
albumList[i].Year = al.Year
albumList[i].Artist = al.Artist
albumList[i].Genre = al.Genre
albumList[i].CoverArt = al.CoverArtId
if al.Starred {
t := time.Now()
albumList[i].Starred = &t
}
}
response := c.NewEmpty()
response.AlbumList = &responses.AlbumList{Album: albumList}
c.SendResponse(response)
}

View File

@ -0,0 +1,56 @@
package api_test
import (
"testing"
"github.com/deluan/gosonic/api/responses"
"github.com/deluan/gosonic/domain"
. "github.com/deluan/gosonic/tests"
"github.com/deluan/gosonic/tests/mocks"
"github.com/deluan/gosonic/utils"
. "github.com/smartystreets/goconvey/convey"
)
func TestGetAlbumList(t *testing.T) {
Init(t, false)
mockAlbumRepo := mocks.CreateMockAlbumRepo()
utils.DefineSingleton(new(domain.AlbumRepository), func() domain.AlbumRepository {
return mockAlbumRepo
})
Convey("Subject: GetAlbumList Endpoint", t, func() {
mockAlbumRepo.SetData(`[
{"Id":"A","Name":"Vagarosa","ArtistId":"2"},
{"Id":"C","Name":"Liberation: The Island Anthology","ArtistId":"3"},
{"Id":"B","Name":"Planet Rock","ArtistId":"1"}]`, 1)
Convey("Should fail if missing 'type' parameter", func() {
_, w := Get(AddParams("/rest/getAlbumList.view"), "TestGetAlbumList")
So(w.Body, ShouldReceiveError, responses.ERROR_MISSING_PARAMETER)
})
Convey("Return fail on Album Table error", func() {
mockAlbumRepo.SetError(true)
_, w := Get(AddParams("/rest/getAlbumList.view", "type=newest"), "TestGetAlbumList")
So(w.Body, ShouldReceiveError, responses.ERROR_GENERIC)
})
Convey("Type is invalid", func() {
_, w := Get(AddParams("/rest/getAlbumList.view", "type=not_implemented"), "TestGetAlbumList")
So(w.Body, ShouldReceiveError, responses.ERROR_GENERIC)
})
Convey("Type == newest", func() {
_, w := Get(AddParams("/rest/getAlbumList.view", "type=newest"), "TestGetAlbumList")
So(w.Body, ShouldBeAValid, responses.AlbumList{})
So(mockAlbumRepo.Options.SortBy, ShouldEqual, "CreatedAt")
So(mockAlbumRepo.Options.Desc, ShouldBeTrue)
So(mockAlbumRepo.Options.Alpha, ShouldBeTrue)
})
Reset(func() {
mockAlbumRepo.SetData("[]", 0)
mockAlbumRepo.SetError(false)
})
})
}

View File

@ -50,6 +50,7 @@ func (c *GetMusicDirectoryController) buildArtistDir(a *domain.Artist, albums []
dir.Child[i].Id = al.Id
dir.Child[i].Title = al.Name
dir.Child[i].IsDir = true
dir.Child[i].Parent = al.ArtistId
dir.Child[i].Album = al.Name
dir.Child[i].Year = al.Year
dir.Child[i].Artist = al.Artist
@ -72,6 +73,7 @@ func (c *GetMusicDirectoryController) buildAlbumDir(al *domain.Album, tracks []d
dir.Child[i].Id = mf.Id
dir.Child[i].Title = mf.Title
dir.Child[i].IsDir = false
dir.Child[i].Parent = mf.AlbumId
dir.Child[i].Album = mf.Album
dir.Child[i].Year = mf.Year
dir.Child[i].Artist = mf.Artist

View File

@ -59,7 +59,7 @@ func TestGetMusicDirectory(t *testing.T) {
mockAlbumRepo.SetData(`[{"Id":"A","Name":"Tardis","ArtistId":"1"}]`, 1)
_, w := Get(AddParams("/rest/getMusicDirectory.view", "id=1"), "TestGetMusicDirectory")
So(w.Body, ShouldContainJSON, `"child":[{"album":"Tardis","id":"A","isDir":true,"title":"Tardis"}]`)
So(w.Body, ShouldContainJSON, `"child":[{"album":"Tardis","id":"A","isDir":true,"parent":"1","title":"Tardis"}]`)
})
})
Convey("When id matches an album with tracks", func() {
@ -68,7 +68,7 @@ func TestGetMusicDirectory(t *testing.T) {
mockMediaFileRepo.SetData(`[{"Id":"3","Title":"Cangote","AlbumId":"A"}]`, 1)
_, w := Get(AddParams("/rest/getMusicDirectory.view", "id=A"), "TestGetMusicDirectory")
So(w.Body, ShouldContainJSON, `"child":[{"id":"3","isDir":false,"title":"Cangote"}]`)
So(w.Body, ShouldContainJSON, `"child":[{"id":"3","isDir":false,"parent":"A","title":"Cangote"}]`)
})
Reset(func() {
mockArtistRepo.SetData("[]", 0)

View File

@ -14,7 +14,8 @@ type Subsonic struct {
MusicFolders *MusicFolders `xml:"musicFolders,omitempty" json:"musicFolders,omitempty"`
Indexes *Indexes `xml:"indexes,omitempty" json:"indexes,omitempty"`
Directory *Directory `xml:"directory,omitempty" json:"directory,omitempty"`
User *User `xml:"user,omitempty" json:"user,omitempty"`
User *User `xml:"user,omitempty" json:"user,omitempty"`
AlbumList *AlbumList `xml:"albumList,omitempty" json:"albumList,omitempty"`
}
type JsonWrapper struct {
@ -57,6 +58,7 @@ type Indexes struct {
type Child struct {
Id string `xml:"id,attr" json:"id"`
Parent string `xml:"parent,attr,omitempty" json:"parent,omitempty"`
IsDir bool `xml:"isDir,attr" json:"isDir"`
Title string `xml:"title,attr" json:"title"`
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
@ -81,22 +83,26 @@ type Directory struct {
Name string `xml:"name,attr" json:"name"`
}
type AlbumList struct {
Album []Child `xml:"album" json:"album,omitempty"`
}
type User struct {
Username string `xml:"username,attr" json:"username"`
Email string `xml:"email,attr,omitempty" json:"email,omitempty"`
ScrobblingEnabled bool `xml:"scrobblingEnabled,attr" json:"scrobblingEnabled"`
MaxBitRate int `xml:"maxBitRate,attr,omitempty" json:"maxBitRate,omitempty"`
AdminRole bool `xml:"adminRole,attr" json:"adminRole"`
SettingsRole bool `xml:"settingsRole,attr" json:"settingsRole"`
DownloadRole bool `xml:"downloadRole,attr" json:"downloadRole"`
UploadRole bool `xml:"uploadRole,attr" json:"uploadRole"`
PlaylistRole bool `xml:"playlistRole,attr" json:"playlistRole"`
CoverArtRole bool `xml:"coverArtRole,attr" json:"coverArtRole"`
CommentRole bool `xml:"commentRole,attr" json:"commentRole"`
PodcastRole bool `xml:"podcastRole,attr" json:"podcastRole"`
StreamRole bool `xml:"streamRole,attr" json:"streamRole"`
JukeboxRole bool `xml:"jukeboxRole,attr" json:"jukeboxRole"`
ShareRole bool `xml:"shareRole,attr" json:"shareRole"`
VideoConversionRole bool `xml:"videoConversionRole,attr" json:"videoConversionRole"`
Folder []int `xml:"folder,omitempty" json:"folder,omitempty"`
ScrobblingEnabled bool `xml:"scrobblingEnabled,attr" json:"scrobblingEnabled"`
MaxBitRate int `xml:"maxBitRate,attr,omitempty" json:"maxBitRate,omitempty"`
AdminRole bool `xml:"adminRole,attr" json:"adminRole"`
SettingsRole bool `xml:"settingsRole,attr" json:"settingsRole"`
DownloadRole bool `xml:"downloadRole,attr" json:"downloadRole"`
UploadRole bool `xml:"uploadRole,attr" json:"uploadRole"`
PlaylistRole bool `xml:"playlistRole,attr" json:"playlistRole"`
CoverArtRole bool `xml:"coverArtRole,attr" json:"coverArtRole"`
CommentRole bool `xml:"commentRole,attr" json:"commentRole"`
PodcastRole bool `xml:"podcastRole,attr" json:"podcastRole"`
StreamRole bool `xml:"streamRole,attr" json:"streamRole"`
JukeboxRole bool `xml:"jukeboxRole,attr" json:"jukeboxRole"`
ShareRole bool `xml:"shareRole,attr" json:"shareRole"`
VideoConversionRole bool `xml:"videoConversionRole,attr" json:"videoConversionRole"`
Folder []int `xml:"folder,omitempty" json:"folder,omitempty"`
}

View File

@ -84,6 +84,27 @@ func TestSubsonicResponses(t *testing.T) {
})
})
Convey("Child", func() {
response.Directory = &Directory{Id: "1", Name: "N"}
Convey("With all data", func() {
child := make([]Child, 1)
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
child[0] = Child{
Id: "1", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1,
Year: 1985, Genre: "Rock", CoverArt: "1", Size: "8421341", ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t,
}
response.Directory.Child = child
Convey("XML", func() {
So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><directory id="1" name="N"><child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320"></child></directory></subsonic-response>`)
})
Convey("JSON", func() {
So(response, ShouldMatchJSON, `{"directory":{"child":[{"album":"album","artist":"artist","bitRate":320,"contentType":"audio/flac","coverArt":"1","duration":146,"genre":"Rock","id":"1","isDir":true,"size":"8421341","starred":"2016-03-02T20:30:00Z","suffix":"flac","title":"title","track":1,"transcodedContentType":"audio/mpeg","transcodedSuffix":"mp3","year":1985}],"id":"1","name":"N"},"status":"ok","version":"1.0.0"}`)
})
})
})
Convey("Directory", func() {
response.Directory = &Directory{Id: "1", Name: "N"}
Convey("Without data", func() {
@ -105,21 +126,27 @@ func TestSubsonicResponses(t *testing.T) {
So(response, ShouldMatchJSON, `{"directory":{"child":[{"id":"1","isDir":false,"title":"title"}],"id":"1","name":"N"},"status":"ok","version":"1.0.0"}`)
})
})
Convey("With all data", func() {
child := make([]Child, 1)
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
child[0] = Child{
Id: "1", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1,
Year: 1985, Genre: "Rock", CoverArt: "1", Size: "8421341", ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t,
}
response.Directory.Child = child
})
Convey("AlbumList", func() {
response.AlbumList = &AlbumList{}
Convey("Without data", func() {
Convey("XML", func() {
So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><directory id="1" name="N"><child id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320"></child></directory></subsonic-response>`)
So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><albumList></albumList></subsonic-response>`)
})
Convey("JSON", func() {
So(response, ShouldMatchJSON, `{"directory":{"child":[{"album":"album","artist":"artist","bitRate":320,"contentType":"audio/flac","coverArt":"1","duration":146,"genre":"Rock","id":"1","isDir":true,"size":"8421341","starred":"2016-03-02T20:30:00Z","suffix":"flac","title":"title","track":1,"transcodedContentType":"audio/mpeg","transcodedSuffix":"mp3","year":1985}],"id":"1","name":"N"},"status":"ok","version":"1.0.0"}`)
So(response, ShouldMatchJSON, `{"albumList":{},"status":"ok","version":"1.0.0"}`)
})
})
Convey("With just required data", func() {
child := make([]Child, 1)
child[0] = Child{Id: "1", Title: "title", IsDir: false}
response.AlbumList.Album = child
Convey("XML", func() {
So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><albumList><album id="1" isDir="false" title="title"></album></albumList></subsonic-response>`)
})
Convey("JSON", func() {
So(response, ShouldMatchJSON, `{"albumList":{"album":[{"id":"1","isDir":false,"title":"title"}]},"status":"ok","version":"1.0.0"}`)
})
})
})

View File

@ -25,6 +25,7 @@ func mapEndpoints() {
beego.NSRouter("/stream.view", &api.StreamController{}, "*:Get"),
beego.NSRouter("/download.view", &api.StreamController{}, "*:Get"),
beego.NSRouter("/getUser.view", &api.UsersController{}, "*:GetUser"),
beego.NSRouter("/getAlbumList.view", &api.GetAlbumListController{}, "*:Get"),
)
beego.AddNamespace(ns)

View File

@ -1,5 +1,7 @@
package domain
import "time"
type Album struct {
Id string
Name string
@ -13,6 +15,8 @@ type Album struct {
Starred bool
Rating int
Genre string
CreatedAt time.Time
UpdatedAt time.Time
}
type Albums []Album
@ -22,4 +26,5 @@ type AlbumRepository interface {
Put(m *Album) error
Get(id string) (*Album, error)
FindByArtist(artistId string) (Albums, error)
GetAll(QueryOptions) (Albums, error)
}

View File

@ -29,8 +29,14 @@ func (r *albumRepository) Get(id string) (*domain.Album, error) {
func (r *albumRepository) FindByArtist(artistId string) (domain.Albums, error) {
var as = make(domain.Albums, 0)
err := r.loadChildren("artist", artistId, &as, domain.QueryOptions{SortBy:"Year"})
err := r.loadChildren("artist", artistId, &as, domain.QueryOptions{SortBy: "Year"})
return as, err
}
var _ domain.AlbumRepository = (*albumRepository)(nil)
func (r *albumRepository) GetAll(options domain.QueryOptions) (domain.Albums, error) {
var as = make(domain.Albums, 0)
err := r.loadAll(&as, options)
return as, err
}
var _ domain.AlbumRepository = (*albumRepository)(nil)

View File

@ -3,14 +3,14 @@ package scanner
import (
"fmt"
"github.com/astaxie/beego"
"github.com/deluan/gosonic/consts"
"github.com/deluan/gosonic/domain"
"github.com/deluan/gosonic/persistence"
"time"
"github.com/dhowden/tag"
"github.com/deluan/gosonic/utils"
"github.com/deluan/gosonic/consts"
"github.com/dhowden/tag"
"os"
"strings"
"time"
)
type Scanner interface {
@ -22,14 +22,13 @@ type tempIndex map[string]domain.ArtistInfo
func StartImport() {
go func() {
i := &Importer{
scanner: &ItunesScanner{},
mediaFolder: beego.AppConfig.String("musicFolder"),
mfRepo: persistence.NewMediaFileRepository(),
albumRepo:persistence.NewAlbumRepository(),
artistRepo: persistence.NewArtistRepository(),
idxRepo: persistence.NewArtistIndexRepository(),
scanner: &ItunesScanner{},
mediaFolder: beego.AppConfig.String("musicFolder"),
mfRepo: persistence.NewMediaFileRepository(),
albumRepo: persistence.NewAlbumRepository(),
artistRepo: persistence.NewArtistRepository(),
idxRepo: persistence.NewArtistIndexRepository(),
propertyRepo: persistence.NewPropertyRepository(),
}
i.Run()
}()
@ -134,6 +133,8 @@ func (i *Importer) parseTrack(t *Track) (*domain.MediaFile, *domain.Album, *doma
Genre: t.Genre,
Artist: t.Artist,
AlbumArtist: t.AlbumArtist,
CreatedAt: t.CreatedAt, // TODO Collect all songs for an album first
UpdatedAt: t.UpdatedAt,
}
if mf.HasCoverArt {

View File

@ -2,12 +2,12 @@ package tests
import (
"bytes"
"crypto/md5"
"encoding/json"
"encoding/xml"
"fmt"
"github.com/deluan/gosonic/api/responses"
. "github.com/smartystreets/goconvey/convey"
"crypto/md5"
)
func ShouldMatchXML(actual interface{}, expected ...interface{}) string {
@ -49,6 +49,16 @@ func ShouldMatchMD5(actual interface{}, expected ...interface{}) string {
return ShouldEqual(a, expected[0].(string))
}
func ShouldBeAValid(actual interface{}, expected ...interface{}) string {
v := responses.Subsonic{}
err := json.Unmarshal(actual.(*bytes.Buffer).Bytes(), &v)
if err != nil {
return fmt.Sprintf("Malformed response: %v", err)
}
return ""
}
func UnindentJSON(j []byte) string {
var m = make(map[string]interface{})
json.Unmarshal(j, &m)

View File

@ -13,8 +13,10 @@ func CreateMockAlbumRepo() *MockAlbum {
type MockAlbum struct {
domain.AlbumRepository
data map[string]*domain.Album
err bool
data map[string]*domain.Album
all domain.Albums
err bool
Options domain.QueryOptions
}
func (m *MockAlbum) SetError(err bool) {
@ -23,12 +25,12 @@ func (m *MockAlbum) SetError(err bool) {
func (m *MockAlbum) SetData(j string, size int) {
m.data = make(map[string]*domain.Album)
var l = make([]domain.Album, size)
err := json.Unmarshal([]byte(j), &l)
m.all = make(domain.Albums, size)
err := json.Unmarshal([]byte(j), &m.all)
if err != nil {
fmt.Println("ERROR: ", err)
}
for _, a := range l {
for _, a := range m.all {
m.data[a.Id] = &a
}
}
@ -48,6 +50,14 @@ func (m *MockAlbum) Get(id string) (*domain.Album, error) {
return m.data[id], nil
}
func (m *MockAlbum) GetAll(qo domain.QueryOptions) (domain.Albums, error) {
m.Options = qo
if m.err {
return nil, errors.New("Error!")
}
return m.all, nil
}
func (m *MockAlbum) FindByArtist(artistId string) (domain.Albums, error) {
if m.err {
return nil, errors.New("Error!")