Skip to content

Commit 650ef35

Browse files
committed
cmd: add video download command (PRINFRA-139)
1 parent 040da1f commit 650ef35

3 files changed

Lines changed: 391 additions & 0 deletions

File tree

cmd/heygen/root.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Use "heygen config list" to see all configuration settings and their sources.`,
4444
root.AddCommand(newAuthCmd(ctx))
4545
root.AddCommand(newConfigCmd(ctx))
4646
registerGroups(root, ctx, gen.Groups)
47+
attachCustomCommands(root, ctx)
4748
installFlattenedHelp(root)
4849

4950
return root
@@ -74,6 +75,7 @@ func newRootCmdWithSpecs(version string, formatter output.Formatter, groups map[
7475
root.AddCommand(newAuthCmd(ctx))
7576
root.AddCommand(newConfigCmd(ctx))
7677
registerGroups(root, ctx, groups)
78+
attachCustomCommands(root, ctx)
7779
installFlattenedHelp(root)
7880

7981
return root
@@ -99,6 +101,12 @@ func registerGroups(root *cobra.Command, ctx *cmdContext, groups map[string][]*c
99101
}
100102
}
101103

104+
func attachCustomCommands(root *cobra.Command, ctx *cmdContext) {
105+
if videoGroup := findGroup(root, "video"); videoGroup != nil {
106+
videoGroup.AddCommand(newVideoDownloadCmd(ctx))
107+
}
108+
}
109+
102110
func registerSpecCommand(groupCmd *cobra.Command, spec *command.Spec, ctx *cmdContext) {
103111
path := commandPathParts(spec)
104112
if len(path) == 0 {
@@ -114,6 +122,15 @@ func registerSpecCommand(groupCmd *cobra.Command, spec *command.Spec, ctx *cmdCo
114122
parent.AddCommand(buildCobraCommand(spec, ctx))
115123
}
116124

125+
func findGroup(root *cobra.Command, name string) *cobra.Command {
126+
for _, child := range root.Commands() {
127+
if child.Name() == name {
128+
return child
129+
}
130+
}
131+
return nil
132+
}
133+
117134
func ensureIntermediateCommand(parent *cobra.Command, token string) *cobra.Command {
118135
for _, child := range parent.Commands() {
119136
if child.Name() == token {

cmd/heygen/video_download.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"time"
12+
13+
"github.com/heygen-com/heygen-cli/internal/command"
14+
clierrors "github.com/heygen-com/heygen-cli/internal/errors"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var downloadClient = &http.Client{Timeout: 10 * time.Minute}
19+
20+
func newVideoDownloadCmd(ctx *cmdContext) *cobra.Command {
21+
var outputPath string
22+
23+
cmd := &cobra.Command{
24+
Use: "download <video-id>",
25+
Short: "Download a video file to disk",
26+
Args: cobra.ExactArgs(1),
27+
Example: "heygen video download <video-id>\n" +
28+
"heygen video download <video-id> --output-path my-video.mp4",
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
videoID := args[0]
31+
32+
spec := &command.Spec{
33+
Endpoint: "/v3/videos/{video_id}",
34+
Method: http.MethodGet,
35+
}
36+
inv := &command.Invocation{
37+
PathParams: map[string]string{"video_id": videoID},
38+
QueryParams: make(url.Values),
39+
}
40+
result, err := ctx.client.Execute(spec, inv)
41+
if err != nil {
42+
return err
43+
}
44+
45+
videoURL, err := extractVideoURL(result, videoID)
46+
if err != nil {
47+
return err
48+
}
49+
50+
dest := outputPath
51+
if dest == "" {
52+
dest = videoID + ".mp4"
53+
}
54+
55+
if err := downloadFile(cmd.Context(), videoURL, dest); err != nil {
56+
return err
57+
}
58+
59+
data, err := json.Marshal(map[string]string{
60+
"message": "Downloaded to " + dest,
61+
"path": dest,
62+
})
63+
if err != nil {
64+
return clierrors.New(fmt.Sprintf("failed to encode response: %v", err))
65+
}
66+
67+
return ctx.formatter.Data(data, "", nil)
68+
},
69+
}
70+
71+
cmd.Flags().StringVar(&outputPath, "output-path", "", "Output file path (default: {video-id}.mp4)")
72+
return cmd
73+
}
74+
75+
func extractVideoURL(raw json.RawMessage, videoID string) (string, error) {
76+
var resp struct {
77+
Data struct {
78+
VideoURL string `json:"video_url"`
79+
Status string `json:"status"`
80+
} `json:"data"`
81+
}
82+
if err := json.Unmarshal(raw, &resp); err != nil {
83+
return "", clierrors.New("failed to parse video response")
84+
}
85+
86+
if resp.Data.VideoURL == "" {
87+
switch resp.Data.Status {
88+
case "failed", "error":
89+
return "", &clierrors.CLIError{
90+
Code: "video_failed",
91+
Message: fmt.Sprintf("video rendering failed (status: %s)", resp.Data.Status),
92+
Hint: "Check details with: heygen video get " + videoID,
93+
ExitCode: clierrors.ExitGeneral,
94+
}
95+
default:
96+
msg := "video URL not available"
97+
if resp.Data.Status != "" {
98+
msg = fmt.Sprintf("video URL not available (status: %s)", resp.Data.Status)
99+
}
100+
return "", &clierrors.CLIError{
101+
Code: "video_not_ready",
102+
Message: msg,
103+
Hint: "Use --wait when creating: heygen video create ... --wait",
104+
ExitCode: clierrors.ExitGeneral,
105+
}
106+
}
107+
}
108+
109+
return resp.Data.VideoURL, nil
110+
}
111+
112+
func downloadFile(ctx context.Context, videoURL, dest string) error {
113+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, videoURL, nil)
114+
if err != nil {
115+
return clierrors.New(fmt.Sprintf("failed to build download request: %v", err))
116+
}
117+
118+
resp, err := downloadClient.Do(req)
119+
if err != nil {
120+
return clierrors.New(fmt.Sprintf("failed to download video: %v", err))
121+
}
122+
defer resp.Body.Close()
123+
124+
if resp.StatusCode != http.StatusOK {
125+
return clierrors.New(fmt.Sprintf("download failed with HTTP %d", resp.StatusCode))
126+
}
127+
128+
out, err := os.Create(dest)
129+
if err != nil {
130+
return clierrors.New(fmt.Sprintf("failed to create file %q: %v", dest, err))
131+
}
132+
133+
_, copyErr := io.Copy(out, resp.Body)
134+
closeErr := out.Close()
135+
if copyErr != nil {
136+
_ = os.Remove(dest)
137+
return clierrors.New(fmt.Sprintf("download interrupted: %v", copyErr))
138+
}
139+
if closeErr != nil {
140+
_ = os.Remove(dest)
141+
return clierrors.New(fmt.Sprintf("failed to finalize file %q: %v", dest, closeErr))
142+
}
143+
144+
return nil
145+
}

0 commit comments

Comments
 (0)