Yimaru-BackEnd/internal/web_server/handlers/vimeo.go
Yared Yemane 7f8ef3373c Add paginated Vimeo video list API (GET /me/videos).
Exposes the Vimeo account library for admin workflows and syncs swagger docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 22:23:50 -07:00

576 lines
18 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/pkgs/vimeo"
vimeoservice "Yimaru-Backend/internal/services/vimeo"
"fmt"
"strconv"
"github.com/gofiber/fiber/v2"
)
type CreatePullUploadRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description"`
SourceURL string `json:"source_url" validate:"required,url"`
FileSize int64 `json:"file_size" validate:"required,gt=0"`
}
type CreateTusUploadRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description"`
FileSize int64 `json:"file_size" validate:"required,gt=0"`
}
type GetEmbedCodeRequest struct {
VideoID string `json:"video_id" validate:"required"`
Width int `json:"width"`
Height int `json:"height"`
Autoplay bool `json:"autoplay"`
Loop bool `json:"loop"`
Muted bool `json:"muted"`
Background bool `json:"background"`
}
type VimeoVideoResponse struct {
VimeoID string `json:"vimeo_id"`
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"`
EmbedURL string `json:"embed_url"`
EmbedHTML string `json:"embed_html"`
ThumbnailURL string `json:"thumbnail_url"`
Status string `json:"status"`
TranscodeStatus string `json:"transcode_status"`
}
type VimeoUploadResponse struct {
VimeoID string `json:"vimeo_id"`
URI string `json:"uri"`
Link string `json:"link"`
UploadLink string `json:"upload_link,omitempty"`
Status string `json:"status"`
}
type VimeoEmbedResponse struct {
VideoID string `json:"video_id"`
EmbedURL string `json:"embed_url"`
EmbedHTML string `json:"embed_html"`
}
type VimeoVideosListMetadata struct {
domain.Pagination
Next string `json:"next,omitempty"`
Previous string `json:"previous,omitempty"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
}
func vimeoVideoInfoToResponse(info *vimeoservice.VideoInfo) VimeoVideoResponse {
return VimeoVideoResponse{
VimeoID: info.VimeoID,
URI: info.URI,
Name: info.Name,
Description: info.Description,
Duration: info.Duration,
Width: info.Width,
Height: info.Height,
Link: info.Link,
EmbedURL: info.EmbedURL,
EmbedHTML: info.EmbedHTML,
ThumbnailURL: info.ThumbnailURL,
Status: info.Status,
TranscodeStatus: info.TranscodeStatus,
}
}
// ListVimeoVideos godoc
// @Summary List videos stored in the Vimeo account
// @Description Returns a paginated list of videos for the Vimeo API token (GET https://api.vimeo.com/me/videos)
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param page query int false "Page number (starts at 1)" default(1)
// @Param per_page query int false "Page size (Vimeo max 100)" default(25)
// @Param query query string false "Search query"
// @Param sort query string false "Sort field (e.g. date, alphabetical, plays, likes, comments, duration, relevance)"
// @Param direction query string false "asc or desc"
// @Param filter query string false "Vimeo filter (e.g. embeddable, playable)"
// @Param filter_type query string false "Vimeo filter_type when using filter"
// @Success 200 {object} domain.Response{data=[]handlers.VimeoVideoResponse,metadata=handlers.VimeoVideosListMetadata}
// @Failure 503 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/videos [get]
func (h *Handler) ListVimeoVideos(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
page, _ := strconv.Atoi(c.Query("page", "1"))
perPage, _ := strconv.Atoi(c.Query("per_page", "25"))
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 25
}
if perPage > 100 {
perPage = 100
}
params := vimeo.ListVideosParams{
Page: page,
PerPage: perPage,
Query: c.Query("query"),
Sort: c.Query("sort"),
Direction: c.Query("direction"),
Filter: c.Query("filter"),
FilterType: c.Query("filter_type"),
}
pageResult, err := h.vimeoSvc.ListVideos(c.Context(), params)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list Vimeo videos",
Error: err.Error(),
})
}
items := make([]VimeoVideoResponse, 0, len(pageResult.Videos))
for _, info := range pageResult.Videos {
if info == nil {
continue
}
items = append(items, vimeoVideoInfoToResponse(info))
}
totalPages := 0
if pageResult.PerPage > 0 && pageResult.Total > 0 {
totalPages = (pageResult.Total + pageResult.PerPage - 1) / pageResult.PerPage
}
currentPage := pageResult.Page
if currentPage < 1 {
currentPage = page
}
return c.JSON(domain.Response{
Message: "Vimeo videos listed successfully",
Data: items,
MetaData: VimeoVideosListMetadata{
Pagination: domain.Pagination{
Total: pageResult.Total,
TotalPages: totalPages,
CurrentPage: currentPage,
Limit: pageResult.PerPage,
},
Next: pageResult.Paging.Next,
Previous: pageResult.Paging.Previous,
First: pageResult.Paging.First,
Last: pageResult.Paging.Last,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetVimeoVideo godoc
// @Summary Get video information from Vimeo
// @Description Retrieves video details from Vimeo by video ID
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param video_id path string true "Vimeo Video ID"
// @Success 200 {object} VimeoVideoResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/videos/{video_id} [get]
func (h *Handler) GetVimeoVideo(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
videoID := c.Params("video_id")
if videoID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "video_id is required",
Error: "video_id path parameter is empty",
})
}
info, err := h.vimeoSvc.GetVideoInfo(c.Context(), videoID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get video info",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Video retrieved successfully",
Data: vimeoVideoInfoToResponse(info),
Success: true,
StatusCode: fiber.StatusOK,
})
}
// CreatePullUpload godoc
// @Summary Create a pull upload to Vimeo
// @Description Initiates a pull upload where Vimeo fetches the video from a URL
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param request body CreatePullUploadRequest true "Pull Upload Request"
// @Success 201 {object} VimeoUploadResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/uploads/pull [post]
func (h *Handler) CreatePullUpload(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not configured",
})
}
var req CreatePullUploadRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to validate request body",
Error: fmt.Sprintf("%v", valErrs),
})
}
result, err := h.vimeoSvc.CreatePullUpload(c.Context(), req.Name, req.Description, req.SourceURL, req.FileSize)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create Vimeo upload",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Vimeo upload created successfully",
Data: result,
Success: true,
StatusCode: fiber.StatusCreated,
})
}
// CreateTusUpload godoc
// @Summary Create a TUS resumable upload to Vimeo
// @Description Initiates a TUS resumable upload and returns the upload link
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param request body CreateTusUploadRequest true "TUS Upload Request"
// @Success 201 {object} VimeoUploadResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/uploads/tus [post]
func (h *Handler) CreateTusUpload(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
var req CreateTusUploadRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to validate request body",
Error: fmt.Sprintf("%v", valErrs),
})
}
result, err := h.vimeoSvc.CreateTusUpload(c.Context(), req.Name, req.Description, req.FileSize)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create Vimeo TUS upload",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Vimeo TUS upload created successfully",
Data: result,
Success: true,
StatusCode: fiber.StatusCreated,
})
}
// GetEmbedCode godoc
// @Summary Get embed code for a Vimeo video
// @Description Generates an embeddable player iframe for the video
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param video_id path string true "Vimeo Video ID"
// @Param width query int false "Player width" default(640)
// @Param height query int false "Player height" default(360)
// @Param autoplay query bool false "Autoplay video"
// @Param loop query bool false "Loop video"
// @Param muted query bool false "Mute video"
// @Param background query bool false "Background mode (no controls)"
// @Success 200 {object} VimeoEmbedResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/videos/{video_id}/embed [get]
func (h *Handler) GetEmbedCode(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not configured",
})
}
videoID := c.Params("video_id")
if videoID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "video_id is required",
Error: "video_id is empty",
})
}
width, _ := strconv.Atoi(c.Query("width", "640"))
height, _ := strconv.Atoi(c.Query("height", "360"))
autoplay := c.Query("autoplay") == "true"
loop := c.Query("loop") == "true"
muted := c.Query("muted") == "true"
background := c.Query("background") == "true"
opts := &vimeo.EmbedOptions{
Autoplay: autoplay,
Loop: loop,
Muted: muted,
Background: background,
Title: true,
Byline: true,
Portrait: true,
}
embedHTML, err := h.vimeoSvc.GetEmbedCode(c.Context(), videoID, width, height, opts)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get embed code",
Error: err.Error(),
})
}
embedURL := h.vimeoSvc.GeneratePlayerURL(videoID, opts)
return c.JSON(domain.Response{
Message: "Embed code retrieved successfully",
Data: VimeoEmbedResponse{
VideoID: videoID,
EmbedURL: embedURL,
EmbedHTML: embedHTML,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// DeleteVimeoVideo godoc
// @Summary Delete a video from Vimeo
// @Description Deletes a video from the Vimeo account
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param video_id path string true "Vimeo Video ID"
// @Success 204
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/videos/{video_id} [delete]
func (h *Handler) DeleteVimeoVideo(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
videoID := c.Params("video_id")
if videoID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "video_id is required",
Error: "video_id path parameter is empty",
})
}
if err := h.vimeoSvc.DeleteVideo(c.Context(), videoID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete video",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Video deleted successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetTranscodeStatus godoc
// @Summary Get transcode status of a Vimeo video
// @Description Returns the current transcoding status of a video
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param video_id path string true "Vimeo Video ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/videos/{video_id}/status [get]
func (h *Handler) GetTranscodeStatus(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
videoID := c.Params("video_id")
if videoID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "video_id is required",
Error: "video_id path parameter is empty",
})
}
status, err := h.vimeoSvc.GetTranscodeStatus(c.Context(), videoID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get transcode status",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Transcode status retrieved successfully",
Data: fiber.Map{
"video_id": videoID,
"status": status,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetSampleVideo godoc
// @Summary Get a sample Vimeo video with iframe embed
// @Description Fetches a sample video from Vimeo and returns video details along with an embeddable iframe for client-side integration
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param video_id query string false "Vimeo Video ID to use as sample" default(76979871)
// @Param width query int false "Player width" default(640)
// @Param height query int false "Player height" default(360)
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/sample [get]
func (h *Handler) GetSampleVideo(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
videoID := c.Query("video_id", "76979871")
width, _ := strconv.Atoi(c.Query("width", "640"))
height, _ := strconv.Atoi(c.Query("height", "360"))
info, iframe, err := h.vimeoSvc.GetSampleVideo(c.Context(), videoID, width, height)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get sample video",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Sample video retrieved successfully",
Data: fiber.Map{
"video": vimeoVideoInfoToResponse(info),
"iframe": iframe,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetOEmbed godoc
// @Summary Get oEmbed data for a Vimeo URL
// @Description Fetches oEmbed metadata for a Vimeo video URL
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param url query string true "Vimeo video URL"
// @Param width query int false "Desired width"
// @Param height query int false "Desired height"
// @Success 200 {object} vimeo.OEmbedResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/oembed [get]
func (h *Handler) GetOEmbed(c *fiber.Ctx) error {
if h.vimeoSvc == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
Message: "Vimeo service is not configured",
Error: "Vimeo service is not enabled or missing access token",
})
}
vimeoURL := c.Query("url")
if vimeoURL == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "url query parameter is required",
Error: "url query parameter is empty",
})
}
width, _ := strconv.Atoi(c.Query("width", "0"))
height, _ := strconv.Atoi(c.Query("height", "0"))
oembed, err := h.vimeoSvc.GetOEmbed(c.Context(), vimeoURL, width, height)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get oEmbed data",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "oEmbed data retrieved successfully",
Data: oembed,
Success: true,
StatusCode: fiber.StatusOK,
})
}