274 lines
6.6 KiB
Go
274 lines
6.6 KiB
Go
package cloudconvert
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const BaseURL = "https://api.cloudconvert.com/v2"
|
|
|
|
type Client struct {
|
|
httpClient *http.Client
|
|
apiKey string
|
|
}
|
|
|
|
func NewClient(apiKey string) *Client {
|
|
return &Client{
|
|
httpClient: &http.Client{
|
|
Timeout: 60 * time.Second,
|
|
},
|
|
apiKey: apiKey,
|
|
}
|
|
}
|
|
|
|
type JobRequest struct {
|
|
Tasks map[string]interface{} `json:"tasks"`
|
|
}
|
|
|
|
type JobResponse struct {
|
|
Data Job `json:"data"`
|
|
}
|
|
|
|
type Job struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
Tasks []Task `json:"tasks"`
|
|
}
|
|
|
|
type Task struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Operation string `json:"operation"`
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
Result *TaskResult `json:"result"`
|
|
}
|
|
|
|
type TaskResult struct {
|
|
Form *UploadForm `json:"form,omitempty"`
|
|
Files []ExportFile `json:"files,omitempty"`
|
|
}
|
|
|
|
type UploadForm struct {
|
|
URL string `json:"url"`
|
|
Parameters map[string]interface{} `json:"parameters"`
|
|
}
|
|
|
|
type ExportFile struct {
|
|
Filename string `json:"filename"`
|
|
URL string `json:"url"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func (c *Client) doRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
jsonBytes, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
}
|
|
reqBody = bytes.NewReader(jsonBytes)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
return c.httpClient.Do(req)
|
|
}
|
|
|
|
func (c *Client) CreateJob(ctx context.Context, jobReq *JobRequest) (*Job, error) {
|
|
resp, err := c.doRequest(ctx, http.MethodPost, BaseURL+"/jobs", jobReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed to create job: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var jobResp JobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&jobResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode job response: %w", err)
|
|
}
|
|
|
|
return &jobResp.Data, nil
|
|
}
|
|
|
|
func (c *Client) GetJob(ctx context.Context, jobID string) (*Job, error) {
|
|
resp, err := c.doRequest(ctx, http.MethodGet, BaseURL+"/jobs/"+jobID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed to get job: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var jobResp JobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&jobResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode job response: %w", err)
|
|
}
|
|
|
|
return &jobResp.Data, nil
|
|
}
|
|
|
|
func (c *Client) UploadFile(ctx context.Context, form *UploadForm, filename string, fileData io.Reader) error {
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
|
|
for key, val := range form.Parameters {
|
|
var strVal string
|
|
switch v := val.(type) {
|
|
case string:
|
|
strVal = v
|
|
case float64:
|
|
strVal = fmt.Sprintf("%.0f", v)
|
|
default:
|
|
strVal = fmt.Sprintf("%v", v)
|
|
}
|
|
if err := writer.WriteField(key, strVal); err != nil {
|
|
return fmt.Errorf("failed to write form field %s: %w", key, err)
|
|
}
|
|
}
|
|
|
|
part, err := writer.CreateFormFile("file", filename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create form file: %w", err)
|
|
}
|
|
|
|
if _, err := io.Copy(part, fileData); err != nil {
|
|
return fmt.Errorf("failed to copy file data: %w", err)
|
|
}
|
|
|
|
if err := writer.Close(); err != nil {
|
|
return fmt.Errorf("failed to close multipart writer: %w", err)
|
|
}
|
|
|
|
uploadClient := &http.Client{
|
|
Timeout: 30 * time.Minute,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, form.URL, body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create upload request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
resp, err := uploadClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upload file: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("upload failed: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) WaitForJob(ctx context.Context, jobID string, pollInterval time.Duration, maxWait time.Duration) (*Job, error) {
|
|
deadline := time.Now().Add(maxWait)
|
|
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
job, err := c.GetJob(ctx, jobID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch job.Status {
|
|
case "finished":
|
|
return job, nil
|
|
case "error":
|
|
for _, task := range job.Tasks {
|
|
if task.Status == "error" {
|
|
return nil, fmt.Errorf("job failed: task '%s' error: %s", task.Name, task.Message)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("job failed with unknown error")
|
|
}
|
|
|
|
time.Sleep(pollInterval)
|
|
}
|
|
|
|
return nil, fmt.Errorf("job timed out after %v", maxWait)
|
|
}
|
|
|
|
func (c *Client) DownloadFile(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
|
downloadClient := &http.Client{
|
|
Timeout: 30 * time.Minute,
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to create download request: %w", err)
|
|
}
|
|
|
|
resp, err := downloadClient.Do(req)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to download file: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
resp.Body.Close()
|
|
return nil, 0, fmt.Errorf("download failed: status %d", resp.StatusCode)
|
|
}
|
|
|
|
return resp.Body, resp.ContentLength, nil
|
|
}
|
|
|
|
func (c *Client) CreateVideoCompressionJob(ctx context.Context) (*Job, error) {
|
|
jobReq := &JobRequest{
|
|
Tasks: map[string]interface{}{
|
|
"import-video": map[string]interface{}{
|
|
"operation": "import/upload",
|
|
},
|
|
"convert-video": map[string]interface{}{
|
|
"operation": "convert",
|
|
"input": "import-video",
|
|
"output_format": "mp4",
|
|
"video_codec": "x264",
|
|
"crf": 28,
|
|
"preset": "medium",
|
|
"height": 720,
|
|
"fit": "max",
|
|
"audio_codec": "aac",
|
|
"audio_bitrate": 128,
|
|
},
|
|
"export-video": map[string]interface{}{
|
|
"operation": "export/url",
|
|
"input": "convert-video",
|
|
},
|
|
},
|
|
}
|
|
|
|
return c.CreateJob(ctx, jobReq)
|
|
}
|