feat(smartplaylist): per-playlist refreshDelay for stable daily/weekly playlists (#5790)

* feat(utils): add ParseDuration/FormatDuration with day and week units

* feat(criteria): add per-playlist refreshDelay to smart playlist rules

* feat(smartplaylist): honor per-playlist refreshDelay in refresh gate

* feat(subsonic): compute smart playlist validUntil from effective refresh delay

* fix(playlists): reset smart playlist evaluation window when rules change via API

* refactor(utils): flatten FormatDuration recursion, single-pass duration regex

* fix(playlists): address PR review feedback

- Reset EvaluatedAt to nil instead of zero-time on rules change and NSP
  re-import, so getPlaylist(s) never reports year-1 Changed/validUntil in
  the window between an edit and the next owner read
- Parse negative d/w durations so they are rejected with the consistent
  "negative duration" error instead of "unknown unit"
- Quote input in the negative-duration error, matching the parse error
- Gate per-playlist RefreshDelay behind IsSmartPlaylist, matching its doc
This commit is contained in:
Deluan Quintão 2026-07-16 22:20:35 -04:00 • committed by GitHub
commit 85132240e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 351 additions and 6 deletions

View file

@ -168,7 +168,7 @@ func buildOSPlaylist(ctx context.Context, p model.Playlist) *responses.OpenSubso
pls.Readonly = true
if p.EvaluatedAt != nil {
pls.ValidUntil = new(p.EvaluatedAt.Add(conf.Server.SmartPlaylistRefreshDelay))
pls.ValidUntil = new(p.EvaluatedAt.Add(p.RefreshDelay()))
}
} else {
user, ok := request.UserFrom(ctx)

View file

@ -248,6 +248,20 @@ var _ = Describe("buildPlaylist", func() {
Expect(result.OpenSubsonicPlaylist).To(BeNil())
})
})
Context("with a per-playlist refreshDelay", func() {
BeforeEach(func() {
playlist.Rules.RefreshDelay = 24 * time.Hour
player := model.Player{Client: "regular-client"}
ctx = request.WithPlayer(ctx, player)
})
It("computes validUntil from the playlist's own delay", func() {
result := router.buildPlaylist(ctx, playlist)
expected := evaluatedAt.Add(24 * time.Hour)
Expect(result.ValidUntil).To(Equal(&expected))
})
})
})
Describe("annotation leakage", func() {