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