package main import ( "context" "fmt" "io" "log" "math/big" "os" "os/exec" "os/signal" "path" "slices" "sort" "strconv" "strings" "syscall" "time" ) func main() { ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT) defer can() if err := Run(ctx, os.Args[1:]); err != nil { panic(err) } } func Run(ctx context.Context, args []string) error { cams, err := lsd(args[0]) if err != nil { return err } for _, cam := range cams { files, err := lsf(cam) if err != nil { return err } else if len(files) < 1 { continue } lastChunk := strings.Split(path.Base(files[len(files)-1]), ".")[0] files = slices.DeleteFunc(files, func(f string) bool { return strings.Contains(f, lastChunk) }) if len(files) == 0 { continue } lastMovementAt := time.Unix(0, 0) prevF := files[0] for _, f := range files[1:] { prev, err := os.Stat(prevF) if err != nil { return err } if ok, err := hasMovement(ctx, prevF, f); err != nil { return err } else if ok { lastMovementAt = prev.ModTime() } target := "trash" if prev.ModTime().Before(lastMovementAt.Add(3 * time.Minute)) { target = "movement" } if target == "trash" { log.Println("deleting", prevF) if err := os.Remove(prevF); err != nil { return err } } else if err := func() error { gName := strings.ReplaceAll(prevF, "record", target) if gName == prevF { return fmt.Errorf("would overwrite original %s", prevF) } os.MkdirAll(path.Dir(gName), os.ModePerm) log.Println("moving", prevF, "to", gName) f, err := os.Open(prevF) if err != nil { return err } defer f.Close() g, err := os.Create(gName) if err != nil { return err } defer g.Close() if _, err := io.Copy(g, f); err != nil { return err } return os.Remove(prevF) }(); err != nil { return err } prevF = f } } return nil } func hasMovement(ctx context.Context, a, b string) (bool, error) { sizeOfCmd := exec.CommandContext(ctx, "identify", a) sizeOfOutput, err := sizeOfCmd.CombinedOutput() if err != nil { return false, fmt.Errorf("failed to identify %s: (%w) %s", a, err, sizeOfOutput) } hw := strings.Fields(string(sizeOfOutput))[2] h, err := strconv.ParseInt(strings.Split(hw, "x")[0], 10, 16) if err != nil { return false, err } w, err := strconv.ParseInt(strings.Split(hw, "x")[1], 10, 16) if err != nil { return false, err } total := h * w compareCmd := exec.CommandContext(ctx, "compare", "-metric", "AE", "-fuzz", "15%", a, b, "/dev/null") compareOutput, _ := compareCmd.CombinedOutput() f, _, err := big.ParseFloat(string(compareOutput), 10, 0, big.ToNearestEven) if err != nil { return false, err } i := new(big.Int) f.Int(i) delta := i.Int64() percentPixelsChanged := int(100.0 * float64(delta) / float64(total)) return percentPixelsChanged > 10, nil } func lsd(d string) ([]string, error) { return ls(d, true) } func lsf(d string) ([]string, error) { return ls(d, false) } func ls(d string, dirs bool) ([]string, error) { entries, err := os.ReadDir(d) results := make([]string, 0, len(entries)) for i := range entries { if dirs == entries[i].IsDir() { results = append(results, path.Join(d, entries[i].Name())) } } sort.Strings(results) return results, err }