156 lines
3.1 KiB
Go
156 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"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 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)
|
|
|
|
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
|
|
}
|