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
This commit is contained in:
Deluan 2021-05-06 17:56:10 -04:00
parent 1d6aa70033
commit f8dbc41b6d
9 changed files with 164 additions and 30 deletions

View File

@ -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")
}
}
}

View File

@ -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
}

View File

@ -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,
))
}

View File

@ -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)

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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()

24
scheduler/log_adapter.go Normal file
View File

@ -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...)
}

34
scheduler/scheduler.go Normal file
View File

@ -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
}