diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index bf13dc731..d7b6a3ad2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -69,9 +69,9 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router { artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() - scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) - watcher := scanner.GetWatcher(dataStore, scannerScanner) - library := core.NewLibrary(dataStore, scannerScanner, watcher, broker) + modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) + watcher := scanner.GetWatcher(dataStore, modelScanner) + library := core.NewLibrary(dataStore, modelScanner, watcher, broker) maintenance := core.NewMaintenance(dataStore) router := nativeapi.New(dataStore, share, playlists, insights, library, maintenance) return router @@ -95,10 +95,10 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router { cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) - scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) + modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager) playbackServer := playback.GetInstance(dataStore) - router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, scannerScanner, broker, playlists, playTracker, share, playbackServer, metricsMetrics) + router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlists, playTracker, share, playbackServer, metricsMetrics) return router } @@ -150,7 +150,7 @@ func CreatePrometheus() metrics.Metrics { return metricsMetrics } -func CreateScanner(ctx context.Context) scanner.Scanner { +func CreateScanner(ctx context.Context) model.Scanner { sqlDB := db.Db() dataStore := persistence.New(sqlDB) fileCache := artwork.GetImageCache() @@ -163,8 +163,8 @@ func CreateScanner(ctx context.Context) scanner.Scanner { cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) - scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) - return scannerScanner + modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) + return modelScanner } func CreateScanWatcher(ctx context.Context) scanner.Watcher { @@ -180,8 +180,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher { cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) broker := events.GetBroker() playlists := core.NewPlaylists(dataStore) - scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) - watcher := scanner.GetWatcher(dataStore, scannerScanner) + modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) + watcher := scanner.GetWatcher(dataStore, modelScanner) return watcher } @@ -202,7 +202,7 @@ func getPluginManager() plugins.Manager { // wire_injectors.go: -var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, plugins.GetManager, metrics.GetPrometheusInstance, db.Db, wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), wire.Bind(new(metrics.PluginLoader), new(plugins.Manager)), wire.Bind(new(core.Scanner), new(scanner.Scanner)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) +var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, plugins.GetManager, metrics.GetPrometheusInstance, db.Db, wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), wire.Bind(new(metrics.PluginLoader), new(plugins.Manager)), wire.Bind(new(core.Watcher), new(scanner.Watcher))) func GetPluginManager(ctx context.Context) plugins.Manager { manager := getPluginManager() diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index e8759ac53..595d406b9 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -45,7 +45,6 @@ var allProviders = wire.NewSet( wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), wire.Bind(new(metrics.PluginLoader), new(plugins.Manager)), - wire.Bind(new(core.Scanner), new(scanner.Scanner)), wire.Bind(new(core.Watcher), new(scanner.Watcher)), ) @@ -103,7 +102,7 @@ func CreatePrometheus() metrics.Metrics { )) } -func CreateScanner(ctx context.Context) scanner.Scanner { +func CreateScanner(ctx context.Context) model.Scanner { panic(wire.Build( allProviders, )) diff --git a/core/library.go b/core/library.go index 8e17445ec..f4f55ec5a 100644 --- a/core/library.go +++ b/core/library.go @@ -21,11 +21,6 @@ import ( "github.com/navidrome/navidrome/utils/slice" ) -// Scanner interface for triggering scans. This is a subset of the full scanner.Scanner interface. -type Scanner interface { - ScanAll(ctx context.Context, fullScan bool) (warnings []string, err error) -} - // Watcher interface for managing file system watchers type Watcher interface { Watch(ctx context.Context, lib *model.Library) error @@ -43,13 +38,13 @@ type Library interface { type libraryService struct { ds model.DataStore - scanner Scanner + scanner model.Scanner watcher Watcher broker events.Broker } // NewLibrary creates a new Library service -func NewLibrary(ds model.DataStore, scanner Scanner, watcher Watcher, broker events.Broker) Library { +func NewLibrary(ds model.DataStore, scanner model.Scanner, watcher Watcher, broker events.Broker) Library { return &libraryService{ ds: ds, scanner: scanner, @@ -155,7 +150,7 @@ type libraryRepositoryWrapper struct { model.LibraryRepository ctx context.Context ds model.DataStore - scanner Scanner + scanner model.Scanner watcher Watcher broker events.Broker } @@ -192,7 +187,7 @@ func (r *libraryRepositoryWrapper) Save(entity interface{}) (string, error) { return strconv.Itoa(lib.ID), nil } -func (r *libraryRepositoryWrapper) Update(id string, entity interface{}, cols ...string) error { +func (r *libraryRepositoryWrapper) Update(id string, entity interface{}, _ ...string) error { lib := entity.(*model.Library) libID, err := strconv.Atoi(id) if err != nil { diff --git a/core/library_test.go b/core/library_test.go index bfbb4300a..bf73a62b7 100644 --- a/core/library_test.go +++ b/core/library_test.go @@ -29,7 +29,7 @@ var _ = Describe("Library Service", func() { var userRepo *tests.MockedUserRepo var ctx context.Context var tempDir string - var scanner *mockScanner + var scanner *tests.MockScanner var watcherManager *mockWatcherManager var broker *mockEventBroker @@ -43,7 +43,7 @@ var _ = Describe("Library Service", func() { ds.MockedUser = userRepo // Create a mock scanner that tracks calls - scanner = &mockScanner{} + scanner = tests.NewMockScanner() // Create a mock watcher manager watcherManager = &mockWatcherManager{ libraryStates: make(map[int]model.Library), @@ -616,11 +616,12 @@ var _ = Describe("Library Service", func() { // Wait briefly for the goroutine to complete Eventually(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "1s", "10ms").Should(Equal(1)) // Verify scan was called with correct parameters - Expect(scanner.ScanCalls[0].FullScan).To(BeFalse()) // Should be quick scan + calls := scanner.GetScanAllCalls() + Expect(calls[0].FullScan).To(BeFalse()) // Should be quick scan }) It("triggers scan when updating library path", func() { @@ -641,11 +642,12 @@ var _ = Describe("Library Service", func() { // Wait briefly for the goroutine to complete Eventually(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "1s", "10ms").Should(Equal(1)) // Verify scan was called with correct parameters - Expect(scanner.ScanCalls[0].FullScan).To(BeFalse()) // Should be quick scan + calls := scanner.GetScanAllCalls() + Expect(calls[0].FullScan).To(BeFalse()) // Should be quick scan }) It("does not trigger scan when updating library without path change", func() { @@ -661,7 +663,7 @@ var _ = Describe("Library Service", func() { // Wait a bit to ensure no scan was triggered Consistently(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "100ms", "10ms").Should(Equal(0)) }) @@ -674,7 +676,7 @@ var _ = Describe("Library Service", func() { // Ensure no scan was triggered since creation failed Consistently(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "100ms", "10ms").Should(Equal(0)) }) @@ -691,7 +693,7 @@ var _ = Describe("Library Service", func() { // Ensure no scan was triggered since update failed Consistently(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "100ms", "10ms").Should(Equal(0)) }) @@ -707,11 +709,12 @@ var _ = Describe("Library Service", func() { // Wait briefly for the goroutine to complete Eventually(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "1s", "10ms").Should(Equal(1)) // Verify scan was called with correct parameters - Expect(scanner.ScanCalls[0].FullScan).To(BeFalse()) // Should be quick scan + calls := scanner.GetScanAllCalls() + Expect(calls[0].FullScan).To(BeFalse()) // Should be quick scan }) It("does not trigger scan when library deletion fails", func() { @@ -721,7 +724,7 @@ var _ = Describe("Library Service", func() { // Ensure no scan was triggered since deletion failed Consistently(func() int { - return scanner.len() + return scanner.GetScanAllCallCount() }, "100ms", "10ms").Should(Equal(0)) }) @@ -868,31 +871,6 @@ var _ = Describe("Library Service", func() { }) }) -// mockScanner provides a simple mock implementation of core.Scanner for testing -type mockScanner struct { - ScanCalls []ScanCall - mu sync.RWMutex -} - -type ScanCall struct { - FullScan bool -} - -func (m *mockScanner) ScanAll(ctx context.Context, fullScan bool) (warnings []string, err error) { - m.mu.Lock() - defer m.mu.Unlock() - m.ScanCalls = append(m.ScanCalls, ScanCall{ - FullScan: fullScan, - }) - return []string{}, nil -} - -func (m *mockScanner) len() int { - m.mu.RLock() - defer m.mu.RUnlock() - return len(m.ScanCalls) -} - // mockWatcherManager provides a simple mock implementation of core.Watcher for testing type mockWatcherManager struct { StartedWatchers []model.Library diff --git a/model/scanner.go b/model/scanner.go index 756688a23..12a386ea0 100644 --- a/model/scanner.go +++ b/model/scanner.go @@ -1,6 +1,7 @@ package model import ( + "context" "fmt" "time" ) @@ -26,3 +27,12 @@ type ScannerStatus struct { ScanType string ElapsedTime time.Duration } + +type Scanner interface { + // ScanAll starts a scan of all libraries. This is a blocking operation. + ScanAll(ctx context.Context, fullScan bool) (warnings []string, err error) + // ScanFolders scans specific library/folder pairs, recursing into subdirectories. + // If targets is nil, it scans all libraries. This is a blocking operation. + ScanFolders(ctx context.Context, fullScan bool, targets []ScanTarget) (warnings []string, err error) + Status(context.Context) (*ScannerStatus, error) +} diff --git a/scanner/controller.go b/scanner/controller.go index b5ba2ddb7..8e308f51d 100644 --- a/scanner/controller.go +++ b/scanner/controller.go @@ -69,17 +69,8 @@ func ParseTargets(libFolders []string) ([]model.ScanTarget, error) { return targets, nil } -type Scanner interface { - // ScanAll starts a scan of all libraries. This is a blocking operation. - ScanAll(ctx context.Context, fullScan bool) (warnings []string, err error) - // ScanFolders scans specific library/folder pairs, recursing into subdirectories. - // If targets is nil, it scans all libraries. This is a blocking operation. - ScanFolders(ctx context.Context, fullScan bool, targets []model.ScanTarget) (warnings []string, err error) - Status(context.Context) (*model.ScannerStatus, error) -} - func New(rootCtx context.Context, ds model.DataStore, cw artwork.CacheWarmer, broker events.Broker, - pls core.Playlists, m metrics.Metrics) Scanner { + pls core.Playlists, m metrics.Metrics) model.Scanner { c := &controller{ rootCtx: rootCtx, ds: ds, diff --git a/scanner/controller_test.go b/scanner/controller_test.go index d1109b0be..929fa09ba 100644 --- a/scanner/controller_test.go +++ b/scanner/controller_test.go @@ -9,6 +9,7 @@ import ( "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/db" + "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server/events" @@ -20,7 +21,7 @@ import ( var _ = Describe("Controller", func() { var ctx context.Context var ds *tests.MockDataStore - var ctrl scanner.Scanner + var ctrl model.Scanner Describe("Status", func() { BeforeEach(func() { diff --git a/scanner/scanner_multilibrary_test.go b/scanner/scanner_multilibrary_test.go index f27ad52fc..66db62edf 100644 --- a/scanner/scanner_multilibrary_test.go +++ b/scanner/scanner_multilibrary_test.go @@ -32,7 +32,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() { var ctx context.Context var lib1, lib2 model.Library var ds *tests.MockDataStore - var s scanner.Scanner + var s model.Scanner createFS := func(path string, files fstest.MapFS) storagetest.FakeFS { fs := storagetest.FakeFS{} diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index e1b2e6f32..604561058 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -39,7 +39,7 @@ var _ = Describe("Scanner", Ordered, func() { var lib model.Library var ds *tests.MockDataStore var mfRepo *mockMediaFileRepo - var s scanner.Scanner + var s model.Scanner createFS := func(files fstest.MapFS) storagetest.FakeFS { fs := storagetest.FakeFS{} diff --git a/scanner/selective_scan_test.go b/scanner/selective_scan_test.go index 4bac1c2e1..13d0f02cb 100644 --- a/scanner/selective_scan_test.go +++ b/scanner/selective_scan_test.go @@ -28,7 +28,7 @@ var _ = Describe("Selective Scan - Deleted Child Folders", Ordered, func() { var ctx context.Context var lib model.Library var ds model.DataStore - var s scanner.Scanner + var s model.Scanner var fsys storagetest.FakeFS BeforeAll(func() { diff --git a/scanner/watcher.go b/scanner/watcher.go index 911ea310b..849ddf91a 100644 --- a/scanner/watcher.go +++ b/scanner/watcher.go @@ -24,7 +24,7 @@ type Watcher interface { type watcher struct { mainCtx context.Context ds model.DataStore - scanner Scanner + scanner model.Scanner triggerWait time.Duration watcherNotify chan scanNotification libraryWatchers map[int]*libraryWatcherInstance @@ -42,7 +42,7 @@ type scanNotification struct { } // GetWatcher returns the watcher singleton -func GetWatcher(ds model.DataStore, s Scanner) Watcher { +func GetWatcher(ds model.DataStore, s model.Scanner) Watcher { return singleton.GetInstance(func() *watcher { return &watcher{ ds: ds, diff --git a/server/subsonic/api.go b/server/subsonic/api.go index d08d3eb5b..f0e73c3d2 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -18,7 +18,6 @@ import ( "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/server/subsonic/responses" @@ -39,7 +38,7 @@ type Router struct { players core.Players provider external.Provider playlists core.Playlists - scanner scanner.Scanner + scanner model.Scanner broker events.Broker scrobbler scrobbler.PlayTracker share core.Share @@ -48,7 +47,7 @@ type Router struct { } func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver, - players core.Players, provider external.Provider, scanner scanner.Scanner, broker events.Broker, + players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker, playlists core.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer, metrics metrics.Metrics, ) *Router {