package vimeo import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strconv" "time" ) const ( BaseURL = "https://api.vimeo.com" APIVersion = "application/vnd.vimeo.*+json;version=3.4" ) type Client struct { httpClient *http.Client accessToken string } func NewClient(accessToken string) *Client { return &Client{ httpClient: &http.Client{ Timeout: 30 * time.Second, }, accessToken: accessToken, } } type Video struct { URI string `json:"uri"` Name string `json:"name"` Description string `json:"description"` Duration int `json:"duration"` Width int `json:"width"` Height int `json:"height"` Link string `json:"link"` PlayerEmbedURL string `json:"player_embed_url"` Pictures *Pictures `json:"pictures"` Status string `json:"status"` Transcode *Transcode `json:"transcode"` Privacy *Privacy `json:"privacy"` Embed *Embed `json:"embed"` CreatedTime time.Time `json:"created_time"` ModifiedTime time.Time `json:"modified_time"` } type Pictures struct { URI string `json:"uri"` Active bool `json:"active"` Sizes []Size `json:"sizes"` BaseURL string `json:"base_link"` } type Size struct { Width int `json:"width"` Height int `json:"height"` Link string `json:"link"` } type Transcode struct { Status string `json:"status"` } type Privacy struct { View string `json:"view"` Embed string `json:"embed"` Download bool `json:"download"` } type Embed struct { HTML string `json:"html"` Badges struct { HDR bool `json:"hdr"` Live struct{ Streaming bool } `json:"live"` StaffPick struct{ Normal bool } `json:"staff_pick"` VOD bool `json:"vod"` WeekendChallenge bool `json:"weekend_challenge"` } `json:"badges"` } type UploadRequest struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Upload UploadParams `json:"upload"` Privacy *PrivacyParams `json:"privacy,omitempty"` } type UploadParams struct { Approach string `json:"approach"` Size int64 `json:"size,omitempty"` Link string `json:"link,omitempty"` RedirectURL string `json:"redirect_url,omitempty"` } type PrivacyParams struct { View string `json:"view,omitempty"` Embed string `json:"embed,omitempty"` Download bool `json:"download,omitempty"` } type UploadResponse struct { URI string `json:"uri"` Name string `json:"name"` Link string `json:"link"` Upload struct { Status string `json:"status"` UploadLink string `json:"upload_link"` Approach string `json:"approach"` Size int64 `json:"size"` } `json:"upload"` Transcode *Transcode `json:"transcode"` } type OEmbedResponse struct { Type string `json:"type"` Version string `json:"version"` ProviderName string `json:"provider_name"` ProviderURL string `json:"provider_url"` Title string `json:"title"` AuthorName string `json:"author_name"` AuthorURL string `json:"author_url"` IsPlus string `json:"is_plus"` HTML string `json:"html"` Width int `json:"width"` Height int `json:"height"` Duration int `json:"duration"` Description string `json:"description"` ThumbnailURL string `json:"thumbnail_url"` ThumbnailWidth int `json:"thumbnail_width"` ThumbnailHeight int `json:"thumbnail_height"` VideoID int64 `json:"video_id"` } type UpdateVideoRequest struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Privacy *PrivacyParams `json:"privacy,omitempty"` } func (c *Client) doRequest(ctx context.Context, method, path 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, BaseURL+path, reqBody) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.accessToken) req.Header.Set("Accept", APIVersion) if body != nil { req.Header.Set("Content-Type", "application/json") } return c.httpClient.Do(req) } func (c *Client) GetVideo(ctx context.Context, videoID string) (*Video, error) { resp, err := c.doRequest(ctx, http.MethodGet, "/videos/"+videoID, 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 video: status %d, body: %s", resp.StatusCode, string(bodyBytes)) } var video Video if err := json.NewDecoder(resp.Body).Decode(&video); err != nil { return nil, fmt.Errorf("failed to decode video response: %w", err) } return &video, nil } func (c *Client) CreateUpload(ctx context.Context, req *UploadRequest) (*UploadResponse, error) { resp, err := c.doRequest(ctx, http.MethodPost, "/me/videos", req) 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 upload: status %d, body: %s", resp.StatusCode, string(bodyBytes)) } var uploadResp UploadResponse if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil { return nil, fmt.Errorf("failed to decode upload response: %w", err) } return &uploadResp, nil } func (c *Client) CreatePullUpload(ctx context.Context, name, description, sourceURL string, fileSize int64) (*UploadResponse, error) { req := &UploadRequest{ Name: name, Description: description, Upload: UploadParams{ Approach: "pull", Size: fileSize, Link: sourceURL, }, Privacy: &PrivacyParams{ View: "unlisted", Embed: "public", }, } return c.CreateUpload(ctx, req) } func (c *Client) CreateTusUpload(ctx context.Context, name, description string, fileSize int64) (*UploadResponse, error) { req := &UploadRequest{ Name: name, Description: description, Upload: UploadParams{ Approach: "tus", Size: fileSize, }, Privacy: &PrivacyParams{ View: "unlisted", Embed: "public", }, } return c.CreateUpload(ctx, req) } func (c *Client) UploadTusVideoFile(ctx context.Context, uploadLink string, fileData io.Reader, fileSize int64) error { uploadClient := &http.Client{ Timeout: 30 * time.Minute, } req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uploadLink, fileData) if err != nil { return fmt.Errorf("failed to create TUS upload request: %w", err) } req.Header.Set("Tus-Resumable", "1.0.0") req.Header.Set("Upload-Offset", "0") req.Header.Set("Content-Type", "application/offset+octet-stream") req.ContentLength = fileSize resp, err := uploadClient.Do(req) if err != nil { return fmt.Errorf("failed to upload video file to Vimeo: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("TUS upload failed: status %d, body: %s", resp.StatusCode, string(bodyBytes)) } offsetStr := resp.Header.Get("Upload-Offset") if offsetStr != "" { offset, err := strconv.ParseInt(offsetStr, 10, 64) if err == nil && offset < fileSize { return fmt.Errorf("incomplete upload: uploaded %d of %d bytes", offset, fileSize) } } return nil } func (c *Client) UpdateVideo(ctx context.Context, videoID string, req *UpdateVideoRequest) (*Video, error) { resp, err := c.doRequest(ctx, http.MethodPatch, "/videos/"+videoID, req) 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 update video: status %d, body: %s", resp.StatusCode, string(bodyBytes)) } var video Video if err := json.NewDecoder(resp.Body).Decode(&video); err != nil { return nil, fmt.Errorf("failed to decode video response: %w", err) } return &video, nil } func (c *Client) DeleteVideo(ctx context.Context, videoID string) error { resp, err := c.doRequest(ctx, http.MethodDelete, "/videos/"+videoID, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to delete video: status %d, body: %s", resp.StatusCode, string(bodyBytes)) } return nil } func (c *Client) GetTranscodeStatus(ctx context.Context, videoID string) (string, error) { video, err := c.GetVideo(ctx, videoID) if err != nil { return "", err } if video.Transcode != nil { return video.Transcode.Status, nil } return "unknown", nil } func GetOEmbed(ctx context.Context, vimeoURL string, width, height int) (*OEmbedResponse, error) { client := &http.Client{Timeout: 10 * time.Second} oembedURL := fmt.Sprintf("https://vimeo.com/api/oembed.json?url=%s", vimeoURL) if width > 0 { oembedURL += "&width=" + strconv.Itoa(width) } if height > 0 { oembedURL += "&height=" + strconv.Itoa(height) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, oembedURL, nil) if err != nil { return nil, fmt.Errorf("failed to create oembed request: %w", err) } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch oembed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("oembed failed: status %d, body: %s", resp.StatusCode, string(bodyBytes)) } var oembed OEmbedResponse if err := json.NewDecoder(resp.Body).Decode(&oembed); err != nil { return nil, fmt.Errorf("failed to decode oembed response: %w", err) } return &oembed, nil } func GenerateEmbedURL(videoID string, options *EmbedOptions) string { url := fmt.Sprintf("https://player.vimeo.com/video/%s", videoID) if options == nil { return url } params := "" if options.Autoplay { params += "&autoplay=1" } if options.Loop { params += "&loop=1" } if options.Muted { params += "&muted=1" } if !options.Title { params += "&title=0" } if !options.Byline { params += "&byline=0" } if !options.Portrait { params += "&portrait=0" } if options.Color != "" { params += "&color=" + options.Color } if options.Background { params += "&background=1" } if options.Responsive { params += "&responsive=1" } if params != "" { url += "?" + params[1:] } return url } type EmbedOptions struct { Autoplay bool Loop bool Muted bool Title bool Byline bool Portrait bool Color string Background bool Responsive bool } func GenerateIframeEmbed(videoID string, width, height int, options *EmbedOptions) string { embedURL := GenerateEmbedURL(videoID, options) return fmt.Sprintf( ``, embedURL, width, height, ) } func ExtractVideoID(vimeoURL string) string { // Handle URLs like: // - https://vimeo.com/123456789 // - https://player.vimeo.com/video/123456789 // - /videos/123456789 patterns := []string{ "vimeo.com/", "player.vimeo.com/video/", "/videos/", } for _, pattern := range patterns { if idx := len(pattern); len(vimeoURL) > idx { for i := 0; i < len(vimeoURL)-len(pattern)+1; i++ { if vimeoURL[i:i+len(pattern)] == pattern { videoID := "" for j := i + len(pattern); j < len(vimeoURL); j++ { if vimeoURL[j] >= '0' && vimeoURL[j] <= '9' { videoID += string(vimeoURL[j]) } else { break } } if videoID != "" { return videoID } } } } } return "" }