feat(scanner): add option to use fsnotify based scan watcher (#232)
* First cut at fsnotify support. Tested on Linux. * Fixed bug in logging. * Fixed lint issues. * Added new scan watcher option to README.md * Inverted conditional and dedented following code as per PR discussion. * Changed command line switch and error return on ExecuteWatch() as per GH comments. * Scan watcher: Removed scan at first start. Modified watcher to set a 10 second timer and then process in bulk. Co-authored-by: Brian Doherty <brian@hplaptop.dohertyfamily.me>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/rainycape/unidecode"
|
||||
|
||||
@@ -34,6 +35,9 @@ type Scanner struct {
|
||||
genreSplit string
|
||||
tagger tags.Reader
|
||||
scanning *int32
|
||||
watcher *fsnotify.Watcher
|
||||
watchMap map[string]string // maps watched dirs back to root music dir
|
||||
watchDone chan bool
|
||||
}
|
||||
|
||||
func New(musicDirs []string, db *db.DB, genreSplit string, tagger tags.Reader) *Scanner {
|
||||
@@ -43,23 +47,30 @@ func New(musicDirs []string, db *db.DB, genreSplit string, tagger tags.Reader) *
|
||||
genreSplit: genreSplit,
|
||||
tagger: tagger,
|
||||
scanning: new(int32),
|
||||
watchMap: make(map[string]string),
|
||||
watchDone: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) IsScanning() bool {
|
||||
return atomic.LoadInt32(s.scanning) == 1
|
||||
}
|
||||
func (s *Scanner) StartScanning() bool {
|
||||
return atomic.CompareAndSwapInt32(s.scanning, 0, 1)
|
||||
}
|
||||
func (s *Scanner) StopScanning() {
|
||||
defer atomic.StoreInt32(s.scanning, 0)
|
||||
}
|
||||
|
||||
type ScanOptions struct {
|
||||
IsFull bool
|
||||
}
|
||||
|
||||
func (s *Scanner) ScanAndClean(opts ScanOptions) (*Context, error) {
|
||||
if s.IsScanning() {
|
||||
if !s.StartScanning() {
|
||||
return nil, ErrAlreadyScanning
|
||||
}
|
||||
atomic.StoreInt32(s.scanning, 1)
|
||||
defer atomic.StoreInt32(s.scanning, 0)
|
||||
defer s.StopScanning()
|
||||
|
||||
start := time.Now()
|
||||
c := &Context{
|
||||
@@ -108,6 +119,115 @@ func (s *Scanner) ScanAndClean(opts ScanOptions) (*Context, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) ExecuteWatch() error {
|
||||
var err error
|
||||
s.watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Printf("error creating watcher: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer s.watcher.Close()
|
||||
|
||||
t := time.NewTimer(10 * time.Second)
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
|
||||
for _, dir := range s.musicDirs {
|
||||
err := filepath.WalkDir(dir, func(absPath string, d fs.DirEntry, err error) error {
|
||||
return s.watchCallback(dir, absPath, d, err)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("error watching directory tree: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
scanList := map[string]struct{}{}
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
if !s.StartScanning() {
|
||||
scanList = map[string]struct{}{}
|
||||
break
|
||||
}
|
||||
for dirName := range scanList {
|
||||
c := &Context{
|
||||
errs: &multierr.Err{},
|
||||
seenTracks: map[int]struct{}{},
|
||||
seenAlbums: map[int]struct{}{},
|
||||
isFull: false,
|
||||
}
|
||||
musicDirName := s.watchMap[dirName]
|
||||
if musicDirName == "" {
|
||||
musicDirName = s.watchMap[filepath.Dir(dirName)]
|
||||
}
|
||||
err = filepath.WalkDir(dirName, func(absPath string, d fs.DirEntry, err error) error {
|
||||
return s.watchCallback(musicDirName, absPath, d, err)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("error watching directory tree: %v\n", err)
|
||||
}
|
||||
err = filepath.WalkDir(dirName, func(absPath string, d fs.DirEntry, err error) error {
|
||||
return s.scanCallback(c, musicDirName, absPath, d, err)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("error walking: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
scanList = nil
|
||||
s.StopScanning()
|
||||
case event := <-s.watcher.Events:
|
||||
var dirName string
|
||||
if event.Op&(fsnotify.Create|fsnotify.Write) == 0 {
|
||||
break
|
||||
}
|
||||
if len(scanList) == 0 {
|
||||
t.Reset(10 * time.Second)
|
||||
}
|
||||
fileInfo, err := os.Stat(event.Name)
|
||||
if err != nil && fileInfo.IsDir() {
|
||||
dirName = event.Name
|
||||
} else {
|
||||
dirName = filepath.Dir(event.Name)
|
||||
}
|
||||
scanList[dirName] = struct{}{}
|
||||
case err = <-s.watcher.Errors:
|
||||
log.Printf("error from watcher: %v\n", err)
|
||||
case <-s.watchDone:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) CancelWatch() {
|
||||
s.watchDone <- true
|
||||
}
|
||||
|
||||
func (s *Scanner) watchCallback(dir string, absPath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch d.Type() {
|
||||
case os.ModeDir:
|
||||
case os.ModeSymlink:
|
||||
eval, _ := filepath.EvalSymlinks(absPath)
|
||||
return filepath.WalkDir(eval, func(subAbs string, d fs.DirEntry, err error) error {
|
||||
subAbs = strings.Replace(subAbs, eval, absPath, 1)
|
||||
return s.watchCallback(dir, subAbs, d, err)
|
||||
})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.watchMap[absPath] == "" {
|
||||
s.watchMap[absPath] = dir
|
||||
err = s.watcher.Add(absPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Scanner) scanCallback(c *Context, dir string, absPath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
c.errs.Add(err)
|
||||
|
||||
Reference in New Issue
Block a user