main
1// Package pipeline implements the timelapse generation phases.
2package pipeline
3
4import (
5 "context"
6 "encoding/json"
7 "fmt"
8 "log/slog"
9 "os"
10 "time"
11
12 "github.com/crash/upvs/internal/manifest"
13 "github.com/crash/upvs/internal/output"
14 "github.com/crash/upvs/internal/protect"
15)
16
17// ScanResult holds the output of the scan phase.
18type ScanResult struct {
19 Camera *protect.Camera
20 ClipIndex *manifest.ClipIndex
21 EventCount int
22}
23
24// Scan enumerates events for a day and creates the clip index.
25func Scan(ctx context.Context, client *protect.Client, layout *output.Layout, cameraRef string, date time.Time) (*ScanResult, error) {
26 slog.Info("resolving camera", slog.String("camera", cameraRef))
27
28 camera, err := client.ResolveCamera(ctx, cameraRef)
29 if err != nil {
30 return nil, fmt.Errorf("resolving camera: %w", err)
31 }
32
33 slog.Info("camera resolved",
34 slog.String("id", camera.ID),
35 slog.String("name", camera.Name))
36
37 // Save camera info
38 err = writeCameraJSON(layout.CameraJSONPath(), camera)
39 if err != nil {
40 return nil, err
41 }
42
43 slog.Info("fetching events",
44 slog.String("camera_id", camera.ID),
45 slog.String("date", date.Format("2006-01-02")))
46
47 events, err := client.ListDayEvents(ctx, camera.ID, date)
48 if err != nil {
49 return nil, fmt.Errorf("listing events: %w", err)
50 }
51
52 slog.Info("events found", slog.Int("count", len(events)))
53
54 // Build clip index
55 dateStr := date.Format("2006-01-02")
56 clipIndex := manifest.NewClipIndex(camera.ID, camera.Name, dateStr)
57
58 for _, event := range events {
59 entry := &manifest.ClipEntry{
60 EventID: event.ID,
61 StartMs: event.Start,
62 EndMs: event.End,
63 DurationMs: event.End - event.Start,
64 EventType: event.Type,
65 SmartTypes: event.SmartTypes,
66 Score: event.Score,
67 Status: manifest.StatusPending,
68 FilePath: layout.ClipPath(event.Start, event.End, event.ID),
69 }
70 clipIndex.AddClip(entry)
71 }
72
73 clipIndex.Sort()
74
75 err = clipIndex.Write(layout.ClipIndexPath())
76 if err != nil {
77 return nil, err
78 }
79
80 slog.Info("clip index written",
81 slog.String("path", layout.ClipIndexPath()),
82 slog.Int("clips", len(clipIndex.Clips)))
83
84 return &ScanResult{
85 Camera: camera,
86 ClipIndex: clipIndex,
87 EventCount: len(events),
88 }, nil
89}
90
91func writeCameraJSON(path string, camera *protect.Camera) error {
92 data, err := json.MarshalIndent(camera, "", " ")
93 if err != nil {
94 return fmt.Errorf("marshaling camera: %w", err)
95 }
96
97 err = os.WriteFile(path, data, 0644)
98 if err != nil {
99 return fmt.Errorf("writing camera.json: %w", err)
100 }
101
102 return nil
103}