mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat(scanner): evaluate smart playlists in background after import
Wire SmartPlaylistEvaluator into the scanner pipeline. After importing a .nsp file, the scanner enqueues the playlist for background evaluation. The evaluator processes the queue asynchronously, populating tracks and updating song_count/duration/size without blocking the scan. Fixes #4539
This commit is contained in:
parent
7ce0d3e79f
commit
af0a6615b2
@ -75,7 +75,8 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
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)
|
||||
library := core.NewLibrary(dataStore, modelScanner, watcher, broker, manager)
|
||||
user := core.NewUser(dataStore, manager)
|
||||
@ -103,7 +104,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
imageUploadService := core.NewImageUploadService()
|
||||
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)
|
||||
playbackServer := playback.GetInstance(dataStore)
|
||||
lyricsLyrics := lyrics.NewLyrics(manager)
|
||||
@ -173,7 +175,8 @@ func CreateScanner(ctx context.Context) model.Scanner {
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
imageUploadService := core.NewImageUploadService()
|
||||
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
|
||||
}
|
||||
|
||||
@ -191,7 +194,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
imageUploadService := core.NewImageUploadService()
|
||||
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)
|
||||
return watcher
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ var Set = wire.NewSet(
|
||||
NewPlayers,
|
||||
NewShare,
|
||||
playlists.NewPlaylists,
|
||||
playlists.NewSmartPlaylistEvaluator,
|
||||
NewLibrary,
|
||||
NewUser,
|
||||
NewMaintenance,
|
||||
|
||||
@ -27,13 +27,14 @@ var (
|
||||
)
|
||||
|
||||
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{
|
||||
rootCtx: rootCtx,
|
||||
ds: ds,
|
||||
cw: cw,
|
||||
broker: broker,
|
||||
pls: pls,
|
||||
spe: spe,
|
||||
metrics: m,
|
||||
devExternalScanner: conf.Server.DevExternalScanner,
|
||||
}
|
||||
@ -47,7 +48,7 @@ func (s *controller) getScanner() scanner {
|
||||
if s.devExternalScanner {
|
||||
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.
|
||||
@ -64,7 +65,7 @@ func CallScan(ctx context.Context, ds model.DataStore, pls playlists.Playlists,
|
||||
progress := make(chan *ProgressInfo, 100)
|
||||
go func() {
|
||||
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)
|
||||
}()
|
||||
return progress, nil
|
||||
@ -99,6 +100,7 @@ type controller struct {
|
||||
broker events.Broker
|
||||
metrics metrics.Metrics
|
||||
pls playlists.Playlists
|
||||
spe playlists.SmartPlaylistEvaluator
|
||||
limiter *rate.Sometimes
|
||||
devExternalScanner bool
|
||||
count atomic.Uint32
|
||||
|
||||
@ -32,7 +32,7 @@ var _ = Describe("Controller", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
ds = &tests.MockDataStore{RealDS: persistence.New(db.Db())}
|
||||
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() {
|
||||
|
||||
@ -23,16 +23,19 @@ type phasePlaylists struct {
|
||||
ds model.DataStore
|
||||
pls playlists.Playlists
|
||||
cw artwork.CacheWarmer
|
||||
spe playlists.SmartPlaylistEvaluator
|
||||
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{
|
||||
ctx: ctx,
|
||||
scanState: scanState,
|
||||
ds: ds,
|
||||
pls: pls,
|
||||
cw: cw,
|
||||
spe: spe,
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +108,7 @@ func (p *phasePlaylists) processPlaylistsInFolder(folder *model.Folder) (*model.
|
||||
continue
|
||||
}
|
||||
if pls.IsSmartPlaylist() {
|
||||
p.spe.Enqueue(pls.ID)
|
||||
log.Debug("Scanner: Imported smart playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "elapsed", time.Since(started))
|
||||
} else {
|
||||
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{}
|
||||
cw = artwork.NoopCacheWarmer()
|
||||
state = &scanState{}
|
||||
phase = createPhasePlaylists(ctx, state, ds, pls, cw)
|
||||
phase = createPhasePlaylists(ctx, state, ds, pls, cw, playlists.NoopSmartPlaylistEvaluator())
|
||||
})
|
||||
|
||||
Describe("description", func() {
|
||||
|
||||
@ -24,6 +24,7 @@ type scannerImpl struct {
|
||||
ds model.DataStore
|
||||
cw artwork.CacheWarmer
|
||||
pls playlists.Playlists
|
||||
spe playlists.SmartPlaylistEvaluator
|
||||
}
|
||||
|
||||
// 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)),
|
||||
|
||||
// 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):
|
||||
|
||||
@ -41,7 +41,7 @@ func BenchmarkScan(b *testing.B) {
|
||||
ds := persistence.New(db.Db())
|
||||
conf.Server.DevExternalScanner = false
|
||||
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{}
|
||||
storagetest.Register("fake", &fs)
|
||||
|
||||
@ -78,7 +78,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
|
||||
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
||||
|
||||
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)
|
||||
lib1 = model.Library{Name: "Rock Collection", Path: "rock:///music"}
|
||||
|
||||
@ -64,7 +64,7 @@ var _ = Describe("ScanFolders", Ordered, func() {
|
||||
Expect(ds.User(ctx).Put(&adminUser)).To(Succeed())
|
||||
|
||||
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"}
|
||||
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())
|
||||
|
||||
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"}
|
||||
Expect(ds.Library(ctx).Put(&lib)).To(Succeed())
|
||||
|
||||
@ -453,7 +453,7 @@ var _ = BeforeSuite(func() {
|
||||
|
||||
buildTestFS()
|
||||
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)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
@ -490,7 +490,7 @@ func setupTestDB() {
|
||||
streamerSpy = &spyStreamer{}
|
||||
decider := stream.NewTranscodeDecider(ds, noopFFmpeg{})
|
||||
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(
|
||||
ds,
|
||||
noopArtwork{},
|
||||
|
||||
@ -54,7 +54,7 @@ var _ = Describe("Multi-Library Support", Ordered, func() {
|
||||
|
||||
// Run incremental scan to import lib2 content (lib1 files unchanged → skipped)
|
||||
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)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user