main
1package pipeline
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7
8 "github.com/crash/upvs/internal/ffmpeg"
9 "github.com/crash/upvs/internal/manifest"
10 "github.com/crash/upvs/internal/output"
11)
12
13// ManifestResult holds the output of the manifest phase.
14type ManifestResult struct {
15 ValidClips int
16 TotalDuration float64 // seconds
17}
18
19// BuildManifest filters valid clips and generates the FFmpeg concat file.
20func BuildManifest(ctx context.Context, layout *output.Layout) (*ManifestResult, error) {
21 clipIndex, err := manifest.ReadClipIndex(layout.ClipIndexPath())
22 if err != nil {
23 return nil, fmt.Errorf("reading clip index: %w", err)
24 }
25
26 slog.Info("building manifest", slog.Int("total_clips", len(clipIndex.Clips)))
27
28 var validPaths []string
29 var totalDurationMs int64
30
31 for _, clip := range clipIndex.Clips {
32 if clip.Status != manifest.StatusComplete {
33 continue
34 }
35
36 // Verify file exists and is valid
37 if !fileExists(clip.FilePath) {
38 slog.Warn("clip file missing",
39 slog.String("event_id", clip.EventID),
40 slog.String("path", clip.FilePath))
41 continue
42 }
43
44 // Optionally verify with ffprobe
45 err := ffmpeg.Probe(ctx, clip.FilePath)
46 if err != nil {
47 slog.Warn("clip invalid",
48 slog.String("event_id", clip.EventID),
49 slog.String("error", err.Error()))
50 clip.Status = manifest.StatusFailed
51 clip.Error = "invalid video file"
52 continue
53 }
54
55 validPaths = append(validPaths, clip.FilePath)
56 totalDurationMs += clip.DurationMs
57 }
58
59 if len(validPaths) == 0 {
60 return nil, fmt.Errorf("no valid clips to render")
61 }
62
63 err = ffmpeg.GenerateConcatFile(layout.ConcatTxtPath(), validPaths)
64 if err != nil {
65 return nil, err
66 }
67
68 // Update clip index with any status changes
69 err = clipIndex.Write(layout.ClipIndexPath())
70 if err != nil {
71 return nil, err
72 }
73
74 totalDuration := float64(totalDurationMs) / 1000.0
75
76 slog.Info("manifest built",
77 slog.Int("valid_clips", len(validPaths)),
78 slog.Float64("total_duration_secs", totalDuration),
79 slog.String("concat_path", layout.ConcatTxtPath()))
80
81 return &ManifestResult{
82 ValidClips: len(validPaths),
83 TotalDuration: totalDuration,
84 }, nil
85}