From 1675136966473083e970badfe92433a05d2d7fee Mon Sep 17 00:00:00 2001 From: jakka Date: Sat, 12 Apr 2025 14:44:39 +0300 Subject: implemented database and concurrent file scanning --- cmd/cmd.go | 58 ++++++++++++++++--------- cmd/init.go | 38 +++++++++++++--- files/database.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ files/index.go | 53 ++++++++++++++++++++++- files/types.go | 19 ++++++++ go.mod | 6 +++ go.sum | 19 ++++++++ 7 files changed, 290 insertions(+), 29 deletions(-) create mode 100644 files/database.go create mode 100644 files/types.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 86a1b92..82a9cd5 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,14 +1,13 @@ package main import ( - "fmt" + "context" "os" "os/signal" - "time" + "github.com/justjakka/reencoder/files" "github.com/tidwall/buntdb" "github.com/urfave/cli/v2" - "golang.org/x/sync/errgroup" ) func runCmd(cCtx *cli.Context) error { @@ -17,32 +16,51 @@ func runCmd(cCtx *cli.Context) error { return err } - db, err := buntdb.Open(ctx.Value("database").(string)) + db, err := buntdb.Open(ctx.Value("dbfile").(string)) if err != nil { return err } defer db.Close() + if err := db.Shrink(); err != nil { + return err + } + + if err := db.CreateIndex("process", "*", buntdb.IndexJSON("process")); err != nil { + if err != buntdb.ErrIndexExists { + return err + } + } + + ctx = context.WithValue(ctx, "database", db) + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) defer stop() - test := new(errgroup.Group) - test.SetLimit(3) - for n := 1; n < 10; n++ { - test.Go(func() error { - select { - case <-ctx.Done(): - return nil - default: - fmt.Println("test") - time.Sleep(3 * time.Second) - return nil - } - }) + if err = files.IndexFlacs(ctx); err != nil { + return err } - _ = test.Wait() - fmt.Println("reached") - return cCtx.Err() + + return nil + + /* + test := new(errgroup.Group) + test.SetLimit(3) + for n := 1; n < 10; n++ { + test.Go(func() error { + select { + case <-ctx.Done(): + return nil + default: + fmt.Println("test") + time.Sleep(3 * time.Second) + return nil + } + }) + } + _ = test.Wait() + fmt.Println("reached") + return cCtx.Err() */ } func start() { diff --git a/cmd/init.go b/cmd/init.go index 836a04a..18e3b65 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -4,8 +4,10 @@ import ( "context" "errors" "os" + "os/exec" "path/filepath" "runtime" + "strings" "github.com/urfave/cli/v2" ) @@ -26,21 +28,35 @@ func getLocalStorage() string { } func getDb(cCtx *cli.Context) (context.Context, error) { - if cCtx.Path("database") == "" { + if cCtx.Path("dbfile") == "" { localFolder := getLocalStorage() if localFolder == "" { - return context.WithValue(cCtx.Context, "database", ""), errors.New("failed to locate application data folder") + return context.WithValue(cCtx.Context, "dbfile", ""), errors.New("failed to locate application data folder") } - return context.WithValue(cCtx.Context, "database", filepath.Join(localFolder, "reencoder.db")), nil + return context.WithValue(cCtx.Context, "dbfile", filepath.Join(localFolder, "reencoder.db")), nil } - if _, err := os.Stat(cCtx.Path("database")); err != nil { - return context.WithValue(cCtx.Context, "database", ""), err + if _, err := os.Stat(cCtx.Path("dbfile")); err != nil { + return context.WithValue(cCtx.Context, "dbfile", ""), err } - return context.WithValue(cCtx.Context, "database", cCtx.Path("database")), nil + return context.WithValue(cCtx.Context, "dbfile", cCtx.Path("dbfile")), nil +} + +func checkTools() error { + if _, err := exec.LookPath("flac"); err != nil { + return errors.New("missing flac executable") + } + if _, err := exec.LookPath("metaflac"); err != nil { + return errors.New("missing metaflac executable") + } + return nil } func initCmd(cCtx *cli.Context) (context.Context, error) { + if err := checkTools(); err != nil { + return nil, err + } + if _, err := os.Stat(cCtx.Path("path")); err != nil { return nil, err } @@ -49,5 +65,13 @@ func initCmd(cCtx *cli.Context) (context.Context, error) { if err != nil { return nil, err } - return context.WithValue(ctx, "path", cCtx.Path("path")), nil + + ctx = context.WithValue(ctx, "path", cCtx.Path("path")) + + encoder, err := exec.Command("flac", "-v").Output() + if err != nil { + return nil, err + } + + return context.WithValue(ctx, "encoder", strings.ReplaceAll(strings.Split(string(encoder), " ")[1], "\n", "")), nil } diff --git a/files/database.go b/files/database.go new file mode 100644 index 0000000..24bd5af --- /dev/null +++ b/files/database.go @@ -0,0 +1,126 @@ +package files + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/tidwall/buntdb" +) + +func evaluateFile(database *buntdb.DB, hashsum string, filedata FileInfo) error { + return database.View(func(tx *buntdb.Tx) error { + data, err := tx.Get(hashsum) + if err != nil { + return err + } + + var info FileInfo + if err := json.Unmarshal([]byte(data), &info); err != nil { + return err + } + if info == filedata { + return UpToDate + } + switch info.Modtime == filedata.Modtime { + case true: + if info.AbsPath == filedata.AbsPath { + if info.Process { + return NeedsReencode + } + return UpToDate + } + if info.Process { + return NeedsReencode + } + return MovedFile + default: + return nil + } + }) +} + +func getEncoderVer(path string) (string, error) { + out, err := exec.Command("metaflac", "--show-vendor-tag", path).Output() + if err != nil { + return "", err + } + + r := regexp.MustCompile("libFLAC \\d\\.\\d\\.\\d") + encoder := r.FindString(string(out)) + switch encoder { + case "": + return "", nil + default: + return strings.Split(encoder, " ")[1], nil + } +} + +func updateFile(database *buntdb.DB, hashsum string, filedata FileInfo) error { + return database.Update(func(tx *buntdb.Tx) error { + data, err := json.Marshal(filedata) + if err != nil { + return err + } + if _, _, err := tx.Set(hashsum, string(data), nil); err != nil { + return err + } + return nil + }) +} + +func ProcessFile(ctx context.Context, path string, info fs.DirEntry) error { + tmp, err := info.Info() + if err != nil { + return err + } + + modtime := tmp.ModTime() + + database := ctx.Value("database").(*buntdb.DB) + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return err + } + hashsum := fmt.Sprintf("%x", hash.Sum(nil)) + + abspath, err := filepath.Abs(path) + if err != nil { + return err + } + + encoder, err := getEncoderVer(path) + + filedata := FileInfo{AbsPath: abspath, Modtime: modtime, Encoder: encoder, Process: true} + + err = evaluateFile(database, hashsum, filedata) + switch err { + case MovedFile, UpToDate: + if filedata.Encoder == ctx.Value("encoder").(string) { + return nil + } + case buntdb.ErrNotFound, NeedsReencode: + + default: + return err + } + if err := updateFile(database, hashsum, filedata); err != nil { + return err + } + return nil +} diff --git a/files/index.go b/files/index.go index e02e4ba..ea05dc4 100644 --- a/files/index.go +++ b/files/index.go @@ -1,7 +1,56 @@ -package main +package files -import "context" +import ( + "context" + "io/fs" + "path/filepath" + "time" + + "github.com/briandowns/spinner" + "golang.org/x/sync/errgroup" +) func IndexFlacs(ctx context.Context) error { + spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond) + spin.Suffix = " Indexing flacs..." + + spin.Start() + + wg := new(errgroup.Group) + wg.SetLimit(100) + + if err := filepath.WalkDir(ctx.Value("path").(string), func(path string, info fs.DirEntry, err error) error { + select { + case <-ctx.Done(): + spin.FinalMSG = "Stopping...\n" + spin.Stop() + return nil + default: + if err != nil { + return err + } + if !info.IsDir() { + if filepath.Ext(path) == ".flac" { + wg.Go(func() error { + select { + case <-ctx.Done(): + return nil + default: + return ProcessFile(ctx, path, info) + } + }) + } + + } + return nil + } + + }); err != nil { + return err + } + + wg.Wait() + spin.FinalMSG = "Done indexing flacs" + spin.Stop() return nil } diff --git a/files/types.go b/files/types.go new file mode 100644 index 0000000..54d8563 --- /dev/null +++ b/files/types.go @@ -0,0 +1,19 @@ +package files + +import ( + "errors" + "time" +) + +var ( + UpToDate = errors.New("file is up to date") + MovedFile = errors.New("file was moved") + NeedsReencode = errors.New("file needs to be reencoded") +) + +type FileInfo struct { + AbsPath string `json:"abspath"` + Modtime time.Time `json:"modtime"` + Encoder string `json:"encoder"` + Process bool `json:"process"` +} diff --git a/go.mod b/go.mod index 9c36db4..0c1e6dd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.24.2 require github.com/urfave/cli/v2 v2.27.6 require ( + github.com/fatih/color v1.7.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/tidwall/btree v1.4.2 // indirect github.com/tidwall/gjson v1.14.3 // indirect github.com/tidwall/grect v0.1.4 // indirect @@ -12,9 +15,12 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect ) require ( + github.com/briandowns/spinner v1.23.2 github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/buntdb v1.3.2 diff --git a/go.sum b/go.sum index cdaf029..c088acc 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,18 @@ +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g= github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg= @@ -11,6 +22,8 @@ github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -25,3 +38,9 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -- cgit v1.3.1