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