main
Raw Download raw file
  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}