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 (
"context"
"time"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/utils"
@ -118,6 +119,8 @@ type PlaylistInfo struct {
Public bool
Owner string
Comment string
Created time.Time
Changed time.Time
}
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,
Owner: pl.Owner,
Comment: pl.Comment,
Changed: pl.UpdatedAt,
Created: pl.CreatedAt,
}
plsInfo.Entries = FromMediaFiles(pl.Tracks)

View file

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

View file

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

View file

@ -3,20 +3,24 @@ package persistence
import (
"context"
"strings"
"time"
. "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
)
type playlist struct {
ID string `orm:"column(id)"`
Name string
Comment string
Duration float32
Owner string
Public bool
Tracks string
ID string `orm:"column(id)"`
Name string
Comment string
Duration float32
Owner string
Public bool
Tracks string
CreatedAt time.Time
UpdatedAt time.Time
}
type playlistRepository struct {
@ -44,6 +48,10 @@ func (r *playlistRepository) Delete(id string) error {
}
func (r *playlistRepository) Put(p *model.Playlist) error {
if p.ID == "" {
p.CreatedAt = time.Now()
}
p.UpdatedAt = time.Now()
pls := r.fromModel(p)
_, err := r.put(pls.ID, pls)
return err
@ -62,18 +70,11 @@ func (r *playlistRepository) GetWithTracks(id string) (*model.Playlist, error) {
if err != nil {
return nil, err
}
mfRepo := NewMediaFileRepository(r.ctx, r.ormer)
pls.Duration = 0
newTracks := model.MediaFiles{}
pls.Tracks = r.loadTracks(pls)
for _, t := range pls.Tracks {
mf, err := mfRepo.Get(t.ID)
if err != nil {
continue
}
pls.Duration += mf.Duration
newTracks = append(newTracks, *mf)
pls.Duration += t.Duration
}
pls.Tracks = newTracks
return pls, err
}
@ -94,12 +95,14 @@ func (r *playlistRepository) toModels(all []playlist) model.Playlists {
func (r *playlistRepository) toModel(p *playlist) model.Playlist {
pls := model.Playlist{
ID: p.ID,
Name: p.Name,
Comment: p.Comment,
Duration: p.Duration,
Owner: p.Owner,
Public: p.Public,
ID: p.ID,
Name: p.Name,
Comment: p.Comment,
Duration: p.Duration,
Owner: p.Owner,
Public: p.Public,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
if strings.TrimSpace(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 = r.loadTracks(&pls)
return pls
}
func (r *playlistRepository) fromModel(p *model.Playlist) playlist {
pls := playlist{
ID: p.ID,
Name: p.Name,
Comment: p.Comment,
Duration: p.Duration,
Owner: p.Owner,
Public: p.Public,
ID: p.ID,
Name: p.Name,
Comment: p.Comment,
Owner: p.Owner,
Public: p.Public,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
p.Tracks = r.loadTracks(p)
var newTracks []string
for _, t := range p.Tracks {
newTracks = append(newTracks, t.ID)
pls.Duration += t.Duration
}
pls.Tracks = strings.Join(newTracks, ",")
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)

View file

@ -32,7 +32,18 @@ var _ = Describe("PlaylistRepository", func() {
Describe("Get", 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() {
_, err := repo.Get("666")
@ -40,20 +51,22 @@ var _ = Describe("PlaylistRepository", func() {
})
})
Describe("Put/Get/Delete", func() {
newPls := model.Playlist{ID: "22", Name: "Great!", Tracks: model.MediaFiles{{ID: "4"}}}
Describe("Put/Exists/Delete", func() {
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() {
Expect(repo.Put(&newPls)).To(BeNil())
})
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() {
Expect(repo.Delete("22")).To(BeNil())
})
It("returns error if tries to retrieve the deleted playlist", func() {
_, err := repo.Get("22")
Expect(err).To(MatchError(model.ErrNotFound))
Expect(repo.Exists("22")).To(BeFalse())
})
})
@ -71,7 +84,10 @@ var _ = Describe("PlaylistRepository", func() {
Describe("GetAll", 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) {
values, _ := toSqlArgs(m)
// Remove created_at from args and save it for later, if needed fo insert
createdAt := values["created_at"]
delete(values, "created_at")
if id != "" {
@ -178,6 +179,7 @@ func (r sqlRepository) put(id string, m interface{}) (newId string, err error) {
id = rand.String()
values["id"] = id
}
// It is a insert, if there was a created_at, add it back to args
if createdAt != nil {
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].Owner = p.Owner
playlists[i].Public = p.Public
playlists[i].Created = &p.CreatedAt
playlists[i].Changed = &p.UpdatedAt
}
response := NewResponse()
response.Playlists = &responses.Playlists{Playlist: playlists}
@ -58,7 +60,7 @@ func (c *PlaylistsController) GetPlaylist(w http.ResponseWriter, r *http.Request
}
response := NewResponse()
response.Playlist = c.buildPlaylist(r.Context(), pinfo)
response.Playlist = c.buildPlaylistWithSongs(r.Context(), pinfo)
return response, nil
}
@ -125,15 +127,24 @@ func (c *PlaylistsController) UpdatePlaylist(w http.ResponseWriter, r *http.Requ
return NewResponse(), nil
}
func (c *PlaylistsController) buildPlaylist(ctx context.Context, d *engine.PlaylistInfo) *responses.PlaylistWithSongs {
pls := &responses.PlaylistWithSongs{}
func (c *PlaylistsController) buildPlaylistWithSongs(ctx context.Context, d *engine.PlaylistInfo) *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.Name = d.Name
pls.Comment = d.Comment
pls.SongCount = d.SongCount
pls.Owner = d.Owner
pls.Duration = d.Duration
pls.Public = d.Public
pls.Entry = ToChildren(ctx, d.Entries)
pls.Created = &d.Created
pls.Changed = &d.Changed
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 {
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
Comment string `xml:"comment,attr,omitempty" json:"comment,omitempty"`
SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
Public bool `xml:"public,attr,omitempty" json:"public,omitempty"`
Owner string `xml:"owner,attr,omitempty" json:"owner,omitempty"`
Id string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
Comment string `xml:"comment,attr,omitempty" json:"comment,omitempty"`
SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
Public bool `xml:"public,attr,omitempty" json:"public,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:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0-->
</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-->
*/
}

View file

@ -235,9 +235,20 @@ var _ = Describe("Responses", func() {
})
Context("with data", func() {
timestamp, _ := time.Parse(time.RFC3339, "2020-04-11T16:43:00Z04:00")
BeforeEach(func() {
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"}
response.Playlists.Playlist = pls
})