Yimaru-BackEnd/internal/pkgs/vimeo/client.go

457 lines
12 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) 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(
`<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 ""
}