package watcher import ( "log" "os" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify" "mediawatcher/internal/config" "mediawatcher/internal/util" ) type FileHandler func(path string) type Watcher struct { cfg *config.Config watcher *fsnotify.Watcher pending map[string]struct{} mu sync.Mutex } func New(cfg *config.Config) (*Watcher, error) { w, err := fsnotify.NewWatcher() if err != nil { return nil, err } return &Watcher{ cfg: cfg, watcher: w, pending: make(map[string]struct{}), }, nil } func (w *Watcher) Close() error { return w.watcher.Close() } func (w *Watcher) Start(handler FileHandler) error { for _, dir := range w.cfg.Watch.Dirs { if err := w.addDir(dir); err != nil { return err } } go w.loop(handler) return nil } func (w *Watcher) addDir(dir string) error { info, err := os.Stat(dir) if err != nil { return err } if !info.IsDir() { return nil } log.Printf("[watch] watching %s", dir) return w.watcher.Add(dir) } func (w *Watcher) loop(handler FileHandler) { for { select { case event, ok := <-w.watcher.Events: if !ok { return } if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename) == 0 { continue } path := event.Name info, err := os.Stat(path) if err != nil || info.IsDir() { continue } extIncl := util.HasExt(path, w.cfg.Watch.IncludeExt) extExcl := util.HasExt(path, w.cfg.Watch.ExcludeExt) if !extIncl || extExcl { continue } w.mu.Lock() if _, exists := w.pending[path]; exists { w.mu.Unlock() continue } w.pending[path] = struct{}{} w.mu.Unlock() go w.handleFile(path, handler) case err, ok := <-w.watcher.Errors: if !ok { return } log.Printf("[watch] error: %v", err) } } } func (w *Watcher) handleFile(path string, handler FileHandler) { defer func() { w.mu.Lock() delete(w.pending, path) w.mu.Unlock() }() stableFor := time.Duration(w.cfg.Watch.StableSeconds) * time.Second if err := util.WaitForStable(path, stableFor, 2*time.Hour); err != nil { log.Printf("[watch] file %s not stable: %v", path, err) return } abs, err := filepath.Abs(path) if err != nil { abs = path } handler(abs) }