mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Merge 359ea3c61cda4781f91dc48e882c15170996f417 into 44e63596a08c83471eaa56b132762267899af44a
This commit is contained in:
commit
c6b686a89d
@ -76,7 +76,8 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
|||||||
provider := external.NewProvider(dataStore, agentsAgents, matcherMatcher)
|
provider := external.NewProvider(dataStore, agentsAgents, matcherMatcher)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics)
|
smartPlaylistEvaluator := playlists.NewSmartPlaylistEvaluator(dataStore)
|
||||||
|
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, smartPlaylistEvaluator, metricsMetrics)
|
||||||
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
||||||
library := core.NewLibrary(dataStore, modelScanner, watcher, broker, manager)
|
library := core.NewLibrary(dataStore, modelScanner, watcher, broker, manager)
|
||||||
user := core.NewUser(dataStore, manager)
|
user := core.NewUser(dataStore, manager)
|
||||||
@ -105,7 +106,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||||
imageUploadService := core.NewImageUploadService()
|
imageUploadService := core.NewImageUploadService()
|
||||||
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
|
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
|
||||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics)
|
smartPlaylistEvaluator := playlists.NewSmartPlaylistEvaluator(dataStore)
|
||||||
|
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, smartPlaylistEvaluator, metricsMetrics)
|
||||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
|
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
|
||||||
playbackServer := playback.GetInstance(dataStore)
|
playbackServer := playback.GetInstance(dataStore)
|
||||||
lyricsLyrics := lyrics.NewLyrics(manager)
|
lyricsLyrics := lyrics.NewLyrics(manager)
|
||||||
@ -177,7 +179,8 @@ func CreateScanner(ctx context.Context) model.Scanner {
|
|||||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||||
imageUploadService := core.NewImageUploadService()
|
imageUploadService := core.NewImageUploadService()
|
||||||
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
|
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
|
||||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics)
|
smartPlaylistEvaluator := playlists.NewSmartPlaylistEvaluator(dataStore)
|
||||||
|
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, smartPlaylistEvaluator, metricsMetrics)
|
||||||
return modelScanner
|
return modelScanner
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +199,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
|
|||||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||||
imageUploadService := core.NewImageUploadService()
|
imageUploadService := core.NewImageUploadService()
|
||||||
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
|
playlistsPlaylists := playlists.NewPlaylists(dataStore, imageUploadService)
|
||||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, metricsMetrics)
|
smartPlaylistEvaluator := playlists.NewSmartPlaylistEvaluator(dataStore)
|
||||||
|
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlistsPlaylists, smartPlaylistEvaluator, metricsMetrics)
|
||||||
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
||||||
return watcher
|
return watcher
|
||||||
}
|
}
|
||||||
|
|||||||
105
core/playlists/evaluator.go
Normal file
105
core/playlists/evaluator.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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) {
|
||||||
|
timer := time.NewTimer(timeout)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
case <-e.wakeSignal:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *smartPlaylistEvaluator) processBatch(batch []string) {
|
||||||
|
// Use admin context so userFilter() returns all playlists.
|
||||||
|
// Evaluate() internally uses pls.OwnerID for annotation JOINs.
|
||||||
|
ctx := auth.WithAdminUser(context.TODO(), e.ds)
|
||||||
|
|
||||||
|
log.Debug(ctx, "Evaluating smart playlists in background", "count", len(batch))
|
||||||
|
for _, id := range batch {
|
||||||
|
start := time.Now()
|
||||||
|
err := e.ds.Playlist(ctx).Evaluate(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Error evaluating smart playlist in background", "id", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debug(ctx, "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())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -21,6 +21,7 @@ var Set = wire.NewSet(
|
|||||||
NewPlayers,
|
NewPlayers,
|
||||||
NewShare,
|
NewShare,
|
||||||
playlists.NewPlaylists,
|
playlists.NewPlaylists,
|
||||||
|
playlists.NewSmartPlaylistEvaluator,
|
||||||
NewLibrary,
|
NewLibrary,
|
||||||
NewUser,
|
NewUser,
|
||||||
NewMaintenance,
|
NewMaintenance,
|
||||||
|
|||||||
@ -131,6 +131,7 @@ type PlaylistRepository interface {
|
|||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
|
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
|
||||||
GetPlaylists(mediaFileId string) (Playlists, error)
|
GetPlaylists(mediaFileId string) (Playlists, error)
|
||||||
|
Evaluate(id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaylistTrack struct {
|
type PlaylistTrack struct {
|
||||||
|
|||||||
@ -310,6 +310,22 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playlistRepository) Evaluate(id string) error {
|
||||||
|
pls, err := r.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !pls.IsSmartPlaylist() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Reset EvaluatedAt so refreshSmartPlaylist won't skip due to delay check
|
||||||
|
pls.EvaluatedAt = nil
|
||||||
|
if !r.refreshSmartPlaylist(pls) {
|
||||||
|
return fmt.Errorf("failed to evaluate smart playlist %s", id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *playlistRepository) addSmartPlaylistAnnotationJoins(sq SelectBuilder, joins criteria.JoinType, userID string) SelectBuilder {
|
func (r *playlistRepository) addSmartPlaylistAnnotationJoins(sq SelectBuilder, joins criteria.JoinType, userID string) SelectBuilder {
|
||||||
if joins.Has(criteria.JoinAlbumAnnotation) {
|
if joins.Has(criteria.JoinAlbumAnnotation) {
|
||||||
sq = sq.LeftJoin("annotation AS album_annotation ON ("+
|
sq = sq.LeftJoin("annotation AS album_annotation ON ("+
|
||||||
|
|||||||
@ -254,6 +254,57 @@ var _ = Describe("PlaylistRepository", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("Evaluate", func() {
|
||||||
|
var testPlaylistID string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
if testPlaylistID != "" {
|
||||||
|
_ = repo.Delete(testPlaylistID)
|
||||||
|
testPlaylistID = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("evaluates a smart playlist and sets EvaluatedAt and SongCount", func() {
|
||||||
|
rules := &criteria.Criteria{
|
||||||
|
Expression: criteria.All{
|
||||||
|
criteria.Contains{"title": "Day"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
newPls := model.Playlist{Name: "Evaluate Test", OwnerID: "userid", Rules: rules}
|
||||||
|
Expect(repo.Put(&newPls)).To(Succeed())
|
||||||
|
testPlaylistID = newPls.ID
|
||||||
|
|
||||||
|
Expect(repo.Evaluate(newPls.ID)).To(Succeed())
|
||||||
|
|
||||||
|
saved, err := repo.Get(newPls.ID)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(saved.EvaluatedAt).ToNot(BeNil())
|
||||||
|
Expect(*saved.EvaluatedAt).To(BeTemporally("~", time.Now(), 2*time.Second))
|
||||||
|
Expect(saved.SongCount).To(BeNumerically(">", 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("is a no-op for non-smart playlists", func() {
|
||||||
|
newPls := model.Playlist{Name: "Regular Playlist", OwnerID: "userid"}
|
||||||
|
Expect(repo.Put(&newPls)).To(Succeed())
|
||||||
|
testPlaylistID = newPls.ID
|
||||||
|
|
||||||
|
Expect(repo.Evaluate(newPls.ID)).To(Succeed())
|
||||||
|
|
||||||
|
saved, err := repo.Get(newPls.ID)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(saved.EvaluatedAt).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound for a non-existent playlist ID", func() {
|
||||||
|
err := repo.Evaluate("nonexistent-id")
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Describe("Playlist Track Sorting", func() {
|
Describe("Playlist Track Sorting", func() {
|
||||||
var testPlaylistID string
|
var testPlaylistID string
|
||||||
|
|
||||||
|
|||||||
@ -27,13 +27,14 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func New(rootCtx context.Context, ds model.DataStore, cw artwork.CacheWarmer, broker events.Broker,
|
func New(rootCtx context.Context, ds model.DataStore, cw artwork.CacheWarmer, broker events.Broker,
|
||||||
pls playlists.Playlists, m metrics.Metrics) model.Scanner {
|
pls playlists.Playlists, spe playlists.SmartPlaylistEvaluator, m metrics.Metrics) model.Scanner {
|
||||||
c := &controller{
|
c := &controller{
|
||||||
rootCtx: rootCtx,
|
rootCtx: rootCtx,
|
||||||
ds: ds,
|
ds: ds,
|
||||||
cw: cw,
|
cw: cw,
|
||||||
broker: broker,
|
broker: broker,
|
||||||
pls: pls,
|
pls: pls,
|
||||||
|
spe: spe,
|
||||||
metrics: m,
|
metrics: m,
|
||||||
devExternalScanner: conf.Server.DevExternalScanner,
|
devExternalScanner: conf.Server.DevExternalScanner,
|
||||||
}
|
}
|
||||||
@ -47,7 +48,7 @@ func (s *controller) getScanner() scanner {
|
|||||||
if s.devExternalScanner {
|
if s.devExternalScanner {
|
||||||
return &scannerExternal{}
|
return &scannerExternal{}
|
||||||
}
|
}
|
||||||
return &scannerImpl{ds: s.ds, cw: s.cw, pls: s.pls}
|
return &scannerImpl{ds: s.ds, cw: s.cw, pls: s.pls, spe: s.spe}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallScan starts an in-process scan of specific library/folder pairs.
|
// CallScan starts an in-process scan of specific library/folder pairs.
|
||||||
@ -64,7 +65,7 @@ func CallScan(ctx context.Context, ds model.DataStore, pls playlists.Playlists,
|
|||||||
progress := make(chan *ProgressInfo, 100)
|
progress := make(chan *ProgressInfo, 100)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(progress)
|
defer close(progress)
|
||||||
scanner := &scannerImpl{ds: ds, cw: artwork.NoopCacheWarmer(), pls: pls}
|
scanner := &scannerImpl{ds: ds, cw: artwork.NoopCacheWarmer(), pls: pls, spe: playlists.NoopSmartPlaylistEvaluator()}
|
||||||
scanner.scanFolders(ctx, fullScan, targets, progress)
|
scanner.scanFolders(ctx, fullScan, targets, progress)
|
||||||
}()
|
}()
|
||||||
return progress, nil
|
return progress, nil
|
||||||
@ -99,6 +100,7 @@ type controller struct {
|
|||||||
broker events.Broker
|
broker events.Broker
|
||||||
metrics metrics.Metrics
|
metrics metrics.Metrics
|
||||||
pls playlists.Playlists
|
pls playlists.Playlists
|
||||||
|
spe playlists.SmartPlaylistEvaluator
|
||||||
limiter *rate.Sometimes
|
limiter *rate.Sometimes
|
||||||
devExternalScanner bool
|
devExternalScanner bool
|
||||||
count atomic.Uint32
|
count atomic.Uint32
|
||||||
|
|||||||
@ -32,7 +32,7 @@ var _ = Describe("Controller", func() {
|
|||||||
DeferCleanup(configtest.SetupConfig())
|
DeferCleanup(configtest.SetupConfig())
|
||||||
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||||
ds.MockedProperty = &tests.MockedPropertyRepo{}
|
ds.MockedProperty = &tests.MockedPropertyRepo{}
|
||||||
ctrl = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
ctrl = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("includes last scan error", func() {
|
It("includes last scan error", func() {
|
||||||
|
|||||||
@ -23,16 +23,19 @@ type phasePlaylists struct {
|
|||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
pls playlists.Playlists
|
pls playlists.Playlists
|
||||||
cw artwork.CacheWarmer
|
cw artwork.CacheWarmer
|
||||||
|
spe playlists.SmartPlaylistEvaluator
|
||||||
refreshed atomic.Uint32
|
refreshed atomic.Uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPhasePlaylists(ctx context.Context, scanState *scanState, ds model.DataStore, pls playlists.Playlists, cw artwork.CacheWarmer) *phasePlaylists {
|
func createPhasePlaylists(ctx context.Context, scanState *scanState, ds model.DataStore,
|
||||||
|
pls playlists.Playlists, cw artwork.CacheWarmer, spe playlists.SmartPlaylistEvaluator) *phasePlaylists {
|
||||||
return &phasePlaylists{
|
return &phasePlaylists{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
scanState: scanState,
|
scanState: scanState,
|
||||||
ds: ds,
|
ds: ds,
|
||||||
pls: pls,
|
pls: pls,
|
||||||
cw: cw,
|
cw: cw,
|
||||||
|
spe: spe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +108,8 @@ func (p *phasePlaylists) processPlaylistsInFolder(folder *model.Folder) (*model.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if pls.IsSmartPlaylist() {
|
if pls.IsSmartPlaylist() {
|
||||||
log.Debug("Scanner: Imported smart playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "elapsed", time.Since(started))
|
p.spe.Enqueue(pls.ID)
|
||||||
|
log.Debug(p.ctx, "Scanner: Imported smart playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "elapsed", time.Since(started))
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Scanner: Imported playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks), "elapsed", time.Since(started))
|
log.Debug("Scanner: Imported playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks), "elapsed", time.Since(started))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ var _ = Describe("phasePlaylists", func() {
|
|||||||
pls = &mockPlaylists{}
|
pls = &mockPlaylists{}
|
||||||
cw = artwork.NoopCacheWarmer()
|
cw = artwork.NoopCacheWarmer()
|
||||||
state = &scanState{}
|
state = &scanState{}
|
||||||
phase = createPhasePlaylists(ctx, state, ds, pls, cw)
|
phase = createPhasePlaylists(ctx, state, ds, pls, cw, playlists.NoopSmartPlaylistEvaluator())
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("description", func() {
|
Describe("description", func() {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type scannerImpl struct {
|
|||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
cw artwork.CacheWarmer
|
cw artwork.CacheWarmer
|
||||||
pls playlists.Playlists
|
pls playlists.Playlists
|
||||||
|
spe playlists.SmartPlaylistEvaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanState holds the state of an in-progress scan, to be passed to the various phases
|
// scanState holds the state of an in-progress scan, to be passed to the various phases
|
||||||
@ -148,7 +149,7 @@ func (s *scannerImpl) scanFolders(ctx context.Context, fullScan bool, targets []
|
|||||||
runPhase[*model.Album](ctx, 3, createPhaseRefreshAlbums(ctx, &state, s.ds)),
|
runPhase[*model.Album](ctx, 3, createPhaseRefreshAlbums(ctx, &state, s.ds)),
|
||||||
|
|
||||||
// Phase 4: Import/update playlists
|
// Phase 4: Import/update playlists
|
||||||
runPhase[*model.Folder](ctx, 4, createPhasePlaylists(ctx, &state, s.ds, s.pls, s.cw)),
|
runPhase[*model.Folder](ctx, 4, createPhasePlaylists(ctx, &state, s.ds, s.pls, s.cw, s.spe)),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Final Steps (cannot be parallelized):
|
// Final Steps (cannot be parallelized):
|
||||||
|
|||||||
@ -41,7 +41,7 @@ func BenchmarkScan(b *testing.B) {
|
|||||||
ds := persistence.New(db.Db())
|
ds := persistence.New(db.Db())
|
||||||
conf.Server.DevExternalScanner = false
|
conf.Server.DevExternalScanner = false
|
||||||
s := scanner.New(context.Background(), ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s := scanner.New(context.Background(), ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
|
|
||||||
fs := storagetest.FakeFS{}
|
fs := storagetest.FakeFS{}
|
||||||
storagetest.Register("fake", &fs)
|
storagetest.Register("fake", &fs)
|
||||||
|
|||||||
@ -79,7 +79,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
|
|||||||
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
||||||
|
|
||||||
s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
|
|
||||||
// Create two test libraries (let DB auto-assign IDs)
|
// Create two test libraries (let DB auto-assign IDs)
|
||||||
lib1 = model.Library{Name: "Rock Collection", Path: "rock:///music"}
|
lib1 = model.Library{Name: "Rock Collection", Path: "rock:///music"}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ var _ = Describe("ScanFolders", Ordered, func() {
|
|||||||
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
||||||
|
|
||||||
s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
|
|
||||||
lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"}
|
lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"}
|
||||||
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())
|
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())
|
||||||
|
|||||||
@ -85,7 +85,7 @@ var _ = Describe("Scanner", Ordered, func() {
|
|||||||
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
||||||
|
|
||||||
s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
|
|
||||||
lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"}
|
lib = model.Library{ID: 1, Name: "Fake Library", Path: "fake:///music"}
|
||||||
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())
|
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())
|
||||||
|
|||||||
@ -458,7 +458,7 @@ var _ = BeforeSuite(func() {
|
|||||||
|
|
||||||
buildTestFS()
|
buildTestFS()
|
||||||
s := scanner.New(ctx, initDS, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s := scanner.New(ctx, initDS, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(initDS, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(initDS, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
_, err = s.ScanAll(ctx, true)
|
_, err = s.ScanAll(ctx, true)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
@ -502,7 +502,7 @@ func setupTestDB() {
|
|||||||
streamerSpy = &spyStreamer{}
|
streamerSpy = &spyStreamer{}
|
||||||
decider := stream.NewTranscodeDecider(ds, noopFFmpeg{})
|
decider := stream.NewTranscodeDecider(ds, noopFFmpeg{})
|
||||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
router = subsonic.New(
|
router = subsonic.New(
|
||||||
ds,
|
ds,
|
||||||
noopArtwork{},
|
noopArtwork{},
|
||||||
|
|||||||
@ -54,7 +54,7 @@ var _ = Describe("Multi-Library Support", Ordered, func() {
|
|||||||
|
|
||||||
// Run incremental scan to import lib2 content (lib1 files unchanged → skipped)
|
// Run incremental scan to import lib2 content (lib1 files unchanged → skipped)
|
||||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||||
playlists.NewPlaylists(ds, core.NewImageUploadService()), metrics.NewNoopInstance())
|
playlists.NewPlaylists(ds, core.NewImageUploadService()), playlists.NoopSmartPlaylistEvaluator(), metrics.NewNoopInstance())
|
||||||
_, err = s.ScanAll(ctx, false)
|
_, err = s.ScanAll(ctx, false)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
|||||||
@ -108,4 +108,11 @@ func (m *MockPlaylistRepo) CountAll(_ ...model.QueryOptions) (int64, error) {
|
|||||||
return int64(len(m.Data)), nil
|
return int64(len(m.Data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockPlaylistRepo) Evaluate(_ string) error {
|
||||||
|
if m.Err {
|
||||||
|
return errors.New("error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ model.PlaylistRepository = (*MockPlaylistRepo)(nil)
|
var _ model.PlaylistRepository = (*MockPlaylistRepo)(nil)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user