mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
145 lines
3.8 KiB
Go
145 lines
3.8 KiB
Go
package scanner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/storage"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type Watcher interface {
|
|
Run(ctx context.Context) error
|
|
}
|
|
|
|
type watcher struct {
|
|
ds model.DataStore
|
|
scanner Scanner
|
|
triggerWait time.Duration
|
|
}
|
|
|
|
func NewWatcher(ds model.DataStore, s Scanner) Watcher {
|
|
return &watcher{ds: ds, scanner: s, triggerWait: conf.Server.Scanner.WatcherWait}
|
|
}
|
|
|
|
func (w *watcher) Run(ctx context.Context) error {
|
|
libs, err := w.ds.Library(ctx).GetAll()
|
|
if err != nil {
|
|
return fmt.Errorf("getting libraries: %w", err)
|
|
}
|
|
|
|
watcherChan := make(chan struct{})
|
|
defer close(watcherChan)
|
|
|
|
// Start a watcher for each library
|
|
for _, lib := range libs {
|
|
go watchLib(ctx, lib, watcherChan)
|
|
}
|
|
|
|
trigger := time.NewTimer(w.triggerWait)
|
|
trigger.Stop()
|
|
waiting := false
|
|
for {
|
|
select {
|
|
case <-trigger.C:
|
|
log.Info("Watcher: Triggering scan")
|
|
status, err := w.scanner.Status(ctx)
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error retrieving Scanner status", err)
|
|
break
|
|
}
|
|
if status.Scanning {
|
|
log.Debug(ctx, "Watcher: Already scanning, will retry later", "waitTime", w.triggerWait*3)
|
|
trigger.Reset(w.triggerWait * 3)
|
|
continue
|
|
}
|
|
waiting = false
|
|
go func() {
|
|
_, err := w.scanner.ScanAll(ctx, false)
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error scanning", err)
|
|
} else {
|
|
log.Info(ctx, "Watcher: Scan completed")
|
|
}
|
|
}()
|
|
case <-ctx.Done():
|
|
return nil
|
|
case <-watcherChan:
|
|
if !waiting {
|
|
log.Debug(ctx, "Watcher: Detected changes. Waiting for more changes before triggering scan")
|
|
waiting = true
|
|
}
|
|
|
|
trigger.Reset(w.triggerWait)
|
|
}
|
|
}
|
|
}
|
|
|
|
func watchLib(ctx context.Context, lib model.Library, watchChan chan struct{}) {
|
|
s, err := storage.For(lib.Path)
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error creating storage", "library", lib.ID, "path", lib.Path, err)
|
|
return
|
|
}
|
|
fsys, err := s.FS()
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error getting FS", "library", lib.ID, "path", lib.Path, err)
|
|
return
|
|
}
|
|
watcher, ok := s.(storage.Watcher)
|
|
if !ok {
|
|
log.Info(ctx, "Watcher not supported", "library", lib.ID, "path", lib.Path)
|
|
return
|
|
}
|
|
c, err := watcher.Start(ctx)
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error watching library", "library", lib.ID, "path", lib.Path, err)
|
|
return
|
|
}
|
|
absLibPath, err := filepath.Abs(lib.Path)
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error converting lib.Path to absolute", "library", lib.ID, "path", lib.Path, err)
|
|
return
|
|
}
|
|
log.Info(ctx, "Watcher started", "library", lib.ID, "libPath", lib.Path, "absoluteLibPath", absLibPath)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case path := <-c:
|
|
path, err = filepath.Rel(absLibPath, path)
|
|
if err != nil {
|
|
log.Error(ctx, "Watcher: Error getting relative path", "library", lib.ID, "libPath", absLibPath, "path", path, err)
|
|
continue
|
|
}
|
|
if isIgnoredPath(ctx, fsys, path) {
|
|
log.Trace(ctx, "Watcher: Ignoring change", "library", lib.ID, "path", path)
|
|
continue
|
|
}
|
|
log.Trace(ctx, "Watcher: Detected change", "library", lib.ID, "path", path, "libPath", absLibPath)
|
|
watchChan <- struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func isIgnoredPath(_ context.Context, _ fs.FS, path string) bool {
|
|
baseDir, name := filepath.Split(path)
|
|
switch {
|
|
case model.IsAudioFile(path):
|
|
return false
|
|
case model.IsValidPlaylist(path):
|
|
return false
|
|
case model.IsImageFile(path):
|
|
return false
|
|
case name == ".DS_Store":
|
|
return true
|
|
}
|
|
// As it can be a deletion and not a change, we cannot reliably know if the path is a file or directory.
|
|
// But at this point, we can assume it's a directory. If it's a file, it would be ignored anyway
|
|
return isDirIgnored(baseDir)
|
|
}
|