feat(server): add percentage-based limits to smart playlists (#5144)

* feat(playlists): add percentage-based limits to smart playlists

Add a new `limitPercent` JSON field to Criteria that allows smart playlist
limits to be expressed as a percentage of matching tracks rather than a
fixed number. For example, a playlist matching 450 songs with a 10% limit
returns 45 songs, scaling dynamically as the library grows.

When `limitPercent` is set, refreshSmartPlaylist runs a COUNT query first
to determine the total matching tracks, then resolves the percentage to an
absolute LIMIT before executing the main query. The fixed `limit` field
takes precedence when both are set. Values are clamped to [0, 100] during
JSON unmarshaling.

No database migration is needed since rules are stored as a JSON string.

* fix(criteria): validate percentage limit range in IsPercentageLimit method

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(criteria): ensure idempotency of ToSql method for expressions

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-03-04 22:42:49 -05:00
committed by GitHub
parent f03ca44a8e
commit 11e4aaed1b
6 changed files with 296 additions and 46 deletions
+15
View File
@@ -122,6 +122,21 @@ var _ = Describe("parseNSP", func() {
Expect(pls.Name).To(Equal("Original"))
})
It("parses limitPercent from NSP", func() {
nsp := `{
"all": [{"is": {"loved": true}}],
"sort": "playCount",
"order": "desc",
"limitPercent": 25
}`
pls := &model.Playlist{}
err := s.parseNSP(ctx, pls, strings.NewReader(nsp))
Expect(err).ToNot(HaveOccurred())
Expect(pls.Rules).ToNot(BeNil())
Expect(pls.Rules.LimitPercent).To(Equal(25))
Expect(pls.Rules.Limit).To(Equal(0))
})
It("parses criteria with multiple rules", func() {
nsp := `{
"all": [