refactor(scanner): update scanner interface and implementations to use model.Scanner

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-11-11 16:02:03 -05:00
parent ed781e8da0
commit bfa31a246f
12 changed files with 51 additions and 78 deletions

View File

@ -69,9 +69,9 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider) artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker() broker := events.GetBroker()
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
watcher := scanner.GetWatcher(dataStore, scannerScanner) watcher := scanner.GetWatcher(dataStore, modelScanner)
library := core.NewLibrary(dataStore, scannerScanner, watcher, broker) library := core.NewLibrary(dataStore, modelScanner, watcher, broker)
maintenance := core.NewMaintenance(dataStore) maintenance := core.NewMaintenance(dataStore)
router := nativeapi.New(dataStore, share, playlists, insights, library, maintenance) router := nativeapi.New(dataStore, share, playlists, insights, library, maintenance)
return router return router
@ -95,10 +95,10 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker() broker := events.GetBroker()
playlists := core.NewPlaylists(dataStore) 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) playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
playbackServer := playback.GetInstance(dataStore) 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 return router
} }
@ -150,7 +150,7 @@ func CreatePrometheus() metrics.Metrics {
return metricsMetrics return metricsMetrics
} }
func CreateScanner(ctx context.Context) scanner.Scanner { func CreateScanner(ctx context.Context) model.Scanner {
sqlDB := db.Db() sqlDB := db.Db()
dataStore := persistence.New(sqlDB) dataStore := persistence.New(sqlDB)
fileCache := artwork.GetImageCache() fileCache := artwork.GetImageCache()
@ -163,8 +163,8 @@ func CreateScanner(ctx context.Context) scanner.Scanner {
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker() broker := events.GetBroker()
playlists := core.NewPlaylists(dataStore) playlists := core.NewPlaylists(dataStore)
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
return scannerScanner return modelScanner
} }
func CreateScanWatcher(ctx context.Context) scanner.Watcher { func CreateScanWatcher(ctx context.Context) scanner.Watcher {
@ -180,8 +180,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache) cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
broker := events.GetBroker() broker := events.GetBroker()
playlists := core.NewPlaylists(dataStore) playlists := core.NewPlaylists(dataStore)
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics) modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
watcher := scanner.GetWatcher(dataStore, scannerScanner) watcher := scanner.GetWatcher(dataStore, modelScanner)
return watcher return watcher
} }
@ -202,7 +202,7 @@ func getPluginManager() plugins.Manager {
// wire_injectors.go: // 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 { func GetPluginManager(ctx context.Context) plugins.Manager {
manager := getPluginManager() manager := getPluginManager()

View File

@ -45,7 +45,6 @@ var allProviders = wire.NewSet(
wire.Bind(new(agents.PluginLoader), new(plugins.Manager)), wire.Bind(new(agents.PluginLoader), new(plugins.Manager)),
wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(plugins.Manager)),
wire.Bind(new(metrics.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)), 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( panic(wire.Build(
allProviders, allProviders,
)) ))

View File

@ -21,11 +21,6 @@ import (
"github.com/navidrome/navidrome/utils/slice" "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 // Watcher interface for managing file system watchers
type Watcher interface { type Watcher interface {
Watch(ctx context.Context, lib *model.Library) error Watch(ctx context.Context, lib *model.Library) error
@ -43,13 +38,13 @@ type Library interface {
type libraryService struct { type libraryService struct {
ds model.DataStore ds model.DataStore
scanner Scanner scanner model.Scanner
watcher Watcher watcher Watcher
broker events.Broker broker events.Broker
} }
// NewLibrary creates a new Library service // 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{ return &libraryService{
ds: ds, ds: ds,
scanner: scanner, scanner: scanner,
@ -155,7 +150,7 @@ type libraryRepositoryWrapper struct {
model.LibraryRepository model.LibraryRepository
ctx context.Context ctx context.Context
ds model.DataStore ds model.DataStore
scanner Scanner scanner model.Scanner
watcher Watcher watcher Watcher
broker events.Broker broker events.Broker
} }
@ -192,7 +187,7 @@ func (r *libraryRepositoryWrapper) Save(entity interface{}) (string, error) {
return strconv.Itoa(lib.ID), nil 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) lib := entity.(*model.Library)
libID, err := strconv.Atoi(id) libID, err := strconv.Atoi(id)
if err != nil { if err != nil {

View File

@ -29,7 +29,7 @@ var _ = Describe("Library Service", func() {
var userRepo *tests.MockedUserRepo var userRepo *tests.MockedUserRepo
var ctx context.Context var ctx context.Context
var tempDir string var tempDir string
var scanner *mockScanner var scanner *tests.MockScanner
var watcherManager *mockWatcherManager var watcherManager *mockWatcherManager
var broker *mockEventBroker var broker *mockEventBroker
@ -43,7 +43,7 @@ var _ = Describe("Library Service", func() {
ds.MockedUser = userRepo ds.MockedUser = userRepo
// Create a mock scanner that tracks calls // Create a mock scanner that tracks calls
scanner = &mockScanner{} scanner = tests.NewMockScanner()
// Create a mock watcher manager // Create a mock watcher manager
watcherManager = &mockWatcherManager{ watcherManager = &mockWatcherManager{
libraryStates: make(map[int]model.Library), libraryStates: make(map[int]model.Library),
@ -616,11 +616,12 @@ var _ = Describe("Library Service", func() {
// Wait briefly for the goroutine to complete // Wait briefly for the goroutine to complete
Eventually(func() int { Eventually(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "1s", "10ms").Should(Equal(1)) }, "1s", "10ms").Should(Equal(1))
// Verify scan was called with correct parameters // 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() { It("triggers scan when updating library path", func() {
@ -641,11 +642,12 @@ var _ = Describe("Library Service", func() {
// Wait briefly for the goroutine to complete // Wait briefly for the goroutine to complete
Eventually(func() int { Eventually(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "1s", "10ms").Should(Equal(1)) }, "1s", "10ms").Should(Equal(1))
// Verify scan was called with correct parameters // 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() { 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 // Wait a bit to ensure no scan was triggered
Consistently(func() int { Consistently(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "100ms", "10ms").Should(Equal(0)) }, "100ms", "10ms").Should(Equal(0))
}) })
@ -674,7 +676,7 @@ var _ = Describe("Library Service", func() {
// Ensure no scan was triggered since creation failed // Ensure no scan was triggered since creation failed
Consistently(func() int { Consistently(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "100ms", "10ms").Should(Equal(0)) }, "100ms", "10ms").Should(Equal(0))
}) })
@ -691,7 +693,7 @@ var _ = Describe("Library Service", func() {
// Ensure no scan was triggered since update failed // Ensure no scan was triggered since update failed
Consistently(func() int { Consistently(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "100ms", "10ms").Should(Equal(0)) }, "100ms", "10ms").Should(Equal(0))
}) })
@ -707,11 +709,12 @@ var _ = Describe("Library Service", func() {
// Wait briefly for the goroutine to complete // Wait briefly for the goroutine to complete
Eventually(func() int { Eventually(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "1s", "10ms").Should(Equal(1)) }, "1s", "10ms").Should(Equal(1))
// Verify scan was called with correct parameters // 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() { 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 // Ensure no scan was triggered since deletion failed
Consistently(func() int { Consistently(func() int {
return scanner.len() return scanner.GetScanAllCallCount()
}, "100ms", "10ms").Should(Equal(0)) }, "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 // mockWatcherManager provides a simple mock implementation of core.Watcher for testing
type mockWatcherManager struct { type mockWatcherManager struct {
StartedWatchers []model.Library StartedWatchers []model.Library

View File

@ -1,6 +1,7 @@
package model package model
import ( import (
"context"
"fmt" "fmt"
"time" "time"
) )
@ -26,3 +27,12 @@ type ScannerStatus struct {
ScanType string ScanType string
ElapsedTime time.Duration 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)
}

View File

@ -69,17 +69,8 @@ func ParseTargets(libFolders []string) ([]model.ScanTarget, error) {
return targets, nil 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, 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{ c := &controller{
rootCtx: rootCtx, rootCtx: rootCtx,
ds: ds, ds: ds,

View File

@ -9,6 +9,7 @@ import (
"github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/core/metrics" "github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/persistence"
"github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/server/events"
@ -20,7 +21,7 @@ import (
var _ = Describe("Controller", func() { var _ = Describe("Controller", func() {
var ctx context.Context var ctx context.Context
var ds *tests.MockDataStore var ds *tests.MockDataStore
var ctrl scanner.Scanner var ctrl model.Scanner
Describe("Status", func() { Describe("Status", func() {
BeforeEach(func() { BeforeEach(func() {

View File

@ -32,7 +32,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
var ctx context.Context var ctx context.Context
var lib1, lib2 model.Library var lib1, lib2 model.Library
var ds *tests.MockDataStore var ds *tests.MockDataStore
var s scanner.Scanner var s model.Scanner
createFS := func(path string, files fstest.MapFS) storagetest.FakeFS { createFS := func(path string, files fstest.MapFS) storagetest.FakeFS {
fs := storagetest.FakeFS{} fs := storagetest.FakeFS{}

View File

@ -39,7 +39,7 @@ var _ = Describe("Scanner", Ordered, func() {
var lib model.Library var lib model.Library
var ds *tests.MockDataStore var ds *tests.MockDataStore
var mfRepo *mockMediaFileRepo var mfRepo *mockMediaFileRepo
var s scanner.Scanner var s model.Scanner
createFS := func(files fstest.MapFS) storagetest.FakeFS { createFS := func(files fstest.MapFS) storagetest.FakeFS {
fs := storagetest.FakeFS{} fs := storagetest.FakeFS{}

View File

@ -28,7 +28,7 @@ var _ = Describe("Selective Scan - Deleted Child Folders", Ordered, func() {
var ctx context.Context var ctx context.Context
var lib model.Library var lib model.Library
var ds model.DataStore var ds model.DataStore
var s scanner.Scanner var s model.Scanner
var fsys storagetest.FakeFS var fsys storagetest.FakeFS
BeforeAll(func() { BeforeAll(func() {

View File

@ -24,7 +24,7 @@ type Watcher interface {
type watcher struct { type watcher struct {
mainCtx context.Context mainCtx context.Context
ds model.DataStore ds model.DataStore
scanner Scanner scanner model.Scanner
triggerWait time.Duration triggerWait time.Duration
watcherNotify chan scanNotification watcherNotify chan scanNotification
libraryWatchers map[int]*libraryWatcherInstance libraryWatchers map[int]*libraryWatcherInstance
@ -42,7 +42,7 @@ type scanNotification struct {
} }
// GetWatcher returns the watcher singleton // 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 singleton.GetInstance(func() *watcher {
return &watcher{ return &watcher{
ds: ds, ds: ds,

View File

@ -18,7 +18,6 @@ import (
"github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
@ -39,7 +38,7 @@ type Router struct {
players core.Players players core.Players
provider external.Provider provider external.Provider
playlists core.Playlists playlists core.Playlists
scanner scanner.Scanner scanner model.Scanner
broker events.Broker broker events.Broker
scrobbler scrobbler.PlayTracker scrobbler scrobbler.PlayTracker
share core.Share share core.Share
@ -48,7 +47,7 @@ type Router struct {
} }
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver, 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, playlists core.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
metrics metrics.Metrics, metrics metrics.Metrics,
) *Router { ) *Router {