mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
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:
parent
1d6aa70033
commit
f8dbc41b6d
9 changed files with 164 additions and 30 deletions
39
cmd/root.go
39
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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
24
scheduler/log_adapter.go
Normal 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
34
scheduler/scheduler.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue