mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-07 06:27:36 +03:00
Playlists working
This commit is contained in:
parent
4bb4fc0cb8
commit
2214e4bd4f
8 changed files with 179 additions and 41 deletions
|
@ -22,9 +22,11 @@ The project's main goals are:
|
||||||
* Learning Go ;) [](https://golang.org)
|
* Learning Go ;) [](https://golang.org)
|
||||||
|
|
||||||
|
|
||||||
### Supported Subsonic API version:
|
### Supported Subsonic API version
|
||||||
|
|
||||||
[(Almost) up to date compatibility chart](https://github.com/deluan/gosonic/wiki/Compatibility)
|
I'm currently trying to implement all functionality from API v1.2.0, with some exceptions.
|
||||||
|
|
||||||
|
Check the (almost) up to date [compatibility chart](https://github.com/deluan/gosonic/wiki/Compatibility) for what is working.
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
|
|
||||||
|
|
|
@ -32,3 +32,50 @@ func (c *PlaylistsController) GetAll() {
|
||||||
response.Playlists = &responses.Playlists{Playlist: playlists}
|
response.Playlists = &responses.Playlists{Playlist: playlists}
|
||||||
c.SendResponse(response)
|
c.SendResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *PlaylistsController) Get() {
|
||||||
|
id := c.RequiredParamString("id", "id parameter required")
|
||||||
|
|
||||||
|
pinfo, err := c.pls.Get(id)
|
||||||
|
switch {
|
||||||
|
case err == engine.DataNotFound:
|
||||||
|
beego.Error(err, "Id:", id)
|
||||||
|
c.SendError(responses.ERROR_DATA_NOT_FOUND, "Directory not found")
|
||||||
|
case err != nil:
|
||||||
|
beego.Error(err)
|
||||||
|
c.SendError(responses.ERROR_GENERIC, "Internal Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
response := c.NewEmpty()
|
||||||
|
response.Playlist = c.buildPlaylist(pinfo)
|
||||||
|
c.SendResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PlaylistsController) buildPlaylist(d *engine.PlaylistInfo) *responses.PlaylistWithSongs {
|
||||||
|
pls := &responses.PlaylistWithSongs{}
|
||||||
|
pls.Id = d.Id
|
||||||
|
pls.Name = d.Name
|
||||||
|
|
||||||
|
pls.Entry = make([]responses.Child, len(d.Entries))
|
||||||
|
for i, child := range d.Entries {
|
||||||
|
pls.Entry[i].Id = child.Id
|
||||||
|
pls.Entry[i].Title = child.Title
|
||||||
|
pls.Entry[i].IsDir = child.IsDir
|
||||||
|
pls.Entry[i].Parent = child.Parent
|
||||||
|
pls.Entry[i].Album = child.Album
|
||||||
|
pls.Entry[i].Year = child.Year
|
||||||
|
pls.Entry[i].Artist = child.Artist
|
||||||
|
pls.Entry[i].Genre = child.Genre
|
||||||
|
pls.Entry[i].CoverArt = child.CoverArt
|
||||||
|
pls.Entry[i].Track = child.Track
|
||||||
|
pls.Entry[i].Duration = child.Duration
|
||||||
|
pls.Entry[i].Size = child.Size
|
||||||
|
pls.Entry[i].Suffix = child.Suffix
|
||||||
|
pls.Entry[i].BitRate = child.BitRate
|
||||||
|
pls.Entry[i].ContentType = child.ContentType
|
||||||
|
if !child.Starred.IsZero() {
|
||||||
|
pls.Entry[i].Starred = &child.Starred
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pls
|
||||||
|
}
|
||||||
|
|
|
@ -6,17 +6,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Subsonic struct {
|
type Subsonic struct {
|
||||||
XMLName xml.Name `xml:"http://subsonic.org/restapi subsonic-response" json:"-"`
|
XMLName xml.Name `xml:"http://subsonic.org/restapi subsonic-response" json:"-"`
|
||||||
Status string `xml:"status,attr" json:"status"`
|
Status string `xml:"status,attr" json:"status"`
|
||||||
Version string `xml:"version,attr" json:"version"`
|
Version string `xml:"version,attr" json:"version"`
|
||||||
Error *Error `xml:"error,omitempty" json:"error,omitempty"`
|
Error *Error `xml:"error,omitempty" json:"error,omitempty"`
|
||||||
License *License `xml:"license,omitempty" json:"license,omitempty"`
|
License *License `xml:"license,omitempty" json:"license,omitempty"`
|
||||||
MusicFolders *MusicFolders `xml:"musicFolders,omitempty" json:"musicFolders,omitempty"`
|
MusicFolders *MusicFolders `xml:"musicFolders,omitempty" json:"musicFolders,omitempty"`
|
||||||
Indexes *Indexes `xml:"indexes,omitempty" json:"indexes,omitempty"`
|
Indexes *Indexes `xml:"indexes,omitempty" json:"indexes,omitempty"`
|
||||||
Directory *Directory `xml:"directory,omitempty" json:"directory,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"`
|
AlbumList *AlbumList `xml:"albumList,omitempty" json:"albumList,omitempty"`
|
||||||
Playlists *Playlists `xml:"playlists,omitempty" json:"playlists,omitempty"`
|
Playlists *Playlists `xml:"playlists,omitempty" json:"playlists,omitempty"`
|
||||||
|
Playlist *PlaylistWithSongs `xml:"playlist,omitempty" json:"playlist,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JsonWrapper struct {
|
type JsonWrapper struct {
|
||||||
|
@ -111,6 +112,11 @@ type Playlists struct {
|
||||||
Playlist []Playlist `xml:"playlist" json:"playlist,omitempty"`
|
Playlist []Playlist `xml:"playlist" json:"playlist,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaylistWithSongs struct {
|
||||||
|
Playlist
|
||||||
|
Entry []Child `xml:"entry" json:"entry,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Username string `xml:"username,attr" json:"username"`
|
Username string `xml:"username,attr" json:"username"`
|
||||||
Email string `xml:"email,attr,omitempty" json:"email,omitempty"`
|
Email string `xml:"email,attr,omitempty" json:"email,omitempty"`
|
||||||
|
|
|
@ -199,6 +199,30 @@ func TestSubsonicResponses(t *testing.T) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Playlist", func() {
|
||||||
|
response.Playlist = &PlaylistWithSongs{}
|
||||||
|
response.Playlist.Id = "1"
|
||||||
|
response.Playlist.Name = "My Playlist"
|
||||||
|
Convey("Without data", func() {
|
||||||
|
Convey("XML", func() {
|
||||||
|
So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><playlist id="1" name="My Playlist"></playlist></subsonic-response>`)
|
||||||
|
})
|
||||||
|
Convey("JSON", func() {
|
||||||
|
So(response, ShouldMatchJSON, `{"playlist":{"id":"1","name":"My Playlist"},"status":"ok","version":"1.0.0"}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Convey("With just required data", func() {
|
||||||
|
entry := make([]Child, 1)
|
||||||
|
entry[0] = Child{Id: "1", Title: "title", IsDir: false}
|
||||||
|
response.Playlist.Entry = entry
|
||||||
|
Convey("XML", func() {
|
||||||
|
So(response, ShouldMatchXML, `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.0.0"><playlist id="1" name="My Playlist"><entry id="1" isDir="false" title="title"></entry></playlist></subsonic-response>`)
|
||||||
|
})
|
||||||
|
Convey("JSON", func() {
|
||||||
|
So(response, ShouldMatchJSON, `{"playlist":{"entry":[{"id":"1","isDir":false,"title":"title"}],"id":"1","name":"My Playlist"},"status":"ok","version":"1.0.0"}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reset(func() {
|
Reset(func() {
|
||||||
response = &Subsonic{Status: "ok", Version: "1.0.0"}
|
response = &Subsonic{Status: "ok", Version: "1.0.0"}
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,6 +30,7 @@ func mapEndpoints() {
|
||||||
beego.NSRouter("/getAlbumList.view", &api.GetAlbumListController{}, "*:Get"),
|
beego.NSRouter("/getAlbumList.view", &api.GetAlbumListController{}, "*:Get"),
|
||||||
|
|
||||||
beego.NSRouter("/getPlaylists.view", &api.PlaylistsController{}, "*:GetAll"),
|
beego.NSRouter("/getPlaylists.view", &api.PlaylistsController{}, "*:GetAll"),
|
||||||
|
beego.NSRouter("/getPlaylist.view", &api.PlaylistsController{}, "*:Get"),
|
||||||
|
|
||||||
beego.NSRouter("/getUser.view", &api.UsersController{}, "*:GetUser"),
|
beego.NSRouter("/getUser.view", &api.UsersController{}, "*:GetUser"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,10 +12,6 @@ import (
|
||||||
"github.com/deluan/gosonic/utils"
|
"github.com/deluan/gosonic/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
DataNotFound = errors.New("Data Not Found")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Browser interface {
|
type Browser interface {
|
||||||
MediaFolders() (*domain.MediaFolders, error)
|
MediaFolders() (*domain.MediaFolders, error)
|
||||||
Indexes(ifModifiedSince time.Time) (*domain.ArtistIndexes, time.Time, error)
|
Indexes(ifModifiedSince time.Time) (*domain.ArtistIndexes, time.Time, error)
|
||||||
|
@ -57,25 +53,6 @@ func (b browser) Indexes(ifModifiedSince time.Time) (*domain.ArtistIndexes, time
|
||||||
return &domain.ArtistIndexes{}, lastModified, nil
|
return &domain.ArtistIndexes{}, lastModified, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Child struct {
|
|
||||||
Id string
|
|
||||||
Title string
|
|
||||||
IsDir bool
|
|
||||||
Parent string
|
|
||||||
Album string
|
|
||||||
Year int
|
|
||||||
Artist string
|
|
||||||
Genre string
|
|
||||||
CoverArt string
|
|
||||||
Starred time.Time
|
|
||||||
Track int
|
|
||||||
Duration int
|
|
||||||
Size string
|
|
||||||
Suffix string
|
|
||||||
BitRate int
|
|
||||||
ContentType string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DirectoryInfo struct {
|
type DirectoryInfo struct {
|
||||||
Id string
|
Id string
|
||||||
Name string
|
Name string
|
||||||
|
|
29
engine/common.go
Normal file
29
engine/common.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Child struct {
|
||||||
|
Id string
|
||||||
|
Title string
|
||||||
|
IsDir bool
|
||||||
|
Parent string
|
||||||
|
Album string
|
||||||
|
Year int
|
||||||
|
Artist string
|
||||||
|
Genre string
|
||||||
|
CoverArt string
|
||||||
|
Starred time.Time
|
||||||
|
Track int
|
||||||
|
Duration int
|
||||||
|
Size string
|
||||||
|
Suffix string
|
||||||
|
BitRate int
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
DataNotFound = errors.New("Data Not Found")
|
||||||
|
)
|
|
@ -6,16 +6,68 @@ import (
|
||||||
|
|
||||||
type Playlists interface {
|
type Playlists interface {
|
||||||
GetAll() (*domain.Playlists, error)
|
GetAll() (*domain.Playlists, error)
|
||||||
|
Get(id string) (*PlaylistInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlaylists(pr domain.PlaylistRepository, mr domain.MediaFileRepository) Playlists {
|
||||||
|
return playlists{pr, mr}
|
||||||
}
|
}
|
||||||
|
|
||||||
type playlists struct {
|
type playlists struct {
|
||||||
plsRepo domain.PlaylistRepository
|
plsRepo domain.PlaylistRepository
|
||||||
}
|
mfileRepo domain.MediaFileRepository
|
||||||
|
|
||||||
func NewPlaylists(pr domain.PlaylistRepository) Playlists {
|
|
||||||
return playlists{pr}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p playlists) GetAll() (*domain.Playlists, error) {
|
func (p playlists) GetAll() (*domain.Playlists, error) {
|
||||||
return p.plsRepo.GetAll(domain.QueryOptions{})
|
return p.plsRepo.GetAll(domain.QueryOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlaylistInfo struct {
|
||||||
|
Id string
|
||||||
|
Name string
|
||||||
|
Entries []Child
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p playlists) Get(id string) (*PlaylistInfo, error) {
|
||||||
|
pl, err := p.plsRepo.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pl == nil {
|
||||||
|
return nil, DataNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
pinfo := &PlaylistInfo{Id: pl.Id, Name: pl.Name}
|
||||||
|
pinfo.Entries = make([]Child, len(pl.Tracks))
|
||||||
|
|
||||||
|
// TODO Optimize: Get all tracks at once
|
||||||
|
for i, mfId := range pl.Tracks {
|
||||||
|
mf, err := p.mfileRepo.Get(mfId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pinfo.Entries[i].Id = mf.Id
|
||||||
|
pinfo.Entries[i].Title = mf.Title
|
||||||
|
pinfo.Entries[i].IsDir = false
|
||||||
|
pinfo.Entries[i].Parent = mf.AlbumId
|
||||||
|
pinfo.Entries[i].Album = mf.Album
|
||||||
|
pinfo.Entries[i].Year = mf.Year
|
||||||
|
pinfo.Entries[i].Artist = mf.Artist
|
||||||
|
pinfo.Entries[i].Genre = mf.Genre
|
||||||
|
//pinfo.Entries[i].Track = mf.TrackNumber
|
||||||
|
pinfo.Entries[i].Duration = mf.Duration
|
||||||
|
pinfo.Entries[i].Size = mf.Size
|
||||||
|
pinfo.Entries[i].Suffix = mf.Suffix
|
||||||
|
pinfo.Entries[i].BitRate = mf.BitRate
|
||||||
|
if mf.Starred {
|
||||||
|
pinfo.Entries[i].Starred = mf.UpdatedAt
|
||||||
|
}
|
||||||
|
if mf.HasCoverArt {
|
||||||
|
pinfo.Entries[i].CoverArt = mf.Id
|
||||||
|
}
|
||||||
|
pinfo.Entries[i].ContentType = mf.ContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pinfo, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue