From f8dbc41b6d8b2a159250e879d90ca319d98f8f8f Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 6 May 2021 17:56:10 -0400 Subject: [PATCH] Breaking change: Add `ScanSchedule`, allows interval and cron based configurations. See https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format for expression syntax. `ScanInterval` will still work for the time being. The only situation it does not work is when you want to disable periodic scanning by setting `ScanInterval=0`. If you want to disable it, please set `ScanSchedule=""` Closes #1085 --- cmd/root.go | 39 +++++++++++++++++++++++++++++---------- cmd/wire_gen.go | 22 +++++++++++++++++++++- cmd/wire_injectors.go | 20 ++++++++++++++++++++ conf/configuration.go | 34 +++++++++++++++++++++++++++++++++- go.mod | 1 + go.sum | 2 ++ scanner/scanner.go | 18 ------------------ scheduler/log_adapter.go | 24 ++++++++++++++++++++++++ scheduler/scheduler.go | 34 ++++++++++++++++++++++++++++++++++ 9 files changed, 164 insertions(+), 30 deletions(-) create mode 100644 scheduler/log_adapter.go create mode 100644 scheduler/scheduler.go diff --git a/cmd/root.go b/cmd/root.go index ead8f72a9..0a64539bf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,12 +56,13 @@ func runNavidrome() { g.Add(startServer()) g.Add(startSignaler()) + g.Add(startScheduler()) - interval := conf.Server.ScanInterval - if interval != 0 { - g.Add(startPeriodicScan(interval)) + schedule := conf.Server.ScanSchedule + if schedule != "" { + go schedulePeriodicScan(schedule) } else { - log.Warn("Periodic scan is DISABLED", "interval", interval) + log.Warn("Periodic scan is DISABLED", "schedule", schedule) } if err := g.Run(); err != nil { @@ -115,22 +116,40 @@ func startSignaler() (func() error, func(err error)) { } } -func startPeriodicScan(interval time.Duration) (func() error, func(err error)) { - log.Info("Starting scanner", "interval", interval.String()) +func schedulePeriodicScan(schedule string) { + time.Sleep(2 * time.Second) // Wait 2 seconds before the first scan scanner := GetScanner() + scheduler := GetScheduler() + + log.Info("Executing initial scan") + if err := scanner.RescanAll(context.Background(), false); err != nil { + log.Error("Error executing initial scan", err) + } + + log.Info("Scheduling periodic scan", "schedule", schedule) + err := scheduler.Add(schedule, func() { + _ = scanner.RescanAll(context.Background(), false) + }) + if err != nil { + log.Error("Error scheduling periodic scan", err) + } +} + +func startScheduler() (func() error, func(err error)) { + log.Info("Starting scheduler") + scheduler := GetScheduler() ctx, cancel := context.WithCancel(context.Background()) return func() error { - time.Sleep(2 * time.Second) // Wait 2 seconds before the first scan - scanner.Run(ctx, interval) + scheduler.Run(ctx) return nil }, func(err error) { cancel() if err != nil { - log.Error("Shutting down Scanner due to error", err) + log.Error("Shutting down Scheduler due to error", err) } else { - log.Info("Shutting down Scanner") + log.Info("Shutting down Scheduler") } } } diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index b41423adc..cec66563a 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -6,16 +6,18 @@ package cmd import ( + "sync" + "github.com/google/wire" "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/transcoder" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/scanner" + "github.com/navidrome/navidrome/scheduler" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server/app" "github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/server/subsonic" - "sync" ) // Injectors from wire_injectors.go: @@ -63,6 +65,11 @@ func createBroker() events.Broker { return broker } +func createScheduler() scheduler.Scheduler { + schedulerScheduler := scheduler.New() + return schedulerScheduler +} + // wire_injectors.go: var allProviders = wire.NewSet(core.Set, subsonic.New, app.New, persistence.New) @@ -92,3 +99,16 @@ func GetBroker() events.Broker { }) return brokerInstance } + +// Scheduler must be a Singleton +var ( + onceScheduler sync.Once + schedulerInstance scheduler.Scheduler +) + +func GetScheduler() scheduler.Scheduler { + onceScheduler.Do(func() { + schedulerInstance = createScheduler() + }) + return schedulerInstance +} diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go index fa543b5a0..364e1c7a9 100644 --- a/cmd/wire_injectors.go +++ b/cmd/wire_injectors.go @@ -9,6 +9,7 @@ import ( "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/scanner" + "github.com/navidrome/navidrome/scheduler" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server/app" "github.com/navidrome/navidrome/server/events" @@ -82,3 +83,22 @@ func createBroker() events.Broker { events.NewBroker, )) } + +// Scheduler must be a Singleton +var ( + onceScheduler sync.Once + schedulerInstance scheduler.Scheduler +) + +func GetScheduler() scheduler.Scheduler { + onceScheduler.Do(func() { + schedulerInstance = createScheduler() + }) + return schedulerInstance +} + +func createScheduler() scheduler.Scheduler { + panic(wire.Build( + scheduler.New, + )) +} diff --git a/conf/configuration.go b/conf/configuration.go index 671ee02ec..cd385c79a 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -10,6 +10,7 @@ import ( "github.com/kr/pretty" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" + "github.com/robfig/cron/v3" "github.com/spf13/viper" ) @@ -22,6 +23,7 @@ type configOptions struct { DbPath string LogLevel string ScanInterval time.Duration + ScanSchedule string SessionTimeout time.Duration BaseURL string UILoginBackgroundURL string @@ -108,6 +110,11 @@ func Load() { log.SetLevelString(Server.LogLevel) log.SetLogSourceLine(Server.DevLogSourceLine) log.SetRedacting(Server.EnableLogRedacting) + + if err := validateScanSchedule(); err != nil { + os.Exit(1) + } + log.Debug(pretty.Sprintf("Loaded configuration from '%s': %# v\n", Server.ConfigFile, Server)) // Call init hooks @@ -116,6 +123,30 @@ func Load() { } } +func validateScanSchedule() error { + if Server.ScanInterval != 0 { + log.Warn("ScanInterval is DEPRECATED. Please use ScanSchedule. See docs at https://navidrome.org/docs/usage/configuration-options/") + if Server.ScanSchedule != "@every 1m" { + log.Error("You cannot specify both ScanInterval and ScanSchedule, ignoring ScanInterval") + } else { + Server.ScanSchedule = fmt.Sprintf("@every %s", Server.ScanInterval) + log.Warn("Setting ScanSchedule", "schedule", Server.ScanSchedule) + } + } + if Server.ScanSchedule != "" { + if _, err := time.ParseDuration(Server.ScanSchedule); err == nil { + Server.ScanSchedule = "@every " + Server.ScanSchedule + } + c := cron.New() + _, err := c.AddFunc(Server.ScanSchedule, func() {}) + if err != nil { + log.Error("Invalid ScanSchedule. Please read format spec at https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format", "schedule", Server.ScanSchedule, err) + return err + } + } + return nil +} + // AddHook is used to register initialization code that should run as soon as the config is loaded func AddHook(hook func()) { hooks = append(hooks, hook) @@ -128,7 +159,8 @@ func init() { viper.SetDefault("address", "0.0.0.0") viper.SetDefault("port", 4533) viper.SetDefault("sessiontimeout", consts.DefaultSessionTimeout) - viper.SetDefault("scaninterval", time.Minute) + viper.SetDefault("scaninterval", 0) + viper.SetDefault("scanschedule", "@every 1m") viper.SetDefault("baseurl", "") viper.SetDefault("uiloginbackgroundurl", consts.DefaultUILoginBackgroundURL) viper.SetDefault("enabletranscodingconfig", false) diff --git a/go.mod b/go.mod index e4e0f68e1..74ea5664b 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/onsi/gomega v1.11.0 github.com/pelletier/go-toml v1.8.0 // indirect github.com/pressly/goose v2.7.0+incompatible + github.com/robfig/cron/v3 v3.0.0 // indirect github.com/sirupsen/logrus v1.8.1 github.com/spf13/afero v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect diff --git a/go.sum b/go.sum index eb6dc8896..a81121758 100644 --- a/go.sum +++ b/go.sum @@ -630,6 +630,8 @@ github.com/quasilyte/go-ruleguard/rules v0.0.0-20210203162857-b223e0831f88/go.mo github.com/quasilyte/go-ruleguard/rules v0.0.0-20210221215616-dfcc94e3dffd/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY= github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= +github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/scanner/scanner.go b/scanner/scanner.go index 4cd569d77..593de3dcb 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -16,7 +16,6 @@ import ( ) type Scanner interface { - Run(ctx context.Context, interval time.Duration) RescanAll(ctx context.Context, fullRescan bool) error Status(mediaFolder string) (*StatusInfo, error) Scanning() bool @@ -72,23 +71,6 @@ func New(ds model.DataStore, cacheWarmer core.CacheWarmer, broker events.Broker) return s } -func (s *scanner) Run(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - for { - err := s.RescanAll(ctx, false) - if err != nil { - log.Error(err) - } - select { - case <-ticker.C: - continue - case <-ctx.Done(): - return - } - } -} - func (s *scanner) rescan(ctx context.Context, mediaFolder string, fullRescan bool) error { folderScanner := s.folders[mediaFolder] start := time.Now() diff --git a/scheduler/log_adapter.go b/scheduler/log_adapter.go new file mode 100644 index 000000000..ccaab0cd4 --- /dev/null +++ b/scheduler/log_adapter.go @@ -0,0 +1,24 @@ +package scheduler + +import ( + "github.com/navidrome/navidrome/log" +) + +type logger struct{} + +func (l *logger) Info(msg string, keysAndValues ...interface{}) { + args := []interface{}{ + "Scheduler: " + msg, + } + args = append(args, keysAndValues...) + log.Debug(args...) +} + +func (l *logger) Error(err error, msg string, keysAndValues ...interface{}) { + args := []interface{}{ + "Scheduler: " + msg, + } + args = append(args, keysAndValues...) + args = append(args, err) + log.Error(args...) +} diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go new file mode 100644 index 000000000..bfc0e81e8 --- /dev/null +++ b/scheduler/scheduler.go @@ -0,0 +1,34 @@ +package scheduler + +import ( + "context" + + "github.com/robfig/cron/v3" +) + +type Scheduler interface { + Run(ctx context.Context) + Add(crontab string, cmd func()) error +} + +func New() Scheduler { + c := cron.New(cron.WithLogger(&logger{})) + return &scheduler{ + c: c, + } +} + +type scheduler struct { + c *cron.Cron +} + +func (s *scheduler) Run(ctx context.Context) { + s.c.Start() + <-ctx.Done() + s.c.Stop() +} + +func (s *scheduler) Add(crontab string, cmd func()) error { + _, err := s.c.AddFunc(crontab, cmd) + return err +}