Add created and changed fields to playlists responses

This commit is contained in:
Deluan 2020-04-11 16:45:21 -04:00
parent 803a5776ae
commit e232c5c561
12 changed files with 163 additions and 68 deletions

View File

@ -0,0 +1,26 @@
package migration
import (
"database/sql"
"github.com/pressly/goose"
)
func init() {
goose.AddMigration(Up20200411164603, Down20200411164603)
}
func Up20200411164603(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table playlist
add created_at datetime;
alter table playlist
add updated_at datetime;
update playlist
set created_at = datetime('now'), updated_at = datetime('now');
`)
return err
}
func Down20200411164603(tx *sql.Tx) error {
return nil
}

View File

@ -2,6 +2,7 @@ package engine
import ( import (
"context" "context"
"time"
"github.com/deluan/navidrome/model" "github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/utils" "github.com/deluan/navidrome/utils"
@ -118,6 +119,8 @@ type PlaylistInfo struct {
Public bool Public bool
Owner string Owner string
Comment string Comment string
Created time.Time
Changed time.Time
} }
func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) { func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
@ -135,6 +138,8 @@ func (p *playlists) Get(ctx context.Context, id string) (*PlaylistInfo, error) {
Public: pl.Public, Public: pl.Public,
Owner: pl.Owner, Owner: pl.Owner,
Comment: pl.Comment, Comment: pl.Comment,
Changed: pl.UpdatedAt,
Created: pl.CreatedAt,
} }
plsInfo.Entries = FromMediaFiles(pl.Tracks) plsInfo.Entries = FromMediaFiles(pl.Tracks)

View File

@ -1,13 +1,17 @@
package model package model
import "time"
type Playlist struct { type Playlist struct {
ID string ID string
Name string Name string
Comment string Comment string
Duration float32 Duration float32
Owner string Owner string
Public bool Public bool
Tracks MediaFiles Tracks MediaFiles
CreatedAt time.Time
UpdatedAt time.Time
} }
type PlaylistRepository interface { type PlaylistRepository interface {

View File

@ -65,13 +65,12 @@ var (
var ( var (
plsBest = model.Playlist{ plsBest = model.Playlist{
ID: "10", ID: "10",
Name: "Best", Name: "Best",
Comment: "No Comments", Comment: "No Comments",
Duration: 10, Owner: "userid",
Owner: "userid", Public: true,
Public: true, Tracks: model.MediaFiles{{ID: "1"}, {ID: "3"}},
Tracks: model.MediaFiles{{ID: "1"}, {ID: "3"}},
} }
plsCool = model.Playlist{ID: "11", Name: "Cool", Tracks: model.MediaFiles{{ID: "4"}}} plsCool = model.Playlist{ID: "11", Name: "Cool", Tracks: model.MediaFiles{{ID: "4"}}}
testPlaylists = model.Playlists{plsBest, plsCool} testPlaylists = model.Playlists{plsBest, plsCool}

View File

@ -3,20 +3,24 @@ package persistence
import ( import (
"context" "context"
"strings" "strings"
"time"
. "github.com/Masterminds/squirrel" . "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model" "github.com/deluan/navidrome/model"
) )
type playlist struct { type playlist struct {
ID string `orm:"column(id)"` ID string `orm:"column(id)"`
Name string Name string
Comment string Comment string
Duration float32 Duration float32
Owner string Owner string
Public bool Public bool
Tracks string Tracks string
CreatedAt time.Time
UpdatedAt time.Time
} }
type playlistRepository struct { type playlistRepository struct {
@ -44,6 +48,10 @@ func (r *playlistRepository) Delete(id string) error {
} }
func (r *playlistRepository) Put(p *model.Playlist) error { func (r *playlistRepository) Put(p *model.Playlist) error {
if p.ID == "" {
p.CreatedAt = time.Now()
}
p.UpdatedAt = time.Now()
pls := r.fromModel(p) pls := r.fromModel(p)
_, err := r.put(pls.ID, pls) _, err := r.put(pls.ID, pls)
return err return err
@ -62,18 +70,11 @@ func (r *playlistRepository) GetWithTracks(id string) (*model.Playlist, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
mfRepo := NewMediaFileRepository(r.ctx, r.ormer)
pls.Duration = 0 pls.Duration = 0
newTracks := model.MediaFiles{} pls.Tracks = r.loadTracks(pls)
for _, t := range pls.Tracks { for _, t := range pls.Tracks {
mf, err := mfRepo.Get(t.ID) pls.Duration += t.Duration
if err != nil {
continue
}
pls.Duration += mf.Duration
newTracks = append(newTracks, *mf)
} }
pls.Tracks = newTracks
return pls, err return pls, err
} }
@ -94,12 +95,14 @@ func (r *playlistRepository) toModels(all []playlist) model.Playlists {
func (r *playlistRepository) toModel(p *playlist) model.Playlist { func (r *playlistRepository) toModel(p *playlist) model.Playlist {
pls := model.Playlist{ pls := model.Playlist{
ID: p.ID, ID: p.ID,
Name: p.Name, Name: p.Name,
Comment: p.Comment, Comment: p.Comment,
Duration: p.Duration, Duration: p.Duration,
Owner: p.Owner, Owner: p.Owner,
Public: p.Public, Public: p.Public,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
} }
if strings.TrimSpace(p.Tracks) != "" { if strings.TrimSpace(p.Tracks) != "" {
tracks := strings.Split(p.Tracks, ",") tracks := strings.Split(p.Tracks, ",")
@ -107,24 +110,44 @@ func (r *playlistRepository) toModel(p *playlist) model.Playlist {
pls.Tracks = append(pls.Tracks, model.MediaFile{ID: t}) pls.Tracks = append(pls.Tracks, model.MediaFile{ID: t})
} }
} }
pls.Tracks = r.loadTracks(&pls)
return pls return pls
} }
func (r *playlistRepository) fromModel(p *model.Playlist) playlist { func (r *playlistRepository) fromModel(p *model.Playlist) playlist {
pls := playlist{ pls := playlist{
ID: p.ID, ID: p.ID,
Name: p.Name, Name: p.Name,
Comment: p.Comment, Comment: p.Comment,
Duration: p.Duration, Owner: p.Owner,
Owner: p.Owner, Public: p.Public,
Public: p.Public, CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
} }
p.Tracks = r.loadTracks(p)
var newTracks []string var newTracks []string
for _, t := range p.Tracks { for _, t := range p.Tracks {
newTracks = append(newTracks, t.ID) newTracks = append(newTracks, t.ID)
pls.Duration += t.Duration
} }
pls.Tracks = strings.Join(newTracks, ",") pls.Tracks = strings.Join(newTracks, ",")
return pls return pls
} }
func (r *playlistRepository) loadTracks(p *model.Playlist) model.MediaFiles {
mfRepo := NewMediaFileRepository(r.ctx, r.ormer)
var ids []string
for _, t := range p.Tracks {
ids = append(ids, t.ID)
}
idsFilter := Eq{"id": ids}
tracks, err := mfRepo.GetAll(model.QueryOptions{Filters: idsFilter})
if err == nil {
return tracks
} else {
log.Error(r.ctx, "Could not load playlist's tracks", "playlistName", p.Name, "playlistId", p.ID, err)
}
return nil
}
var _ model.PlaylistRepository = (*playlistRepository)(nil) var _ model.PlaylistRepository = (*playlistRepository)(nil)

View File

@ -32,7 +32,18 @@ var _ = Describe("PlaylistRepository", func() {
Describe("Get", func() { Describe("Get", func() {
It("returns an existing playlist", func() { It("returns an existing playlist", func() {
Expect(repo.Get("10")).To(Equal(&plsBest)) p, err := repo.Get("10")
Expect(err).To(BeNil())
// Compare all but Tracks and timestamps
p2 := *p
p2.Tracks = plsBest.Tracks
p2.UpdatedAt = plsBest.UpdatedAt
p2.CreatedAt = plsBest.CreatedAt
Expect(p2).To(Equal(plsBest))
// Compare tracks
for i := range p.Tracks {
Expect(p.Tracks[i].ID).To(Equal(plsBest.Tracks[i].ID))
}
}) })
It("returns ErrNotFound for a non-existing playlist", func() { It("returns ErrNotFound for a non-existing playlist", func() {
_, err := repo.Get("666") _, err := repo.Get("666")
@ -40,20 +51,22 @@ var _ = Describe("PlaylistRepository", func() {
}) })
}) })
Describe("Put/Get/Delete", func() { Describe("Put/Exists/Delete", func() {
newPls := model.Playlist{ID: "22", Name: "Great!", Tracks: model.MediaFiles{{ID: "4"}}} var newPls model.Playlist
BeforeEach(func() {
newPls = model.Playlist{ID: "22", Name: "Great!", Tracks: model.MediaFiles{{ID: "4"}}}
})
It("saves the playlist to the DB", func() { It("saves the playlist to the DB", func() {
Expect(repo.Put(&newPls)).To(BeNil()) Expect(repo.Put(&newPls)).To(BeNil())
}) })
It("returns the newly created playlist", func() { It("returns the newly created playlist", func() {
Expect(repo.Get("22")).To(Equal(&newPls)) Expect(repo.Exists("22")).To(BeTrue())
}) })
It("returns deletes the playlist", func() { It("returns deletes the playlist", func() {
Expect(repo.Delete("22")).To(BeNil()) Expect(repo.Delete("22")).To(BeNil())
}) })
It("returns error if tries to retrieve the deleted playlist", func() { It("returns error if tries to retrieve the deleted playlist", func() {
_, err := repo.Get("22") Expect(repo.Exists("22")).To(BeFalse())
Expect(err).To(MatchError(model.ErrNotFound))
}) })
}) })
@ -71,7 +84,10 @@ var _ = Describe("PlaylistRepository", func() {
Describe("GetAll", func() { Describe("GetAll", func() {
It("returns all playlists from DB", func() { It("returns all playlists from DB", func() {
Expect(repo.GetAll()).To(Equal(model.Playlists{plsBest, plsCool})) all, err := repo.GetAll()
Expect(err).To(BeNil())
Expect(all[0].ID).To(Equal(plsBest.ID))
Expect(all[1].ID).To(Equal(plsCool.ID))
}) })
}) })
}) })

View File

@ -160,6 +160,7 @@ func (r sqlRepository) count(countQuery SelectBuilder, options ...model.QueryOpt
func (r sqlRepository) put(id string, m interface{}) (newId string, err error) { func (r sqlRepository) put(id string, m interface{}) (newId string, err error) {
values, _ := toSqlArgs(m) values, _ := toSqlArgs(m)
// Remove created_at from args and save it for later, if needed fo insert
createdAt := values["created_at"] createdAt := values["created_at"]
delete(values, "created_at") delete(values, "created_at")
if id != "" { if id != "" {
@ -178,6 +179,7 @@ func (r sqlRepository) put(id string, m interface{}) (newId string, err error) {
id = rand.String() id = rand.String()
values["id"] = id values["id"] = id
} }
// It is a insert, if there was a created_at, add it back to args
if createdAt != nil { if createdAt != nil {
values["created_at"] = createdAt values["created_at"] = createdAt
} }

View File

@ -36,6 +36,8 @@ func (c *PlaylistsController) GetPlaylists(w http.ResponseWriter, r *http.Reques
playlists[i].Duration = int(p.Duration) playlists[i].Duration = int(p.Duration)
playlists[i].Owner = p.Owner playlists[i].Owner = p.Owner
playlists[i].Public = p.Public playlists[i].Public = p.Public
playlists[i].Created = &p.CreatedAt
playlists[i].Changed = &p.UpdatedAt
} }
response := NewResponse() response := NewResponse()
response.Playlists = &responses.Playlists{Playlist: playlists} response.Playlists = &responses.Playlists{Playlist: playlists}
@ -58,7 +60,7 @@ func (c *PlaylistsController) GetPlaylist(w http.ResponseWriter, r *http.Request
} }
response := NewResponse() response := NewResponse()
response.Playlist = c.buildPlaylist(r.Context(), pinfo) response.Playlist = c.buildPlaylistWithSongs(r.Context(), pinfo)
return response, nil return response, nil
} }
@ -125,15 +127,24 @@ func (c *PlaylistsController) UpdatePlaylist(w http.ResponseWriter, r *http.Requ
return NewResponse(), nil return NewResponse(), nil
} }
func (c *PlaylistsController) buildPlaylist(ctx context.Context, d *engine.PlaylistInfo) *responses.PlaylistWithSongs { func (c *PlaylistsController) buildPlaylistWithSongs(ctx context.Context, d *engine.PlaylistInfo) *responses.PlaylistWithSongs {
pls := &responses.PlaylistWithSongs{} pls := &responses.PlaylistWithSongs{
Playlist: *c.buildPlaylist(d),
}
pls.Entry = ToChildren(ctx, d.Entries)
return pls
}
func (c *PlaylistsController) buildPlaylist(d *engine.PlaylistInfo) *responses.Playlist {
pls := &responses.Playlist{}
pls.Id = d.Id pls.Id = d.Id
pls.Name = d.Name pls.Name = d.Name
pls.Comment = d.Comment
pls.SongCount = d.SongCount pls.SongCount = d.SongCount
pls.Owner = d.Owner pls.Owner = d.Owner
pls.Duration = d.Duration pls.Duration = d.Duration
pls.Public = d.Public pls.Public = d.Public
pls.Created = &d.Created
pls.Entry = ToChildren(ctx, d.Entries) pls.Changed = &d.Changed
return pls return pls
} }

View File

@ -1 +1 @@
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","playlists":{"playlist":[{"id":"111","name":"aaa"},{"id":"222","name":"bbb"}]}} {"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","playlists":{"playlist":[{"id":"111","name":"aaa","comment":"comment","songCount":2,"duration":120,"public":true,"owner":"admin","created":"0001-01-01T00:00:00Z","changed":"0001-01-01T00:00:00Z"},{"id":"222","name":"bbb"}]}}

View File

@ -1 +1 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><playlists><playlist id="111" name="aaa"></playlist><playlist id="222" name="bbb"></playlist></playlists></subsonic-response> <subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><playlists><playlist id="111" name="aaa" comment="comment" songCount="2" duration="120" public="true" owner="admin" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z"></playlist><playlist id="222" name="bbb"></playlist></playlists></subsonic-response>

View File

@ -188,22 +188,20 @@ type AlbumList struct {
} }
type Playlist struct { type Playlist struct {
Id string `xml:"id,attr" json:"id"` Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
Comment string `xml:"comment,attr,omitempty" json:"comment,omitempty"` Comment string `xml:"comment,attr,omitempty" json:"comment,omitempty"`
SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"` SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"` Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
Public bool `xml:"public,attr,omitempty" json:"public,omitempty"` Public bool `xml:"public,attr,omitempty" json:"public,omitempty"`
Owner string `xml:"owner,attr,omitempty" json:"owner,omitempty"` Owner string `xml:"owner,attr,omitempty" json:"owner,omitempty"`
Created *time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
Changed *time.Time `xml:"changed,attr,omitempty" json:"changed,omitempty"`
/* /*
<xs:sequence> <xs:sequence>
<xs:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0--> <xs:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0-->
</xs:sequence> </xs:sequence>
<xs:attribute name="comment" type="xs:string" use="optional"/> <!--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--> <xs:attribute name="coverArt" type="xs:string" use="optional"/> <!--Added in 1.11.0-->
*/ */
} }

View File

@ -235,9 +235,20 @@ var _ = Describe("Responses", func() {
}) })
Context("with data", func() { Context("with data", func() {
timestamp, _ := time.Parse(time.RFC3339, "2020-04-11T16:43:00Z04:00")
BeforeEach(func() { BeforeEach(func() {
pls := make([]Playlist, 2) pls := make([]Playlist, 2)
pls[0] = Playlist{Id: "111", Name: "aaa"} pls[0] = Playlist{
Id: "111",
Name: "aaa",
Comment: "comment",
SongCount: 2,
Duration: 120,
Public: true,
Owner: "admin",
Created: &timestamp,
Changed: &timestamp,
}
pls[1] = Playlist{Id: "222", Name: "bbb"} pls[1] = Playlist{Id: "222", Name: "bbb"}
response.Playlists.Playlist = pls response.Playlists.Playlist = pls
}) })