diff --git a/model/playlist.go b/model/playlist.go index e2f93993d..ff120c736 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -131,6 +131,7 @@ type PlaylistRepository interface { Delete(id string) error Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository GetPlaylists(mediaFileId string) (Playlists, error) + Evaluate(id string) error } type PlaylistTrack struct { diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index 8d1bbe0f8..1aed46e92 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -215,6 +215,10 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { return false } + return r.doRefreshSmartPlaylist(pls, usr.ID) +} + +func (r *playlistRepository) doRefreshSmartPlaylist(pls *model.Playlist, userID string) bool { log.Debug(r.ctx, "Refreshing smart playlist", "playlist", pls.Name, "id", pls.ID) start := time.Now() @@ -244,11 +248,11 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { From("media_file").LeftJoin("annotation on ("+ "annotation.item_id = media_file.id"+ " AND annotation.item_type = 'media_file'"+ - " AND annotation.user_id = ?)", usr.ID) + " AND annotation.user_id = ?)", userID) // Conditionally join album/artist annotation tables only when referenced by criteria or sort requiredJoins := rules.RequiredJoins() - sq = r.addSmartPlaylistAnnotationJoins(sq, requiredJoins, usr.ID) + sq = r.addSmartPlaylistAnnotationJoins(sq, requiredJoins, userID) // Only include media files from libraries the user has access to sq = r.applyLibraryFilter(sq, "media_file") @@ -261,8 +265,8 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { LeftJoin("annotation on ("+ "annotation.item_id = media_file.id"+ " AND annotation.item_type = 'media_file'"+ - " AND annotation.user_id = ?)", usr.ID) - countSq = r.addSmartPlaylistAnnotationJoins(countSq, exprJoins, usr.ID) + " AND annotation.user_id = ?)", userID) + countSq = r.addSmartPlaylistAnnotationJoins(countSq, exprJoins, userID) countSq = r.applyLibraryFilter(countSq, "media_file") countSq = countSq.Where(rules) @@ -310,6 +314,18 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { return true } +func (r *playlistRepository) Evaluate(id string) error { + pls, err := r.Get(id) + if err != nil { + return err + } + if !pls.IsSmartPlaylist() { + return nil + } + r.doRefreshSmartPlaylist(pls, pls.OwnerID) + return nil +} + func (r *playlistRepository) addSmartPlaylistAnnotationJoins(sq SelectBuilder, joins criteria.JoinType, userID string) SelectBuilder { if joins.Has(criteria.JoinAlbumAnnotation) { sq = sq.LeftJoin("annotation AS album_annotation ON ("+ diff --git a/persistence/playlist_repository_test.go b/persistence/playlist_repository_test.go index c091cb32b..0619904c5 100644 --- a/persistence/playlist_repository_test.go +++ b/persistence/playlist_repository_test.go @@ -254,6 +254,57 @@ var _ = Describe("PlaylistRepository", func() { }) }) + Describe("Evaluate", func() { + var testPlaylistID string + + BeforeEach(func() { + DeferCleanup(configtest.SetupConfig()) + }) + + AfterEach(func() { + if testPlaylistID != "" { + _ = repo.Delete(testPlaylistID) + testPlaylistID = "" + } + }) + + It("evaluates a smart playlist and sets EvaluatedAt and SongCount", func() { + rules := &criteria.Criteria{ + Expression: criteria.All{ + criteria.Contains{"title": "Day"}, + }, + } + newPls := model.Playlist{Name: "Evaluate Test", OwnerID: "userid", Rules: rules} + Expect(repo.Put(&newPls)).To(Succeed()) + testPlaylistID = newPls.ID + + Expect(repo.Evaluate(newPls.ID)).To(Succeed()) + + saved, err := repo.Get(newPls.ID) + Expect(err).ToNot(HaveOccurred()) + Expect(saved.EvaluatedAt).ToNot(BeNil()) + Expect(*saved.EvaluatedAt).To(BeTemporally("~", time.Now(), 2*time.Second)) + Expect(saved.SongCount).To(BeNumerically(">", 0)) + }) + + It("is a no-op for non-smart playlists", func() { + newPls := model.Playlist{Name: "Regular Playlist", OwnerID: "userid"} + Expect(repo.Put(&newPls)).To(Succeed()) + testPlaylistID = newPls.ID + + Expect(repo.Evaluate(newPls.ID)).To(Succeed()) + + saved, err := repo.Get(newPls.ID) + Expect(err).ToNot(HaveOccurred()) + Expect(saved.EvaluatedAt).To(BeNil()) + }) + + It("returns ErrNotFound for a non-existent playlist ID", func() { + err := repo.Evaluate("nonexistent-id") + Expect(err).To(MatchError(model.ErrNotFound)) + }) + }) + Describe("Playlist Track Sorting", func() { var testPlaylistID string diff --git a/tests/mock_playlist_repo.go b/tests/mock_playlist_repo.go index 9bdc52152..9717e49d2 100644 --- a/tests/mock_playlist_repo.go +++ b/tests/mock_playlist_repo.go @@ -108,4 +108,11 @@ func (m *MockPlaylistRepo) CountAll(_ ...model.QueryOptions) (int64, error) { return int64(len(m.Data)), nil } +func (m *MockPlaylistRepo) Evaluate(_ string) error { + if m.Err { + return errors.New("error") + } + return nil +} + var _ model.PlaylistRepository = (*MockPlaylistRepo)(nil)