mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat(playlists): add SmartPlaylistEvaluator for background evaluation
Implements the CacheWarmer pattern: a background goroutine processes batched playlist IDs, evaluating smart playlist criteria and populating tracks asynchronously. Includes a noop implementation for CLI/test use. Part of #4539
This commit is contained in:
parent
759ab26b19
commit
7ce0d3e79f
107
core/playlists/evaluator.go
Normal file
107
core/playlists/evaluator.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package playlists
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/auth"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SmartPlaylistEvaluator evaluates smart playlists in the background.
|
||||||
|
// Call Enqueue to queue a playlist for evaluation. The evaluation happens
|
||||||
|
// asynchronously in a background goroutine.
|
||||||
|
type SmartPlaylistEvaluator interface {
|
||||||
|
Enqueue(playlistID string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmartPlaylistEvaluator(ds model.DataStore) SmartPlaylistEvaluator {
|
||||||
|
e := &smartPlaylistEvaluator{
|
||||||
|
ds: ds,
|
||||||
|
buffer: make(map[string]struct{}),
|
||||||
|
wakeSignal: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
go e.run()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type smartPlaylistEvaluator struct {
|
||||||
|
ds model.DataStore
|
||||||
|
buffer map[string]struct{}
|
||||||
|
mutex sync.Mutex
|
||||||
|
wakeSignal chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) Enqueue(playlistID string) {
|
||||||
|
e.mutex.Lock()
|
||||||
|
defer e.mutex.Unlock()
|
||||||
|
e.buffer[playlistID] = struct{}{}
|
||||||
|
e.sendWakeSignal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) sendWakeSignal() {
|
||||||
|
select {
|
||||||
|
case e.wakeSignal <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) run() {
|
||||||
|
for {
|
||||||
|
e.waitSignal(10 * time.Second)
|
||||||
|
|
||||||
|
e.mutex.Lock()
|
||||||
|
if len(e.buffer) == 0 {
|
||||||
|
e.mutex.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := slices.Collect(maps.Keys(e.buffer))
|
||||||
|
e.buffer = make(map[string]struct{})
|
||||||
|
e.mutex.Unlock()
|
||||||
|
|
||||||
|
e.processBatch(batch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) waitSignal(timeout time.Duration) {
|
||||||
|
select {
|
||||||
|
case <-time.After(timeout):
|
||||||
|
case <-e.wakeSignal:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) processBatch(batch []string) {
|
||||||
|
log.Debug("Evaluating smart playlists in background", "count", len(batch))
|
||||||
|
for _, id := range batch {
|
||||||
|
e.doEvaluate(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) doEvaluate(id string) {
|
||||||
|
// Use admin context so userFilter() returns all playlists.
|
||||||
|
// Evaluate() internally uses pls.OwnerID for annotation JOINs.
|
||||||
|
ctx := auth.WithAdminUser(context.TODO(), e.ds)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
err := e.ds.Playlist(ctx).Evaluate(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error evaluating smart playlist in background", "id", id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug("Background smart playlist evaluation complete", "id", id, "elapsed", time.Since(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoopSmartPlaylistEvaluator returns an evaluator that does nothing.
|
||||||
|
// Used in CLI scan and test contexts.
|
||||||
|
func NoopSmartPlaylistEvaluator() SmartPlaylistEvaluator {
|
||||||
|
return &noopSmartPlaylistEvaluator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopSmartPlaylistEvaluator struct{}
|
||||||
|
|
||||||
|
func (n *noopSmartPlaylistEvaluator) Enqueue(string) {}
|
||||||
17
core/playlists/evaluator_test.go
Normal file
17
core/playlists/evaluator_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package playlists_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/playlists"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("SmartPlaylistEvaluator", func() {
|
||||||
|
Describe("NoopSmartPlaylistEvaluator", func() {
|
||||||
|
It("does not panic when enqueuing", func() {
|
||||||
|
noop := playlists.NoopSmartPlaylistEvaluator()
|
||||||
|
Expect(func() { noop.Enqueue("some-id") }).ToNot(Panic())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user