Compare commits
13 Commits
078dd72335
...
2ac2e0c56c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac2e0c56c | ||
|
|
6f0ce15715 | ||
|
|
102df09caf | ||
|
|
eb1ea4adb7 | ||
|
|
38ea2b8128 | ||
|
|
5b0b1aaf7f | ||
|
|
42e858cc33 | ||
|
|
9a09eb7c10 | ||
|
|
9a4abb407a | ||
|
|
10122550d3 | ||
|
|
811d3294d4 | ||
|
|
776db80cff | ||
|
|
6ba815f8b2 |
1
testdata/ffmpeg.d/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
**/*.sw*
|
**/*.sw*
|
||||||
/cmd/prune/prune
|
/cmd/prune/prune
|
||||||
|
/cmd/prune/testdata/**
|
||||||
|
|||||||
161
testdata/ffmpeg.d/cmd/prune/main.go
vendored
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -29,6 +28,11 @@ func main() {
|
|||||||
|
|
||||||
func Run(ctx context.Context, args []string) error {
|
func Run(ctx context.Context, args []string) error {
|
||||||
cams, err := lsd(args[0])
|
cams, err := lsd(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to lsd %s: %w", args[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
movementInterval, err := time.ParseDuration(args[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -36,78 +40,96 @@ func Run(ctx context.Context, args []string) error {
|
|||||||
for _, cam := range cams {
|
for _, cam := range cams {
|
||||||
files, err := lsf(cam)
|
files, err := lsf(cam)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to lsf %s: %w", cam, err)
|
||||||
} else if len(files) < 1 {
|
} else if len(files) < 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lastChunk := strings.Split(path.Base(files[len(files)-1]), ".")[0]
|
series := []string{}
|
||||||
files = slices.DeleteFunc(files, func(f string) bool {
|
for _, f := range files {
|
||||||
return strings.Contains(f, lastChunk)
|
series = append(series, strings.Split(path.Base(f), ".")[0])
|
||||||
})
|
}
|
||||||
if len(files) == 0 {
|
series = slices.Compact(series)
|
||||||
|
if len(series) < 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
series = series[:len(series)-1]
|
||||||
|
|
||||||
lastMovementAt := time.Unix(0, 0)
|
for _, series := range series {
|
||||||
prevF := files[0]
|
if err := func() error {
|
||||||
for _, f := range files[1:] {
|
seriesFiles := []string{}
|
||||||
prev, err := os.Stat(prevF)
|
for _, file := range files {
|
||||||
if err != nil {
|
if strings.HasPrefix(path.Base(file), series) {
|
||||||
return err
|
seriesFiles = append(seriesFiles, file)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if ok, err := hasMovement(ctx, prevF, f); err != nil {
|
if seriesHasMovement, err := seriesHasMovement(ctx, seriesFiles, movementInterval); 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
|
return err
|
||||||
|
} else if seriesHasMovement {
|
||||||
|
outd := strings.ReplaceAll(cam, "record", "movement")
|
||||||
|
os.MkdirAll(outd, os.ModePerm)
|
||||||
|
cmd := exec.CommandContext(ctx,
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-framerate", "3",
|
||||||
|
"-pattern_type", "glob",
|
||||||
|
"-i", path.Join(cam, series)+".*.jpg",
|
||||||
|
"-r", "3",
|
||||||
|
path.Join(outd, series+".gif"),
|
||||||
|
)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to persist series %s: (%w) %s", series, err, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if err := func() error {
|
for _, seriesFile := range seriesFiles {
|
||||||
gName := strings.ReplaceAll(prevF, "record", target)
|
if err := os.Remove(seriesFile); err != nil {
|
||||||
if gName == prevF {
|
return fmt.Errorf("failed to rm series %s[%s]: %w", series, seriesFile, err)
|
||||||
return fmt.Errorf("would overwrite original %s", prevF)
|
}
|
||||||
}
|
}
|
||||||
os.MkdirAll(path.Dir(gName), os.ModePerm)
|
return nil
|
||||||
|
|
||||||
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 {
|
}(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
prevF = f
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func seriesHasMovement(ctx context.Context, files []string, movementInterval time.Duration) (bool, error) {
|
||||||
|
if len(files) < 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f := files[0]
|
||||||
|
fStat, err := os.Stat(f)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
fTime := fStat.ModTime()
|
||||||
|
for i := 1; i < len(files); i++ {
|
||||||
|
g := files[i]
|
||||||
|
gStat, err := os.Stat(g)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
gTime := gStat.ModTime()
|
||||||
|
|
||||||
|
if gTime.Sub(fTime) < movementInterval {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasMovement, err := hasMovement(ctx, f, g); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to check for movement between %s and %s: %w", f, g, err)
|
||||||
|
} else if hasMovement {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f = g
|
||||||
|
fTime = gTime
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func hasMovement(ctx context.Context, a, b string) (bool, error) {
|
func hasMovement(ctx context.Context, a, b string) (bool, error) {
|
||||||
sizeOfCmd := exec.CommandContext(ctx, "identify", a)
|
sizeOfCmd := exec.CommandContext(ctx, "identify", a)
|
||||||
sizeOfOutput, err := sizeOfCmd.CombinedOutput()
|
sizeOfOutput, err := sizeOfCmd.CombinedOutput()
|
||||||
@@ -117,11 +139,11 @@ func hasMovement(ctx context.Context, a, b string) (bool, error) {
|
|||||||
hw := strings.Fields(string(sizeOfOutput))[2]
|
hw := strings.Fields(string(sizeOfOutput))[2]
|
||||||
h, err := strconv.ParseInt(strings.Split(hw, "x")[0], 10, 16)
|
h, err := strconv.ParseInt(strings.Split(hw, "x")[0], 10, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("failed parsing %s for HxW: %w", hw, err)
|
||||||
}
|
}
|
||||||
w, err := strconv.ParseInt(strings.Split(hw, "x")[1], 10, 16)
|
w, err := strconv.ParseInt(strings.Split(hw, "x")[1], 10, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("failed parsing %s for HxW: %w", hw, err)
|
||||||
}
|
}
|
||||||
total := h * w
|
total := h * w
|
||||||
|
|
||||||
@@ -130,7 +152,7 @@ func hasMovement(ctx context.Context, a, b string) (bool, error) {
|
|||||||
|
|
||||||
f, _, err := big.ParseFloat(string(compareOutput), 10, 0, big.ToNearestEven)
|
f, _, err := big.ParseFloat(string(compareOutput), 10, 0, big.ToNearestEven)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("failed to parse %s for a number of changed pixels: %w", compareOutput, err)
|
||||||
}
|
}
|
||||||
i := new(big.Int)
|
i := new(big.Int)
|
||||||
f.Int(i)
|
f.Int(i)
|
||||||
@@ -140,6 +162,29 @@ func hasMovement(ctx context.Context, a, b string) (bool, error) {
|
|||||||
return percentPixelsChanged > 10, nil
|
return percentPixelsChanged > 10, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mv(wPath, rPath string) error {
|
||||||
|
r, err := os.Open(rPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
os.MkdirAll(path.Dir(wPath), os.ModePerm)
|
||||||
|
w, err := os.Create(wPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, r); err != nil {
|
||||||
|
w.Close()
|
||||||
|
os.Remove(wPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(rPath)
|
||||||
|
}
|
||||||
|
|
||||||
func lsd(d string) ([]string, error) {
|
func lsd(d string) ([]string, error) {
|
||||||
return ls(d, true)
|
return ls(d, true)
|
||||||
}
|
}
|
||||||
|
|||||||
72
testdata/ffmpeg.d/cmd/prune/main_test.go
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
d := path.Join(t.TempDir(), "record")
|
||||||
|
if err := os.MkdirAll(d, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := os.ReadFile(path.Join("testdata", "a.jpg"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(path.Join("testdata", "b.jpg"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if err := os.WriteFile(path.Join(d, "series.0.jpg"), a, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.Chtimes(path.Join(d, "series.0.jpg"), now, now.Add(-4000*time.Millisecond)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.WriteFile(path.Join(d, "series.1.jpg"), a, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.Chtimes(path.Join(d, "series.1.jpg"), now, now.Add(-3500*time.Millisecond)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.WriteFile(path.Join(d, "series.2.jpg"), b, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.Chtimes(path.Join(d, "series.2.jpg"), now, now.Add(-3000*time.Millisecond)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.WriteFile(path.Join(d, "series.3.jpg"), b, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.Chtimes(path.Join(d, "series.3.jpg"), now, now.Add(-2500*time.Millisecond)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.WriteFile(path.Join(d, "zlast.0.jpg"), b, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if err := os.Chtimes(path.Join(d, "zlast.0.jpg"), now, now.Add(-2000*time.Millisecond)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
if err := Run(context.Background(), []string{path.Dir(d), "1s"}); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
time.Sleep(time.Minute)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(time.Since(start))
|
||||||
|
|
||||||
|
if results, err := lsf(path.Join(path.Dir(d), "movement")); err != nil {
|
||||||
|
t.Fatal("failed to lsf for results:", err)
|
||||||
|
} else if len(results) != 1 {
|
||||||
|
t.Fatal(results)
|
||||||
|
} else if path.Base(results[0]) != "series.gif" {
|
||||||
|
t.Fatal(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
if records, err := lsf(d); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(records) != 1 {
|
||||||
|
t.Fatal(records)
|
||||||
|
} else if path.Base(records[0]) != "zlast.0.jpg" {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00054.jpg
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00055.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00056.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00057.jpg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00058.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00059.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00060.jpg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00061.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00062.jpg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00063.jpg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00064.jpg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00065.jpg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00066.jpg
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/2024-09-17T22-55-38.00067.jpg
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/a.jpg
vendored
Executable file
|
After Width: | Height: | Size: 9.1 MiB |
BIN
testdata/ffmpeg.d/cmd/prune/testdata/b.jpg
vendored
Executable file
|
After Width: | Height: | Size: 8.3 MiB |