diff --git a/db/migration/20200411164603_add_created_and_updated_fields_to_playlists.go b/db/migration/20200411164603_add_created_and_updated_fields_to_playlists.go
new file mode 100644
index 000000000..19e4326f1
--- /dev/null
+++ b/db/migration/20200411164603_add_created_and_updated_fields_to_playlists.go
@@ -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
+}
diff --git a/engine/playlists.go b/engine/playlists.go
index e1da7748a..409188d48 100644
--- a/engine/playlists.go
+++ b/engine/playlists.go
@@ -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)
diff --git a/model/playlist.go b/model/playlist.go
index 575cd63f5..456052125 100644
--- a/model/playlist.go
+++ b/model/playlist.go
@@ -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 {
diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go
index 6b7798ac3..fc2414124 100644
--- a/persistence/persistence_suite_test.go
+++ b/persistence/persistence_suite_test.go
@@ -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}
diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go
index 06eb7824e..8a7f1d68d 100644
--- a/persistence/playlist_repository.go
+++ b/persistence/playlist_repository.go
@@ -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)
diff --git a/persistence/playlist_repository_test.go b/persistence/playlist_repository_test.go
index 450553efb..bfbbe7adf 100644
--- a/persistence/playlist_repository_test.go
+++ b/persistence/playlist_repository_test.go
@@ -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))
})
})
})
diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go
index ba1881692..1d8eaf81b 100644
--- a/persistence/sql_base_repository.go
+++ b/persistence/sql_base_repository.go
@@ -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
}
diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go
index dc6a52228..31012ef09 100644
--- a/server/subsonic/playlists.go
+++ b/server/subsonic/playlists.go
@@ -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
}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .JSON
index 5f0a425e9..fa750b471 100644
--- a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .JSON
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .JSON
@@ -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"}]}}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .XML
index df243dbb7..db890725a 100644
--- a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .XML
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Playlists with data should match .XML
@@ -1 +1 @@
-
+
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index dbfd9576a..bfb6fbcb4 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -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"`
/*
-
-
-
-
*/
}
diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go
index 8ca719bbc..f5164b5e6 100644
--- a/server/subsonic/responses/responses_test.go
+++ b/server/subsonic/responses/responses_test.go
@@ -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: ×tamp,
+ Changed: ×tamp,
+ }
pls[1] = Playlist{Id: "222", Name: "bbb"}
response.Playlists.Playlist = pls
})