diff --git a/core/archiver.go b/core/archiver.go index 47bad1032..520db2733 100644 --- a/core/archiver.go +++ b/core/archiver.go @@ -59,7 +59,7 @@ func (a *archiver) ZipPlaylist(ctx context.Context, id string, out io.Writer) er log.Error(ctx, "Error loading mediafiles from playlist", "id", id, err) return err } - return a.zipTracks(ctx, id, out, pls.Tracks, a.createPlaylistHeader) + return a.zipTracks(ctx, id, out, pls.MediaFiles(), a.createPlaylistHeader) } func (a *archiver) zipTracks(ctx context.Context, id string, out io.Writer, mfs model.MediaFiles, ch createHeader) error { diff --git a/model/playlist.go b/model/playlist.go index 24e457913..1dd38c78f 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -1,32 +1,87 @@ package model import ( + "strconv" "time" + + "github.com/navidrome/navidrome/utils" ) type Playlist struct { - ID string `structs:"id" json:"id" orm:"column(id)"` - Name string `structs:"name" json:"name"` - Comment string `structs:"comment" json:"comment"` - Duration float32 `structs:"duration" json:"duration"` - Size int64 `structs:"size" json:"size"` - SongCount int `structs:"song_count" json:"songCount"` - Owner string `structs:"owner" json:"owner"` - Public bool `structs:"public" json:"public"` - Tracks MediaFiles `structs:"-" json:"tracks,omitempty"` - Path string `structs:"path" json:"path"` - Sync bool `structs:"sync" json:"sync"` - CreatedAt time.Time `structs:"created_at" json:"createdAt"` - UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` + ID string `structs:"id" json:"id" orm:"column(id)"` + Name string `structs:"name" json:"name"` + Comment string `structs:"comment" json:"comment"` + Duration float32 `structs:"duration" json:"duration"` + Size int64 `structs:"size" json:"size"` + SongCount int `structs:"song_count" json:"songCount"` + Owner string `structs:"owner" json:"owner"` + Public bool `structs:"public" json:"public"` + Tracks PlaylistTracks `structs:"-" json:"tracks,omitempty"` + Path string `structs:"path" json:"path"` + Sync bool `structs:"sync" json:"sync"` + CreatedAt time.Time `structs:"created_at" json:"createdAt"` + UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // SmartPlaylist attributes Rules *SmartPlaylist `structs:"-" json:"rules"` EvaluatedAt time.Time `structs:"evaluated_at" json:"evaluatedAt"` } +func (pls Playlist) IsSmartPlaylist() bool { + return pls.Rules != nil && pls.Rules.Combinator != "" +} + +func (pls Playlist) MediaFiles() MediaFiles { + mfs := make(MediaFiles, len(pls.Tracks)) + for i, t := range pls.Tracks { + mfs[i] = t.MediaFile + } + return mfs +} + +func (pls *Playlist) RemoveTracks(idxToRemove []int) { + var newTracks PlaylistTracks + for i, t := range pls.Tracks { + if utils.IntInSlice(i, idxToRemove) { + continue + } + newTracks = append(newTracks, t) + } + pls.Tracks = newTracks +} + +func (pls *Playlist) AddTracks(mediaFileIds []string) { + pos := len(pls.Tracks) + for _, mfId := range mediaFileIds { + pos++ + t := PlaylistTrack{ + ID: strconv.Itoa(pos), + MediaFileID: mfId, + MediaFile: MediaFile{ID: mfId}, + PlaylistID: pls.ID, + } + pls.Tracks = append(pls.Tracks, t) + } +} + +func (pls *Playlist) AddMediaFiles(mfs MediaFiles) { + pos := len(pls.Tracks) + for _, mf := range mfs { + pos++ + t := PlaylistTrack{ + ID: strconv.Itoa(pos), + MediaFileID: mf.ID, + MediaFile: mf, + PlaylistID: pls.ID, + } + pls.Tracks = append(pls.Tracks, t) + } +} + type Playlists []Playlist type PlaylistRepository interface { + ResourceRepository CountAll(options ...QueryOptions) (int64, error) Exists(id string) (bool, error) Put(pls *Playlist) error diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index b5ee52cb2..44ebfcaea 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -70,16 +70,9 @@ var ( ) var ( - plsBest = model.Playlist{ - Name: "Best", - Comment: "No Comments", - Owner: "userid", - Public: true, - SongCount: 2, - Tracks: model.MediaFiles{{ID: "1001"}, {ID: "1003"}}, - } - plsCool = model.Playlist{Name: "Cool", Owner: "userid", Tracks: model.MediaFiles{{ID: "1004"}}} - testPlaylists = []*model.Playlist{&plsBest, &plsCool} + plsBest model.Playlist + plsCool model.Playlist + testPlaylists []*model.Playlist ) func P(path string) string { @@ -130,6 +123,18 @@ var _ = Describe("Initialize test DB", func() { } } + plsBest = model.Playlist{ + Name: "Best", + Comment: "No Comments", + Owner: "userid", + Public: true, + SongCount: 2, + } + plsBest.AddTracks([]string{"1001", "1003"}) + plsCool = model.Playlist{Name: "Cool", Owner: "userid"} + plsCool.AddTracks([]string{"1004"}) + testPlaylists = []*model.Playlist{&plsBest, &plsCool} + pr := NewPlaylistRepository(ctx, o) for i := range testPlaylists { err := pr.Put(testPlaylists[i]) @@ -162,6 +167,5 @@ var _ = Describe("Initialize test DB", func() { songComeTogether.Starred = true songComeTogether.StarredAt = mf.StarredAt testSongs[1] = songComeTogether - }) }) diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index 44ade6592..28b1a8fb8 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -101,7 +101,7 @@ func (r *playlistRepository) Put(p *model.Playlist) error { if tracks == nil { return nil } - return r.updateTracks(id, tracks) + return r.updateTracks(id, p.MediaFiles()) } func (r *playlistRepository) Get(id string) (*model.Playlist, error) { @@ -185,9 +185,8 @@ func (r *playlistRepository) loadTracks(pls *dbPlaylist) error { Where(Eq{"playlist_id": pls.ID}).OrderBy("playlist_tracks.id") err := r.queryAll(tracksQuery, &pls.Tracks) if err != nil { - log.Error("Error loading playlist tracks", "playlist", pls.Name, "id", pls.ID) + log.Error(r.ctx, "Error loading playlist tracks", "playlist", pls.Name, "id", pls.ID, err) } - err = r.loadMediaFileGenres(&pls.Tracks) return err } diff --git a/persistence/playlist_repository_test.go b/persistence/playlist_repository_test.go index 4588009ac..832f34bdc 100644 --- a/persistence/playlist_repository_test.go +++ b/persistence/playlist_repository_test.go @@ -58,23 +58,23 @@ var _ = Describe("PlaylistRepository", func() { pls, err := repo.GetWithTracks(plsBest.ID) Expect(err).To(BeNil()) Expect(pls.Name).To(Equal(plsBest.Name)) - Expect(pls.Tracks).To(Equal(model.MediaFiles{ - songDayInALife, - songRadioactivity, - })) + mfs := pls.MediaFiles() + Expect(mfs).To(HaveLen(2)) + Expect(mfs[0].ID).To(Equal(songDayInALife.ID)) + Expect(mfs[1].ID).To(Equal(songRadioactivity.ID)) }) }) It("Put/Exists/Delete", func() { By("saves the playlist to the DB") - newPls := model.Playlist{Name: "Great!", Owner: "userid", - Tracks: model.MediaFiles{{ID: "1004"}, {ID: "1003"}}} + newPls := model.Playlist{Name: "Great!", Owner: "userid"} + newPls.AddTracks([]string{"1004", "1003"}) By("saves the playlist to the DB") Expect(repo.Put(&newPls)).To(BeNil()) By("adds repeated songs to a playlist and keeps the order") - newPls.Tracks = append(newPls.Tracks, model.MediaFile{ID: "1004"}) + newPls.AddTracks([]string{"1004"}) Expect(repo.Put(&newPls)).To(BeNil()) saved, _ := repo.GetWithTracks(newPls.ID) Expect(saved.Tracks).To(HaveLen(3)) diff --git a/scanner/playlist_sync.go b/scanner/playlist_sync.go index 79c996fcd..d469e96ce 100644 --- a/scanner/playlist_sync.go +++ b/scanner/playlist_sync.go @@ -83,6 +83,7 @@ func (s *playlistSync) parsePlaylist(ctx context.Context, playlistFile string, b mediaFileRepository := s.ds.MediaFile(ctx) scanner := bufio.NewScanner(file) scanner.Split(scanLines) + var mfs model.MediaFiles for scanner.Scan() { path := scanner.Text() // Skip extended info @@ -101,8 +102,10 @@ func (s *playlistSync) parsePlaylist(ctx context.Context, playlistFile string, b log.Warn(ctx, "Path in playlist not found", "playlist", playlistFile, "path", path, err) continue } - pls.Tracks = append(pls.Tracks, *mf) + mfs = append(mfs, *mf) } + pls.Tracks = nil + pls.AddMediaFiles(mfs) return pls, scanner.Err() } diff --git a/server/subsonic/playlists.go b/server/subsonic/playlists.go index aee1dbbbf..e9ddcf998 100644 --- a/server/subsonic/playlists.go +++ b/server/subsonic/playlists.go @@ -76,16 +76,14 @@ func (c *PlaylistsController) create(ctx context.Context, playlistId, name strin if owner != pls.Owner { return model.ErrNotAuthorized } - pls.Tracks = nil } else { pls = &model.Playlist{ Name: name, Owner: owner, } } - for _, id := range ids { - pls.Tracks = append(pls.Tracks, model.MediaFile{ID: id}) - } + pls.Tracks = nil + pls.AddTracks(ids) err = tx.Playlist(ctx).Put(pls) playlistId = pls.ID @@ -143,18 +141,8 @@ func (c *PlaylistsController) update(ctx context.Context, playlistId string, nam pls.Public = *public } - newTracks := model.MediaFiles{} - for i, t := range pls.Tracks { - if utils.IntInSlice(i, idxToRemove) { - continue - } - newTracks = append(newTracks, t) - } - - for _, id := range idsToAdd { - newTracks = append(newTracks, model.MediaFile{ID: id}) - } - pls.Tracks = newTracks + pls.RemoveTracks(idxToRemove) + pls.AddTracks(idsToAdd) return tx.Playlist(ctx).Put(pls) }) @@ -203,7 +191,7 @@ func (c *PlaylistsController) buildPlaylistWithSongs(ctx context.Context, p *mod pls := &responses.PlaylistWithSongs{ Playlist: *c.buildPlaylist(*p), } - pls.Entry = childrenFromMediaFiles(ctx, p.Tracks) + pls.Entry = childrenFromMediaFiles(ctx, p.MediaFiles()) return pls }