diff --git a/model/playlist.go b/model/playlist.go index 587cd9dd5..0ff57669e 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -1,6 +1,7 @@ package model import ( + "maps" "path/filepath" "slices" "strconv" @@ -118,52 +119,70 @@ func (pls Playlist) UploadedImagePath() string { return UploadedImagePath(consts.EntityPlaylist, pls.UploadedImage) } -func (pls *Playlist) NormalizeChildPaths() { +func (pls Playlist) WithNormalizeChildPaths() Playlist { if pls.Rules == nil || pls.Rules.Expression == nil { - return + return pls } - normalizePlaylistPaths(pls.Rules.Expression, pls.Path) + plsClone := pls + plsClone.Rules = &criteria.Criteria{ + Sort: pls.Rules.Sort, + Limit: pls.Rules.Limit, + LimitPercent: pls.Rules.LimitPercent, + Offset: pls.Rules.Offset, + Order: pls.Rules.Order, + Expression: normalizePlaylistPaths(pls.Rules.Expression, pls.Path), + } + return plsClone } -func normalizePlaylistPaths(inputRule any, referencingPlaylistPath string) { +func normalizePlaylistPaths(inputRule criteria.Expression, referencingPlaylistPath string) criteria.Expression { if referencingPlaylistPath == "" { - return + return inputRule } switch rule := inputRule.(type) { case criteria.Any: - for _, rules := range rule { - normalizePlaylistPaths(rules, referencingPlaylistPath) + anyCriteria := make(criteria.Any, len(rule)) + for i, rules := range rule { + anyCriteria[i] = normalizePlaylistPaths(rules, referencingPlaylistPath) } + return anyCriteria case criteria.All: - for _, rules := range rule { - normalizePlaylistPaths(rules, referencingPlaylistPath) + allCriteria := make(criteria.All, len(rule)) + for i, rules := range rule { + allCriteria[i] = normalizePlaylistPaths(rules, referencingPlaylistPath) } + return allCriteria case criteria.InPlaylist: - dir := filepath.Dir(referencingPlaylistPath) + inPlaylist := maps.Clone(rule) if path, ok := rule["path"].(string); ok { if path == "" { - return + return inPlaylist } if !filepath.IsAbs(path) { - rule["path"] = filepath.Clean(filepath.Join(dir, path)) + dir := filepath.Dir(referencingPlaylistPath) + inPlaylist["path"] = filepath.Clean(filepath.Join(dir, path)) } } + return inPlaylist case criteria.NotInPlaylist: - dir := filepath.Dir(referencingPlaylistPath) + notInPlaylist := maps.Clone(rule) if path, ok := rule["path"].(string); ok { if path == "" { - return + return notInPlaylist } if !filepath.IsAbs(path) { - rule["path"] = filepath.Clean(filepath.Join(dir, path)) + dir := filepath.Dir(referencingPlaylistPath) + notInPlaylist["path"] = filepath.Clean(filepath.Join(dir, path)) } } + return notInPlaylist } - return + + return inputRule } type Playlists []Playlist diff --git a/model/playlist_test.go b/model/playlist_test.go index 82d64ad0e..2f85dd587 100644 --- a/model/playlist_test.go +++ b/model/playlist_test.go @@ -55,6 +55,7 @@ var _ = Describe("Playlist", func() { criteria.InPlaylist{"path": "/test/my-test-path.m3u"}, criteria.InPlaylist{"path": "../my-test-path.m3u"}, criteria.NotInPlaylist{"path": "/not-test/not-my-test-path.m3u"}, + criteria.Eq{"artist": "Bob Dealin'"}, criteria.Any{ criteria.InPlaylist{"path": "../../in-the-test.nsp"}, criteria.NotInPlaylist{"path": "./sibling.nsp"}, @@ -69,12 +70,13 @@ var _ = Describe("Playlist", func() { }, Path: "/test/nested/my-playlist.nsp"} - pls.NormalizeChildPaths() - Expect(pls.Rules).Should(BeEquivalentTo(&criteria.Criteria{ + newPls := pls.WithNormalizeChildPaths() + Expect(newPls.Rules).Should(BeEquivalentTo(&criteria.Criteria{ Expression: criteria.All{ criteria.InPlaylist{"path": "/test/my-test-path.m3u"}, criteria.InPlaylist{"path": "/test/my-test-path.m3u"}, criteria.NotInPlaylist{"path": "/not-test/not-my-test-path.m3u"}, + criteria.Eq{"artist": "Bob Dealin'"}, criteria.Any{ criteria.InPlaylist{"path": "/in-the-test.nsp"}, criteria.NotInPlaylist{"path": "/test/nested/sibling.nsp"}, @@ -98,8 +100,8 @@ var _ = Describe("Playlist", func() { }, Path: ""} - pls.NormalizeChildPaths() - Expect(pls.Rules).Should(BeEquivalentTo(&criteria.Criteria{ + newPls := pls.WithNormalizeChildPaths() + Expect(newPls.Rules).Should(BeEquivalentTo(&criteria.Criteria{ Expression: criteria.All{ criteria.InPlaylist{"path": "../my-test-path.m3u"}, }, diff --git a/persistence/smart_playlist_repository.go b/persistence/smart_playlist_repository.go index 30149b610..e2969d663 100644 --- a/persistence/smart_playlist_repository.go +++ b/persistence/smart_playlist_repository.go @@ -31,17 +31,18 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool { return false } - rulesSQL := newSmartPlaylistCriteria(*pls.Rules, withSmartPlaylistOwner(*usr)) + normalisedPls := pls.WithNormalizeChildPaths() + rulesSQL := newSmartPlaylistCriteria(*normalisedPls.Rules, withSmartPlaylistOwner(*usr)) - if !r.refreshChildPlaylists(pls, rulesSQL) { + if !r.refreshChildPlaylists(&normalisedPls, rulesSQL) { return false } - if err := r.resolvePercentageLimit(pls, &rulesSQL, usr.ID); err != nil { + if err := r.resolvePercentageLimit(&normalisedPls, &rulesSQL, usr.ID); err != nil { return false } - sq := r.buildSmartPlaylistQuery(pls, rulesSQL, usr.ID) + sq := r.buildSmartPlaylistQuery(&normalisedPls, rulesSQL, usr.ID) sq, err := r.addCriteria(sq, rulesSQL) if err != nil { log.Error(r.ctx, "Error building smart playlist criteria", "playlist", pls.Name, "id", pls.ID, err) @@ -96,7 +97,6 @@ func (r *playlistRepository) refreshChildPlaylists(pls *model.Playlist, rulesSQL return true } - pls.NormalizeChildPaths() childPlaylists, err := r.GetAll(model.QueryOptions{Filters: Or{Eq{"playlist.id": childPlaylistIds}, Eq{"playlist.path": childPlaylistPaths}}}) if err != nil { log.Error(r.ctx, "Error loading child playlists for smart playlist refresh", "playlist", pls.Name, "id", pls.ID, "childIds", childPlaylistIds, err)