Recursively refresh playlist tracks within smart playlist rules (#3018)

* Recursively refresh playlists within smart playlist rules

Signed-off-by: reillymc <reilly@mackenzie-cree.net>

* Clean up recursive smart playlist functions

Signed-off-by: reillymc <reilly@mackenzie-cree.net>

* Add smart playlist refresh timeout config and tests for nested track refetching

Signed-off-by: reillymc <reilly@mackenzie-cree.net>

* Change SmartPlaylistRefreshTimeout to SmartPlaylistRefreshDelay, increase default value

* Revert `smartPlaylistRefreshDelay` default to 5 seconds

---------

Signed-off-by: reillymc <reilly@mackenzie-cree.net>
Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Reilly MacKenzie-Cree 2024-09-16 03:27:54 +10:00 committed by GitHub
parent 180035c1e3
commit d683688b0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 217 additions and 2 deletions

View file

@ -50,6 +50,21 @@ func (c Criteria) ToSql() (sql string, args []interface{}, err error) {
return c.Expression.ToSql()
}
func (c Criteria) ChildPlaylistIds() (ids []string) {
if c.Expression == nil {
return ids
}
switch rules := c.Expression.(type) {
case Any:
ids = rules.ChildPlaylistIds()
case All:
ids = rules.ChildPlaylistIds()
}
return ids
}
func (c Criteria) MarshalJSON() ([]byte, error) {
aux := struct {
All []Expression `json:"all,omitempty"`

View file

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)
@ -89,4 +90,94 @@ var _ = Describe("Criteria", func() {
gomega.Expect(newObj.OrderBy()).To(gomega.Equal("random() asc"))
})
It("extracts all child smart playlist IDs from All expression criteria", func() {
topLevelInPlaylistID := uuid.NewString()
topLevelNotInPlaylistID := uuid.NewString()
nestedAnyInPlaylistID := uuid.NewString()
nestedAnyNotInPlaylistID := uuid.NewString()
nestedAllInPlaylistID := uuid.NewString()
nestedAllNotInPlaylistID := uuid.NewString()
goObj := Criteria{
Expression: All{
InPlaylist{"id": topLevelInPlaylistID},
NotInPlaylist{"id": topLevelNotInPlaylistID},
Any{
InPlaylist{"id": nestedAnyInPlaylistID},
NotInPlaylist{"id": nestedAnyNotInPlaylistID},
},
All{
InPlaylist{"id": nestedAllInPlaylistID},
NotInPlaylist{"id": nestedAllNotInPlaylistID},
},
},
}
ids := goObj.ChildPlaylistIds()
gomega.Expect(ids).To(gomega.ConsistOf(topLevelInPlaylistID, topLevelNotInPlaylistID, nestedAnyInPlaylistID, nestedAnyNotInPlaylistID, nestedAllInPlaylistID, nestedAllNotInPlaylistID))
})
It("extracts all child smart playlist IDs from Any expression criteria", func() {
topLevelInPlaylistID := uuid.NewString()
topLevelNotInPlaylistID := uuid.NewString()
nestedAnyInPlaylistID := uuid.NewString()
nestedAnyNotInPlaylistID := uuid.NewString()
nestedAllInPlaylistID := uuid.NewString()
nestedAllNotInPlaylistID := uuid.NewString()
goObj := Criteria{
Expression: Any{
InPlaylist{"id": topLevelInPlaylistID},
NotInPlaylist{"id": topLevelNotInPlaylistID},
Any{
InPlaylist{"id": nestedAnyInPlaylistID},
NotInPlaylist{"id": nestedAnyNotInPlaylistID},
},
All{
InPlaylist{"id": nestedAllInPlaylistID},
NotInPlaylist{"id": nestedAllNotInPlaylistID},
},
},
}
ids := goObj.ChildPlaylistIds()
gomega.Expect(ids).To(gomega.ConsistOf(topLevelInPlaylistID, topLevelNotInPlaylistID, nestedAnyInPlaylistID, nestedAnyNotInPlaylistID, nestedAllInPlaylistID, nestedAllNotInPlaylistID))
})
It("extracts child smart playlist IDs from deeply nested expression", func() {
nestedAnyInPlaylistID := uuid.NewString()
nestedAnyNotInPlaylistID := uuid.NewString()
nestedAllInPlaylistID := uuid.NewString()
nestedAllNotInPlaylistID := uuid.NewString()
goObj := Criteria{
Expression: Any{
Any{
All{
Any{
InPlaylist{"id": nestedAnyInPlaylistID},
NotInPlaylist{"id": nestedAnyNotInPlaylistID},
Any{
All{
InPlaylist{"id": nestedAllInPlaylistID},
NotInPlaylist{"id": nestedAllNotInPlaylistID},
},
},
},
},
},
},
}
ids := goObj.ChildPlaylistIds()
gomega.Expect(ids).To(gomega.ConsistOf(nestedAnyInPlaylistID, nestedAnyNotInPlaylistID, nestedAllInPlaylistID, nestedAllNotInPlaylistID))
})
})

View file

@ -23,6 +23,10 @@ func (all All) MarshalJSON() ([]byte, error) {
return marshalConjunction("all", all)
}
func (all All) ChildPlaylistIds() (ids []string) {
return extractPlaylistIds(all)
}
type (
Any squirrel.Or
Or = Any
@ -36,6 +40,10 @@ func (any Any) MarshalJSON() ([]byte, error) {
return marshalConjunction("any", any)
}
func (any Any) ChildPlaylistIds() (ids []string) {
return extractPlaylistIds(any)
}
type Is squirrel.Eq
type Eq = Is
@ -275,3 +283,29 @@ func inList(m map[string]interface{}, negate bool) (sql string, args []interface
return "media_file.id IN (" + subQText + ")", subQArgs, nil
}
}
func extractPlaylistIds(inputRule interface{}) (ids []string) {
var id string
var ok bool
switch rule := inputRule.(type) {
case Any:
for _, rules := range rule {
ids = append(ids, extractPlaylistIds(rules)...)
}
case All:
for _, rules := range rule {
ids = append(ids, extractPlaylistIds(rules)...)
}
case InPlaylist:
if id, ok = rule["id"].(string); ok {
ids = append(ids, id)
}
case NotInPlaylist:
if id, ok = rule["id"].(string); ok {
ids = append(ids, id)
}
}
return
}