From 7161325716d9d6fc8d39a47c98cd0b660e4542be Mon Sep 17 00:00:00 2001
From: Deluan <github@deluan.com>
Date: Wed, 9 Mar 2016 10:09:15 -0500
Subject: [PATCH] Initial wiring for getPlaylists endpoint

---
 api/playlists.go                   | 34 ++++++++++++++++++
 api/responses/responses.go         | 28 ++++++++++++++-
 api/responses/responses_test.go    | 30 ++++++++++++++--
 conf/inject_definitions.go         |  2 ++
 conf/router.go                     |  1 +
 domain/playlist.go                 | 17 +++++++++
 engine/playlists.go                | 21 ++++++++++++
 persistence/playlist_repository.go | 55 ++++++++++++++++++++++++++++++
 8 files changed, 185 insertions(+), 3 deletions(-)
 create mode 100644 api/playlists.go
 create mode 100644 domain/playlist.go
 create mode 100644 engine/playlists.go
 create mode 100644 persistence/playlist_repository.go

diff --git a/api/playlists.go b/api/playlists.go
new file mode 100644
index 000000000..9bd4d64de
--- /dev/null
+++ b/api/playlists.go
@@ -0,0 +1,34 @@
+package api
+
+import (
+	"github.com/astaxie/beego"
+	"github.com/deluan/gosonic/api/responses"
+	"github.com/deluan/gosonic/engine"
+	"github.com/deluan/gosonic/utils"
+	"github.com/karlkfi/inject"
+)
+
+type PlaylistsController struct {
+	BaseAPIController
+	pls engine.Playlists
+}
+
+func (c *PlaylistsController) Prepare() {
+	inject.ExtractAssignable(utils.Graph, &c.pls)
+}
+
+func (c *PlaylistsController) GetAll() {
+	allPls, err := c.pls.GetAll()
+	if err != nil {
+		beego.Error(err)
+		c.SendError(responses.ERROR_GENERIC, "Internal error")
+	}
+	playlists := make([]responses.Playlist, len(*allPls))
+	for i, f := range *allPls {
+		playlists[i].Id = f.Id
+		playlists[i].Name = f.Name
+	}
+	response := c.NewEmpty()
+	response.Playlists = &responses.Playlists{Playlist: playlists}
+	c.SendResponse(response)
+}
diff --git a/api/responses/responses.go b/api/responses/responses.go
index 1b6355602..6f53b1fe3 100644
--- a/api/responses/responses.go
+++ b/api/responses/responses.go
@@ -14,8 +14,9 @@ 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"`
+	Playlists    *Playlists    `xml:"playlists,omitempty"                           json:"playlists,omitempty"`
 }
 
 type JsonWrapper struct {
@@ -87,6 +88,31 @@ type AlbumList struct {
 	Album []Child `xml:"album"                         json:"album,omitempty"`
 }
 
+type Playlist struct {
+	Id   string `xml:"id,attr"                                 json:"id"`
+	Name string `xml:"name,attr"                               json:"name"`
+	/*
+		<xs:sequence>
+		    <xs:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0-->
+		</xs:sequence>
+		<xs:attribute name="id" type="xs:string" use="required"/>
+		<xs:attribute name="name" type="xs:string" use="required"/>
+		<xs:attribute name="comment" type="xs:string" use="optional"/>   <!--Added in 1.8.0-->
+		<xs:attribute name="owner" type="xs:string" use="optional"/>     <!--Added in 1.8.0-->
+		<xs:attribute name="public" type="xs:boolean" use="optional"/>   <!--Added in 1.8.0-->
+		<xs:attribute name="songCount" type="xs:int" use="required"/>    <!--Added in 1.8.0-->
+		<xs:attribute name="duration" type="xs:int" use="required"/>     <!--Added in 1.8.0-->
+		<xs:attribute name="created" type="xs:dateTime" use="required"/> <!--Added in 1.8.0-->
+		<xs:attribute name="changed" type="xs:dateTime" use="required"/> <!--Added in 1.13.0-->
+		<xs:attribute name="coverArt" type="xs:string" use="optional"/>  <!--Added in 1.11.0-->
+
+	*/
+}
+
+type Playlists struct {
+	Playlist []Playlist `xml:"playlist"                         json:"playlist,omitempty"`
+}
+
 type User struct {
 	Username            string `xml:"username,attr" json:"username"`
 	Email               string `xml:"email,attr,omitempty" json:"email,omitempty"`
diff --git a/api/responses/responses_test.go b/api/responses/responses_test.go
index af7a68670..da622812d 100644
--- a/api/responses/responses_test.go
+++ b/api/responses/responses_test.go
@@ -1,11 +1,12 @@
 package responses_test
 
 import (
+	"testing"
+	"time"
+
 	. "github.com/deluan/gosonic/api/responses"
 	. "github.com/deluan/gosonic/tests"
 	. "github.com/smartystreets/goconvey/convey"
-	"testing"
-	"time"
 )
 
 func TestSubsonicResponses(t *testing.T) {
@@ -172,6 +173,31 @@ func TestSubsonicResponses(t *testing.T) {
 				})
 			})
 		})
+		Convey("Playlists", func() {
+			response.Playlists = &Playlists{}
+
+			Convey("Without data", func() {
+				Convey("XML", func() {
+					So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><playlists></playlists></subsonic-response>`)
+				})
+				Convey("JSON", func() {
+					So(response, ShouldMatchJSON, `{"playlists":{},"status":"ok","version":"1.0.0"}`)
+				})
+			})
+			Convey("With data", func() {
+				pls := make([]Playlist, 2)
+				pls[0] = Playlist{Id: "111", Name: "aaa"}
+				pls[1] = Playlist{Id: "222", Name: "bbb"}
+				response.Playlists.Playlist = pls
+
+				Convey("XML", func() {
+					So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><playlists><playlist id="111" name="aaa"></playlist><playlist id="222" name="bbb"></playlist></playlists></subsonic-response>`)
+				})
+				Convey("JSON", func() {
+					So(response, ShouldMatchJSON, `{"playlists":{"playlist":[{"id":"111","name":"aaa"},{"id":"222","name":"bbb"}]},"status":"ok","version":"1.0.0"}`)
+				})
+			})
+		})
 
 		Reset(func() {
 			response = &Subsonic{Status: "ok", Version: "1.0.0"}
diff --git a/conf/inject_definitions.go b/conf/inject_definitions.go
index 131e46fca..2ef02d164 100644
--- a/conf/inject_definitions.go
+++ b/conf/inject_definitions.go
@@ -14,10 +14,12 @@ func init() {
 	utils.DefineSingleton(new(domain.ArtistRepository), persistence.NewArtistRepository)
 	utils.DefineSingleton(new(domain.AlbumRepository), persistence.NewAlbumRepository)
 	utils.DefineSingleton(new(domain.MediaFileRepository), persistence.NewMediaFileRepository)
+	utils.DefineSingleton(new(domain.PlaylistRepository), persistence.NewPlaylistRepository)
 
 	// Engine (Use cases)
 	utils.DefineSingleton(new(engine.PropertyRepository), persistence.NewPropertyRepository)
 	utils.DefineSingleton(new(engine.Browser), engine.NewBrowser)
 	utils.DefineSingleton(new(engine.ListGenerator), engine.NewListGenerator)
 	utils.DefineSingleton(new(engine.Cover), engine.NewCover)
+	utils.DefineSingleton(new(engine.Playlists), engine.NewPlaylists)
 }
diff --git a/conf/router.go b/conf/router.go
index 48b436ab7..ad38c838f 100644
--- a/conf/router.go
+++ b/conf/router.go
@@ -26,6 +26,7 @@ func mapEndpoints() {
 		beego.NSRouter("/download.view", &api.StreamController{}, "*:Download"),
 		beego.NSRouter("/getUser.view", &api.UsersController{}, "*:GetUser"),
 		beego.NSRouter("/getAlbumList.view", &api.GetAlbumListController{}, "*:Get"),
+		beego.NSRouter("/getPlaylists.view", &api.PlaylistsController{}, "*:GetAll"),
 	)
 	beego.AddNamespace(ns)
 
diff --git a/domain/playlist.go b/domain/playlist.go
new file mode 100644
index 000000000..7dc3c0215
--- /dev/null
+++ b/domain/playlist.go
@@ -0,0 +1,17 @@
+package domain
+
+type Playlist struct {
+	Id     string
+	Name   string
+	Tracks []string
+}
+
+type PlaylistRepository interface {
+	BaseRepository
+	Put(m *Playlist) error
+	Get(id string) (*Playlist, error)
+	GetAll(options QueryOptions) (*Playlists, error)
+	PurgeInactive(active *Playlists) error
+}
+
+type Playlists []Playlist
diff --git a/engine/playlists.go b/engine/playlists.go
new file mode 100644
index 000000000..ee23536f4
--- /dev/null
+++ b/engine/playlists.go
@@ -0,0 +1,21 @@
+package engine
+
+import (
+	"github.com/deluan/gosonic/domain"
+)
+
+type Playlists interface {
+	GetAll() (*domain.Playlists, error)
+}
+
+type playlists struct {
+	plsRepo domain.PlaylistRepository
+}
+
+func NewPlaylists(pr domain.PlaylistRepository) Playlists {
+	return playlists{pr}
+}
+
+func (p playlists) GetAll() (*domain.Playlists, error) {
+	return p.plsRepo.GetAll(domain.QueryOptions{})
+}
diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go
new file mode 100644
index 000000000..0617c8581
--- /dev/null
+++ b/persistence/playlist_repository.go
@@ -0,0 +1,55 @@
+package persistence
+
+import (
+	"errors"
+
+	"github.com/deluan/gosonic/domain"
+)
+
+type playlistRepository struct {
+	ledisRepository
+}
+
+func NewPlaylistRepository() domain.PlaylistRepository {
+	r := &playlistRepository{}
+	r.init("playlist", &domain.Playlist{})
+	return r
+}
+
+func (r *playlistRepository) Put(m *domain.Playlist) error {
+	if m.Id == "" {
+		return errors.New("Playlist Id is not set")
+	}
+	return r.saveOrUpdate(m.Id, m)
+}
+
+func (r *playlistRepository) Get(id string) (*domain.Playlist, error) {
+	var rec interface{}
+	rec, err := r.readEntity(id)
+	return rec.(*domain.Playlist), err
+}
+
+func (r *playlistRepository) GetAll(options domain.QueryOptions) (*domain.Playlists, error) {
+	var as = make(domain.Playlists, 0)
+	err := r.loadAll(&as, options)
+	return &as, err
+}
+
+func (r *playlistRepository) PurgeInactive(active *domain.Playlists) error {
+	currentIds, err := r.getAllIds()
+	if err != nil {
+		return err
+	}
+	for _, a := range *active {
+		currentIds[a.Id] = false
+	}
+	inactiveIds := make(map[string]bool)
+	for id, inactive := range currentIds {
+		if inactive {
+			inactiveIds[id] = true
+		}
+	}
+	return r.DeleteAll(inactiveIds)
+}
+
+var _ domain.PlaylistRepository = (*playlistRepository)(nil)