@@ -18,17 +18,35 @@ import (
1818
1919var downloadClient = & http.Client {Timeout : 10 * time .Minute }
2020
21+ type assetInfo struct {
22+ field string
23+ ext string
24+ label string
25+ }
26+
27+ var assetTypes = map [string ]assetInfo {
28+ "video" : {field : "video_url" , ext : ".mp4" , label : "video" },
29+ "captioned" : {field : "captioned_video_url" , ext : ".mp4" , label : "captioned video" },
30+ }
31+
2132func newVideoDownloadCmd (ctx * cmdContext ) * cobra.Command {
2233 var outputPath string
34+ var asset string
2335
2436 cmd := & cobra.Command {
2537 Use : "download <video-id>" ,
26- Short : "Download a video file to disk" ,
38+ Short : "Download a video file or related asset to disk" ,
2739 Args : cobra .ExactArgs (1 ),
2840 Example : "heygen video download <video-id>\n " +
41+ "heygen video download <video-id> --asset captioned\n " +
2942 "heygen video download <video-id> --output-path my-video.mp4" ,
3043 RunE : func (cmd * cobra.Command , args []string ) error {
3144 videoID := args [0 ]
45+ info , ok := assetTypes [asset ]
46+ if ! ok {
47+ return clierrors .NewUsage (
48+ fmt .Sprintf ("invalid --asset value %q: must be one of: video, captioned" , asset ))
49+ }
3250
3351 spec := & command.Spec {
3452 Endpoint : "/v3/videos/{video_id}" ,
@@ -43,7 +61,7 @@ func newVideoDownloadCmd(ctx *cmdContext) *cobra.Command {
4361 return err
4462 }
4563
46- videoURL , err := extractVideoURL (result , videoID )
64+ assetURL , err := extractAssetURL (result , videoID , info )
4765 if err != nil {
4866 return err
4967 }
@@ -52,15 +70,16 @@ func newVideoDownloadCmd(ctx *cmdContext) *cobra.Command {
5270 if dest == "" {
5371 // Sanitize: strip directory components from video ID to prevent
5472 // path traversal. Handles IDs with / or \ safely.
55- dest = filepath .Base (videoID ) + ".mp4"
73+ dest = filepath .Base (videoID ) + info . ext
5674 }
5775
58- if err := downloadFile (cmd .Context (), videoURL , dest ); err != nil {
76+ if err := downloadFile (cmd .Context (), assetURL , dest ); err != nil {
5977 return err
6078 }
6179
6280 data , err := json .Marshal (map [string ]string {
63- "message" : "Downloaded to " + dest ,
81+ "asset" : asset ,
82+ "message" : fmt .Sprintf ("Downloaded %s to %s" , info .label , dest ),
6483 "path" : dest ,
6584 })
6685 if err != nil {
@@ -71,34 +90,49 @@ func newVideoDownloadCmd(ctx *cmdContext) *cobra.Command {
7190 },
7291 }
7392
93+ cmd .Flags ().StringVar (& asset , "asset" , "video" , "Asset to download: video, captioned" )
7494 cmd .Flags ().StringVar (& outputPath , "output-path" , "" , "Output file path (default: {video-id}.mp4)" )
7595 return cmd
7696}
7797
78- func extractVideoURL (raw json.RawMessage , videoID string ) (string , error ) {
98+ func extractAssetURL (raw json.RawMessage , videoID string , info assetInfo ) (string , error ) {
7999 var resp struct {
80- Data struct {
81- VideoURL string `json:"video_url"`
82- Status string `json:"status"`
83- } `json:"data"`
100+ Data map [string ]json.RawMessage `json:"data"`
84101 }
85102 if err := json .Unmarshal (raw , & resp ); err != nil {
86103 return "" , clierrors .New ("failed to parse video response" )
87104 }
88105
89- if resp .Data .VideoURL == "" {
90- switch resp .Data .Status {
106+ var status string
107+ if rawStatus , ok := resp .Data ["status" ]; ok {
108+ _ = json .Unmarshal (rawStatus , & status )
109+ }
110+
111+ var assetURL string
112+ if rawURL , ok := resp .Data [info .field ]; ok {
113+ _ = json .Unmarshal (rawURL , & assetURL )
114+ }
115+
116+ if assetURL == "" {
117+ switch status {
91118 case "failed" , "error" :
92119 return "" , & clierrors.CLIError {
93120 Code : "video_failed" ,
94- Message : fmt .Sprintf ("video rendering failed (status: %s)" , resp . Data . Status ),
121+ Message : fmt .Sprintf ("video rendering failed (status: %s)" , status ),
95122 Hint : "Check details with: heygen video get " + videoID ,
96123 ExitCode : clierrors .ExitGeneral ,
97124 }
125+ case "completed" :
126+ return "" , & clierrors.CLIError {
127+ Code : "asset_not_available" ,
128+ Message : fmt .Sprintf ("%s not available for this video" , info .label ),
129+ Hint : assetHint (info .field ),
130+ ExitCode : clierrors .ExitGeneral ,
131+ }
98132 default :
99- msg := "video URL not available"
100- if resp . Data . Status != "" {
101- msg = fmt .Sprintf ("video URL not available (status: %s)" , resp . Data . Status )
133+ msg := fmt . Sprintf ( "%s URL not available", info . label )
134+ if status != "" {
135+ msg = fmt .Sprintf ("%s URL not available (status: %s)" , info . label , status )
102136 }
103137 return "" , & clierrors.CLIError {
104138 Code : "video_not_ready" ,
@@ -109,7 +143,16 @@ func extractVideoURL(raw json.RawMessage, videoID string) (string, error) {
109143 }
110144 }
111145
112- return resp .Data .VideoURL , nil
146+ return assetURL , nil
147+ }
148+
149+ func assetHint (field string ) string {
150+ switch field {
151+ case "captioned_video_url" :
152+ return "Video may not have been created with captions enabled."
153+ default :
154+ return ""
155+ }
113156}
114157
115158func downloadFile (ctx context.Context , videoURL , dest string ) error {
0 commit comments