diff --git a/cmd/um/main.go b/cmd/um/main.go index 714bbbf..8402142 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "os/signal" "path/filepath" "runtime" "runtime/debug" @@ -14,6 +15,7 @@ import ( "strings" "time" + "github.com/fsnotify/fsnotify" "github.com/urfave/cli/v2" "go.uber.org/zap" @@ -52,6 +54,7 @@ func main() { &cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true}, &cli.BoolFlag{Name: "update-metadata", Usage: "update metadata & album art from network", Required: false, Value: false}, &cli.BoolFlag{Name: "overwrite", Usage: "overwrite output file without asking", Required: false, Value: false}, + &cli.BoolFlag{Name: "watch", Usage: "watch the input dir and process new files", Required: false, Value: false}, &cli.BoolFlag{Name: "supported-ext", Usage: "show supported file extensions and exit", Required: false, Value: false}, }, @@ -66,6 +69,7 @@ func main() { logger.Fatal("run app failed", zap.Error(err)) } } + func printSupportedExtensions() { var exts []string for ext := range common.DecoderRegistry { @@ -76,6 +80,7 @@ func printSupportedExtensions() { fmt.Printf("%s: %d\n", ext, len(common.DecoderRegistry[ext])) } } + func appMain(c *cli.Context) (err error) { if c.Bool("supported-ext") { printSupportedExtensions() @@ -134,7 +139,12 @@ func appMain(c *cli.Context) (err error) { } if inputStat.IsDir() { - return proc.processDir(input) + wacthDir := c.Bool("watch") + if !wacthDir { + return proc.processDir(input) + } else { + return proc.watchDir(input) + } } else { return proc.processFile(input) } @@ -150,6 +160,60 @@ type processor struct { overwriteOutput bool } +func (p *processor) watchDir(inputDir string) error { + if err := p.processDir(inputDir); err != nil { + return err + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("failed to create watcher: %w", err) + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) { + // try open with exclusive mode, to avoid file is still writing + f, err := os.OpenFile(event.Name, os.O_RDONLY, os.ModeExclusive) + if err != nil { + logger.Debug("failed to open file exclusively", zap.String("path", event.Name), zap.Error(err)) + time.Sleep(1 * time.Second) // wait for file writing complete + continue + } + _ = f.Close() + + if err := p.processFile(event.Name); err != nil { + logger.Warn("failed to process file", zap.String("path", event.Name), zap.Error(err)) + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + logger.Error("file watcher got error", zap.Error(err)) + } + } + }() + + err = watcher.Add(inputDir) + if err != nil { + return fmt.Errorf("failed to watch dir %s: %w", inputDir, err) + } + + signalCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + <-signalCtx.Done() + return nil +} + func (p *processor) processDir(inputDir string) error { items, err := os.ReadDir(inputDir) if err != nil { diff --git a/go.mod b/go.mod index 67f7119..7362c01 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module unlock-music.dev/cli go 1.19 require ( + github.com/fsnotify/fsnotify v1.6.0 github.com/go-flac/flacpicture v0.2.0 github.com/go-flac/flacvorbis v0.1.0 github.com/go-flac/go-flac v0.3.1 @@ -22,5 +23,6 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect + golang.org/x/sys v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 865c2c8..2a3652f 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ddliu/go-httpclient v0.5.1 h1:ys4KozrhBaGdI1yuWIFwNNILqhnMU9ozTvRNfCTorvs= github.com/ddliu/go-httpclient v0.5.1/go.mod h1:8QVbjq00YK2f2MQyiKuWMdaKOFRcoD9VuubkNCNOuZo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-flac/flacpicture v0.2.0 h1:rS/ZOR/ZxlEwMf3yOPFcTAmGyoV6rDtcYdd+6CwWQAw= github.com/go-flac/flacpicture v0.2.0/go.mod h1:M4a1J0v6B5NHsck4GA1yZg0vFQzETVPd3kuj6Ow+q9o= github.com/go-flac/flacvorbis v0.1.0 h1:xStJfPrZ/IoA2oBUEwgrlaSf+Opo6/YuQfkqVhkP0cM= @@ -46,6 +48,9 @@ golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=