diff --git a/db/db.go b/db/db.go index d572f6a..b45350b 100644 --- a/db/db.go +++ b/db/db.go @@ -44,6 +44,8 @@ func New(path string) (*DB, error) { model.Setting{}, model.Play{}, model.Album{}, + model.Playlist{}, + model.PlaylistItem{}, ) // TODO: don't log if user already exists db.FirstOrCreate(&model.User{}, model.User{ diff --git a/model/model.go b/model/model.go index a98303f..6497c77 100644 --- a/model/model.go +++ b/model/model.go @@ -108,3 +108,21 @@ func (a *Album) IndexRightPath() string { } return a.RightPath } + +type Playlist struct { + ID int `gorm:"primary_key"` + UpdatedAt time.Time + ModifiedAt time.Time + User *User + UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` + Name string + Comment string +} + +type PlaylistItem struct { + ID int `gorm:"primary_key"` + Playlist Playlist + PlaylistID int `sql:"default: null; type:int REFERENCES playlists(id) ON DELETE CASCADE"` + Track Track + TrackID int `sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"` +} diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index a9e0ba3..4665bdc 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -3,9 +3,12 @@ package ctrlsubsonic import ( "log" "net/http" + "strconv" "time" "unicode" + "github.com/jinzhu/gorm" + "senan.xyz/g/gonic/model" "senan.xyz/g/gonic/scanner" "senan.xyz/g/gonic/server/ctrlsubsonic/spec" @@ -114,3 +117,94 @@ func (c *Controller) ServeGetUser(r *http.Request) *spec.Response { func (c *Controller) ServeNotFound(r *http.Request) *spec.Response { return spec.NewError(70, "view not found") } + +func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response { + user := r.Context().Value(key.User).(*model.User) + var playlists []*model.Playlist + c.DB. + Where("user_id = ?", user.ID). + Find(&playlists) + sub := spec.NewResponse() + sub.Playlists = &spec.Playlists{ + List: make([]*spec.Playlist, len(playlists)), + } + for i, playlist := range playlists { + sub.Playlists.List[i] = spec.NewPlaylist(playlist) + sub.Playlists.List[i].Owner = user.Name + } + return sub +} + +func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response { + playlistID, err := parsing.GetIntParam(r, "id") + if err != nil { + return spec.NewError(10, "please provide an `id` parameter") + } + playlist := model.Playlist{} + err = c.DB. + Where("id = ?", playlistID). + Find(&playlist). + Error + if gorm.IsRecordNotFoundError(err) { + return spec.NewError(70, "playlist with id `%d` not found", playlistID) + } + var tracks []*model.Track + c.DB. + Joins(` + JOIN playlist_items + ON playlist_items.track_id = tracks.id + `). + Where("playlist_items.playlist_id = ?", playlistID). + Group("tracks.id"). + Preload("Album"). + Find(&tracks) + user := r.Context().Value(key.User).(*model.User) + sub := spec.NewResponse() + sub.Playlist = spec.NewPlaylist(&playlist) + sub.Playlist.Owner = user.Name + sub.Playlist.List = make([]*spec.TrackChild, len(tracks)) + for i, track := range tracks { + sub.Playlist.List[i] = spec.NewTCTrackByFolder(track, track.Album) + } + return sub +} + +func (c *Controller) ServeCreatePlaylist(r *http.Request) *spec.Response { + user := r.Context().Value(key.User).(*model.User) + playlist := &model.Playlist{} + c.DB. + Select("id"). + Where("id = ?", parsing.GetIntParamOr(r, "id", 0)). + First(playlist) + playlist.UserID = user.ID + playlist.Name = parsing.GetStrParam(r, "name") + c.DB.Save(playlist) + sub := spec.NewResponse() + tracks, ok := r.URL.Query()["songId"] + if !ok { + return sub + } + for _, trackIDStr := range tracks { + trackID, err := strconv.Atoi(trackIDStr) + if err != nil { + continue + } + c.DB.Save(&model.PlaylistItem{ + PlaylistID: playlist.ID, + TrackID: trackID, + }) + } + return sub +} + +func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response { + // user := r.Context().Value(key.User).(*model.User) + sub := spec.NewResponse() + return sub +} + +func (c *Controller) ServeDeletePlaylist(r *http.Request) *spec.Response { + // user := r.Context().Value(key.User).(*model.User) + sub := spec.NewResponse() + return sub +} diff --git a/server/ctrlsubsonic/spec/construct.go b/server/ctrlsubsonic/spec/construct.go new file mode 100644 index 0000000..9850158 --- /dev/null +++ b/server/ctrlsubsonic/spec/construct.go @@ -0,0 +1,11 @@ +package spec + +import "senan.xyz/g/gonic/model" + +func NewPlaylist(p *model.Playlist) *Playlist { + return &Playlist{ + ID: p.ID, + Name: p.Name, + Comment: p.Comment, + } +} diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index 73c01d0..1bce3d7 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -30,6 +30,8 @@ type Response struct { SearchResultTwo *SearchResultTwo `xml:"searchResult2" json:"searchResult2,omitempty"` SearchResultThree *SearchResultThree `xml:"searchResult3" json:"searchResult3,omitempty"` User *User `xml:"user" json:"user,omitempty"` + Playlists *Playlists `xml:"playlists" json:"playlists,omitempty"` + Playlist *Playlist `xml:"playlist" json:"playlist,omitempty"` } func NewResponse() *Response { @@ -196,3 +198,17 @@ type User struct { VideoConversionRole bool `xml:"videoConversionRole,attr" json:"videoConversionRole"` Folder []int `xml:"folder,attr" json:"folder"` } + +type Playlists struct { + List []*Playlist `xml:"playlist" json:"playlist,omitempty"` +} + +type Playlist struct { + ID int `xml:"id,attr" json:"id,omitempty"` + Name string `xml:"name,attr" json:"name,omitempty"` + Comment string `xml:"comment,attr" json:"comment,omitempty"` + Owner string `xml:"owner,attr" json:"owner,omitempty"` + SongCount string `xml:"songCount,attr" json:"songCount,omitempty"` + Created string `xml:"created,attr" json:"created,omitempty"` + List []*TrackChild `xml:"entry" json:"entry,omitempty"` +} diff --git a/server/server.go b/server/server.go index f7ed694..57b8332 100644 --- a/server/server.go +++ b/server/server.go @@ -126,6 +126,11 @@ func (s *Server) SetupSubsonic() error { rout.Handle("/scrobble{_:(?:\\.view)?}", ctrl.H(ctrl.ServeScrobble)) rout.Handle("/startScan{_:(?:\\.view)?}", ctrl.H(ctrl.ServeStartScan)) rout.Handle("/getUser{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetUser)) + rout.Handle("/getPlaylists{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlaylists)) + rout.Handle("/getPlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlaylist)) + rout.Handle("/createPlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreatePlaylist)) + rout.Handle("/updatePlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeUpdatePlaylist)) + rout.Handle("/deletePlaylist{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePlaylist)) // // begin raw rout.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))