420 lines
11 KiB
Go
420 lines
11 KiB
Go
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) 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(
|
|
`<iframe src="%s" width="%d" height="%d" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`,
|
|
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 ""
|
|
}
|