diff --git a/api/get_album_list.go b/api/get_album_list.go
new file mode 100644
index 000000000..d3dafc170
--- /dev/null
+++ b/api/get_album_list.go
@@ -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)
+}
diff --git a/api/get_album_list_test.go b/api/get_album_list_test.go
new file mode 100644
index 000000000..acc72d5e6
--- /dev/null
+++ b/api/get_album_list_test.go
@@ -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)
+ })
+ })
+}
diff --git a/api/get_music_directory.go b/api/get_music_directory.go
index e28e755e7..02a68c947 100644
--- a/api/get_music_directory.go
+++ b/api/get_music_directory.go
@@ -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
diff --git a/api/get_music_directory_test.go b/api/get_music_directory_test.go
index 1b4ee90c9..8f1f1a868 100644
--- a/api/get_music_directory_test.go
+++ b/api/get_music_directory_test.go
@@ -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)
diff --git a/api/responses/responses.go b/api/responses/responses.go
index fe701dbf2..1b6355602 100644
--- a/api/responses/responses.go
+++ b/api/responses/responses.go
@@ -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"`
}
diff --git a/api/responses/responses_test.go b/api/responses/responses_test.go
index 7e4e4abd3..af7a68670 100644
--- a/api/responses/responses_test.go
+++ b/api/responses/responses_test.go
@@ -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, ``)
+ })
+ 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, ``)
+ So(response, ShouldMatchXML, ``)
})
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, ``)
+ })
+ Convey("JSON", func() {
+ So(response, ShouldMatchJSON, `{"albumList":{"album":[{"id":"1","isDir":false,"title":"title"}]},"status":"ok","version":"1.0.0"}`)
})
})
})
diff --git a/conf/router.go b/conf/router.go
index f5c93fe15..f58b2fc25 100644
--- a/conf/router.go
+++ b/conf/router.go
@@ -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)
diff --git a/domain/album.go b/domain/album.go
index 2652893e5..0f14560a8 100644
--- a/domain/album.go
+++ b/domain/album.go
@@ -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)
}
diff --git a/persistence/album_repository.go b/persistence/album_repository.go
index f99e288f0..d28c513a7 100644
--- a/persistence/album_repository.go
+++ b/persistence/album_repository.go
@@ -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)
\ No newline at end of file
+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)
diff --git a/scanner/importer.go b/scanner/importer.go
index e7a4878a6..45215fd25 100644
--- a/scanner/importer.go
+++ b/scanner/importer.go
@@ -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 {
diff --git a/tests/matchers.go b/tests/matchers.go
index 1d142588a..33009d2f0 100644
--- a/tests/matchers.go
+++ b/tests/matchers.go
@@ -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)
diff --git a/tests/mocks/mock_album_repo.go b/tests/mocks/mock_album_repo.go
index 2a1feb973..f3c923f48 100644
--- a/tests/mocks/mock_album_repo.go
+++ b/tests/mocks/mock_album_repo.go
@@ -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!")