diff --git a/backend/app.go b/backend/app.go index c4a659c..bb032ce 100644 --- a/backend/app.go +++ b/backend/app.go @@ -198,8 +198,17 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) { // orchestrate queue clearing and playlist restoration // without depending on those packages directly. yj.library.SetRescanHooks(library.RescanHooks{ - PreClear: yj.queue.Clear, - PostScan: yj.playlist.RestoreAllPlaylists, + PreClear: func() { + yj.queue.Clear() + // Stop the search index build so it doesn't fight + // with the rescan for DB access. + yj.explore.StopIndexBuild() + }, + PostScan: func() { + yj.playlist.RestoreAllPlaylists() + // Restart the index build now that the scan is done. + yj.explore.StartIndexBuild() + }, }) // Wire scan hooks so the playlist service can resolve @@ -319,5 +328,9 @@ func (yj *YellowJacketApp) OnDomReady(ctx context.Context) { if err := yj.library.SoftScanAllLibraries(); err != nil { yj.logger.Error("soft scan failed", "err", err) } + + // Start the explore search index build AFTER the library + // scan completes so they don't fight for DB access. + yj.explore.StartIndexBuild() }() } diff --git a/backend/explore/explore.go b/backend/explore/explore.go index b98b5f8..5b6c765 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -63,7 +63,19 @@ func NewExploreService(logger *slog.Logger, db *database.DB) *Service { // OnStartup after the Wails runtime is initialised. func (e *Service) SetContext(ctx context.Context) { e.ctx = ctx - e.index.StartBuild(ctx) +} + +// StartIndexBuild kicks off the background search index build. +// Call this after the library scan completes so the indexer doesn't +// starve the scan for DB access. +func (e *Service) StartIndexBuild() { + e.index.StartBuild(e.ctx) +} + +// StopIndexBuild cancels the background search index build. +// Call before a full rescan to free the DB for the scan. +func (e *Service) StopIndexBuild() { + e.index.StopBuild() } // --------------------------------------------------------------------------- diff --git a/backend/explore/searchindex.go b/backend/explore/searchindex.go index ededa88..e90e804 100644 --- a/backend/explore/searchindex.go +++ b/backend/explore/searchindex.go @@ -128,30 +128,57 @@ func NewSearchIndex( lb: lb, artistImg: artistImg, logger: logger, - done: make(chan struct{}), } } // StartBuild launches the background index build goroutine. // Returns immediately. func (si *SearchIndex) StartBuild(ctx context.Context) { + si.mu.Lock() + // Don't start if already running. + if si.cancel != nil { + si.mu.Unlock() + + return + } + + si.done = make(chan struct{}) + si.mu.Unlock() + buildCtx, cancel := context.WithCancel(ctx) + + si.mu.Lock() si.cancel = cancel + si.mu.Unlock() go func() { - defer close(si.done) + defer func() { + si.mu.Lock() + si.cancel = nil + si.mu.Unlock() + + close(si.done) + }() si.build(buildCtx) }() } // StopBuild cancels an in-flight build and waits for it to finish. +// Safe to call even if no build is running. func (si *SearchIndex) StopBuild() { - if si.cancel != nil { - si.cancel() + si.mu.RLock() + cancel := si.cancel + done := si.done + si.mu.RUnlock() + + if cancel != nil { + cancel() } - <-si.done + if done != nil { + <-done + } } // IsReady returns true once the index has been built at least once.