main
1package cli
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/crash/upvs/internal/config"
8 "github.com/crash/upvs/internal/output"
9 "github.com/crash/upvs/internal/pipeline"
10 "github.com/crash/upvs/internal/protect"
11 "github.com/spf13/cobra"
12)
13
14var runCmd = &cobra.Command{
15 Use: "run",
16 Short: "Run full pipeline (scan + fetch + render)",
17 Long: "Execute all phases: enumerate events, download clips, and render timelapse.",
18 RunE: runAll,
19}
20
21func init() {
22 runCmd.Flags().StringVar(&dateStr, "date", "", "Target date YYYY-MM-DD (required)")
23 runCmd.Flags().IntVar(&cfg.Workers, "workers", config.DefaultWorkers, "Number of concurrent download workers")
24 runCmd.Flags().IntVar(&cfg.Retries, "retries", config.DefaultRetries, "Number of retry attempts per clip")
25 runCmd.Flags().IntVar(&cfg.TargetSecs, "target", config.DefaultTargetSecs, "Target output duration in seconds")
26 runCmd.Flags().IntVar(&cfg.FPS, "fps", config.DefaultFPS, "Output frame rate")
27 runCmd.Flags().IntVar(&cfg.CRF, "crf", config.DefaultCRF, "FFmpeg CRF quality (0-51, lower=better)")
28 runCmd.Flags().StringVar(&cfg.Preset, "preset", config.DefaultPreset, "FFmpeg preset (ultrafast to veryslow)")
29 runCmd.MarkFlagRequired("date")
30}
31
32func runAll(cmd *cobra.Command, args []string) error {
33 // Parse date
34 date, err := config.ParseDate(dateStr)
35 if err != nil {
36 return err
37 }
38 cfg.Date = date
39
40 // Validate all required fields
41 err = cfg.Validate()
42 if err != nil {
43 return err
44 }
45
46 // Create layout and ensure directories
47 layout := output.NewLayout(cfg.OutDir, cfg.Camera, cfg.DateString())
48 err = layout.EnsureDirs()
49 if err != nil {
50 return err
51 }
52
53 // Create client
54 var opts []protect.ClientOption
55 if cfg.TLSInsecure {
56 opts = append(opts, protect.WithTLSInsecure())
57 }
58 if cfg.DirectAPI {
59 opts = append(opts, protect.WithDirectAPI())
60 }
61 client := protect.NewClient(cfg.Host, opts...)
62
63 // Login
64 ctx := context.Background()
65 err = client.Login(ctx, cfg.Username, cfg.Password)
66 if err != nil {
67 return fmt.Errorf("login: %w", err)
68 }
69
70 // Phase A: Scan
71 fmt.Println("=== Phase 1: Scanning events ===")
72 scanResult, err := pipeline.Scan(ctx, client, layout, cfg.Camera, cfg.Date)
73 if err != nil {
74 return fmt.Errorf("scan phase: %w", err)
75 }
76 fmt.Printf("Found %d events for camera %s\n\n", scanResult.EventCount, scanResult.Camera.Name)
77
78 if scanResult.EventCount == 0 {
79 fmt.Println("No events found for this day. Nothing to do.")
80 return nil
81 }
82
83 // Phase B: Fetch
84 fmt.Println("=== Phase 2: Downloading clips ===")
85 fetchResult, err := pipeline.Fetch(ctx, client, layout, pipeline.FetchConfig{
86 Workers: cfg.Workers,
87 Retries: cfg.Retries,
88 })
89 if err != nil {
90 return fmt.Errorf("fetch phase: %w", err)
91 }
92 fmt.Printf("Downloaded %d, skipped %d, failed %d\n\n",
93 fetchResult.Downloaded, fetchResult.Skipped, fetchResult.Failed)
94
95 // Phase C-E: Render (includes manifest and speed calculation)
96 fmt.Println("=== Phase 3: Rendering timelapse ===")
97 renderResult, err := pipeline.Render(ctx, layout, pipeline.RenderConfig{
98 TargetSecs: cfg.TargetSecs,
99 FPS: cfg.FPS,
100 CRF: cfg.CRF,
101 Preset: cfg.Preset,
102 MinSpeedMult: cfg.MinSpeedMult,
103 MaxSpeedMult: cfg.MaxSpeedMult,
104 })
105 if err != nil {
106 return fmt.Errorf("render phase: %w", err)
107 }
108
109 fmt.Println()
110 fmt.Println("=== Complete ===")
111 fmt.Printf("Output: %s\n", renderResult.OutputPath)
112 fmt.Printf("Duration: %.1f seconds (%.1fx speed)\n",
113 renderResult.OutputDuration, renderResult.SpeedFactor)
114
115 return nil
116}