draft grpc
This commit is contained in:
477
internal/api/admin/videos.go
Normal file
477
internal/api/admin/videos.go
Normal file
@@ -0,0 +1,477 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/pkg/response"
|
||||
)
|
||||
|
||||
type AdminVideoPayload struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
Size int64 `json:"size"`
|
||||
Duration int32 `json:"duration"`
|
||||
Format string `json:"format"`
|
||||
OwnerEmail string `json:"owner_email,omitempty"`
|
||||
AdTemplateID *string `json:"ad_template_id,omitempty"`
|
||||
AdTemplateName string `json:"ad_template_name,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type SaveAdminVideoRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Size int64 `json:"size" binding:"required"`
|
||||
Duration int32 `json:"duration"`
|
||||
Format string `json:"format"`
|
||||
Status string `json:"status"`
|
||||
AdTemplateID *string `json:"ad_template_id,omitempty"`
|
||||
}
|
||||
|
||||
func normalizeAdminVideoStatus(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "processing", "pending":
|
||||
return "processing"
|
||||
case "failed", "error":
|
||||
return "failed"
|
||||
default:
|
||||
return "ready"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) loadAdminVideoPayload(ctx *gin.Context, video model.Video) (AdminVideoPayload, error) {
|
||||
payload := AdminVideoPayload{
|
||||
ID: video.ID,
|
||||
UserID: video.UserID,
|
||||
Title: video.Title,
|
||||
Description: adminStringValue(video.Description),
|
||||
URL: video.URL,
|
||||
Status: adminStringValue(video.Status),
|
||||
Size: video.Size,
|
||||
Duration: video.Duration,
|
||||
Format: video.Format,
|
||||
CreatedAt: adminFormatTime(video.CreatedAt),
|
||||
UpdatedAt: adminFormatTimeValue(video.UpdatedAt),
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := h.db.WithContext(ctx.Request.Context()).Select("id, email").Where("id = ?", video.UserID).First(&user).Error; err == nil {
|
||||
payload.OwnerEmail = user.Email
|
||||
}
|
||||
|
||||
var adConfig model.VideoAdConfig
|
||||
if err := h.db.WithContext(ctx.Request.Context()).Where("video_id = ?", video.ID).First(&adConfig).Error; err == nil {
|
||||
payload.AdTemplateID = &adConfig.AdTemplateID
|
||||
var template model.AdTemplate
|
||||
if err := h.db.WithContext(ctx.Request.Context()).Where("id = ?", adConfig.AdTemplateID).First(&template).Error; err == nil {
|
||||
payload.AdTemplateName = template.Name
|
||||
}
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (h *Handler) saveAdminVideoAdConfig(tx *gorm.DB, videoID, userID string, adTemplateID *string) error {
|
||||
if adTemplateID == nil {
|
||||
return nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(*adTemplateID)
|
||||
if trimmed == "" {
|
||||
return tx.Where("video_id = ? AND user_id = ?", videoID, userID).Delete(&model.VideoAdConfig{}).Error
|
||||
}
|
||||
|
||||
var template model.AdTemplate
|
||||
if err := tx.Where("id = ? AND user_id = ?", trimmed, userID).First(&template).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &gin.Error{Err: errors.New("Ad template not found"), Type: gin.ErrorTypeBind}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var existing model.VideoAdConfig
|
||||
if err := tx.Where("video_id = ? AND user_id = ?", videoID, userID).First(&existing).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return tx.Create(&model.VideoAdConfig{
|
||||
VideoID: videoID,
|
||||
UserID: userID,
|
||||
AdTemplateID: template.ID,
|
||||
VastTagURL: template.VastTagURL,
|
||||
AdFormat: template.AdFormat,
|
||||
Duration: template.Duration,
|
||||
}).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
existing.AdTemplateID = template.ID
|
||||
existing.VastTagURL = template.VastTagURL
|
||||
existing.AdFormat = template.AdFormat
|
||||
existing.Duration = template.Duration
|
||||
return tx.Save(&existing).Error
|
||||
}
|
||||
|
||||
// @Summary List All Videos
|
||||
// @Description Get paginated list of all videos across users (admin only)
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param page query int false "Page" default(1)
|
||||
// @Param limit query int false "Limit" default(20)
|
||||
// @Param search query string false "Search by title"
|
||||
// @Param user_id query string false "Filter by user ID"
|
||||
// @Param status query string false "Filter by status"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 401 {object} response.Response
|
||||
// @Failure 403 {object} response.Response
|
||||
// @Router /admin/videos [get]
|
||||
// @Security BearerAuth
|
||||
func (h *Handler) ListVideos(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
userID := strings.TrimSpace(c.Query("user_id"))
|
||||
status := strings.TrimSpace(c.Query("status"))
|
||||
|
||||
db := h.db.WithContext(ctx).Model(&model.Video{})
|
||||
if search != "" {
|
||||
like := "%" + search + "%"
|
||||
db = db.Where("title ILIKE ?", like)
|
||||
}
|
||||
if userID != "" {
|
||||
db = db.Where("user_id = ?", userID)
|
||||
}
|
||||
if status != "" && !strings.EqualFold(status, "all") {
|
||||
db = db.Where("status = ?", normalizeAdminVideoStatus(status))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to list videos")
|
||||
return
|
||||
}
|
||||
|
||||
var videos []model.Video
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&videos).Error; err != nil {
|
||||
h.logger.Error("Failed to list videos", "error", err)
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to list videos")
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]AdminVideoPayload, 0, len(videos))
|
||||
for _, v := range videos {
|
||||
payload, err := h.loadAdminVideoPayload(c, v)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to list videos")
|
||||
return
|
||||
}
|
||||
result = append(result, payload)
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"videos": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get Video Detail
|
||||
// @Description Get video detail by ID (admin only)
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param id path string true "Video ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /admin/videos/{id} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *Handler) GetVideo(c *gin.Context) {
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
response.Error(c, http.StatusNotFound, "Video not found")
|
||||
return
|
||||
}
|
||||
|
||||
var video model.Video
|
||||
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, http.StatusNotFound, "Video not found")
|
||||
return
|
||||
}
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to get video")
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := h.loadAdminVideoPayload(c, video)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to get video")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"video": payload})
|
||||
}
|
||||
|
||||
// @Summary Create Video
|
||||
// @Description Create a model video record for a user (admin only)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body SaveAdminVideoRequest true "Video payload"
|
||||
// @Success 201 {object} response.Response
|
||||
// @Router /admin/videos [post]
|
||||
// @Security BearerAuth
|
||||
func (h *Handler) CreateVideo(c *gin.Context) {
|
||||
var req SaveAdminVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.UserID) == "" || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.URL) == "" {
|
||||
response.Error(c, http.StatusBadRequest, "User ID, title, and URL are required")
|
||||
return
|
||||
}
|
||||
if req.Size < 0 {
|
||||
response.Error(c, http.StatusBadRequest, "Size must be greater than or equal to 0")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var user model.User
|
||||
if err := h.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.UserID)).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, http.StatusBadRequest, "User not found")
|
||||
return
|
||||
}
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to create video")
|
||||
return
|
||||
}
|
||||
|
||||
status := normalizeAdminVideoStatus(req.Status)
|
||||
processingStatus := strings.ToUpper(status)
|
||||
storageType := "WORKER"
|
||||
video := &model.Video{
|
||||
ID: uuid.New().String(),
|
||||
UserID: user.ID,
|
||||
Name: strings.TrimSpace(req.Title),
|
||||
Title: strings.TrimSpace(req.Title),
|
||||
Description: adminStringPtr(req.Description),
|
||||
URL: strings.TrimSpace(req.URL),
|
||||
Size: req.Size,
|
||||
Duration: req.Duration,
|
||||
Format: strings.TrimSpace(req.Format),
|
||||
Status: &status,
|
||||
ProcessingStatus: &processingStatus,
|
||||
StorageType: &storageType,
|
||||
}
|
||||
|
||||
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.saveAdminVideoAdConfig(tx, video.ID, user.ID, req.AdTemplateID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
if strings.Contains(err.Error(), "Ad template not found") {
|
||||
response.Error(c, http.StatusBadRequest, "Ad template not found")
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to create video", "error", err)
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to create video")
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := h.loadAdminVideoPayload(c, *video)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to create video")
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, gin.H{"video": payload})
|
||||
}
|
||||
|
||||
// @Summary Update Video
|
||||
// @Description Update video metadata and status (admin only)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Video ID"
|
||||
// @Param request body SaveAdminVideoRequest true "Video payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /admin/videos/{id} [put]
|
||||
// @Security BearerAuth
|
||||
func (h *Handler) UpdateVideo(c *gin.Context) {
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
response.Error(c, http.StatusNotFound, "Video not found")
|
||||
return
|
||||
}
|
||||
|
||||
var req SaveAdminVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.UserID) == "" || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.URL) == "" {
|
||||
response.Error(c, http.StatusBadRequest, "User ID, title, and URL are required")
|
||||
return
|
||||
}
|
||||
if req.Size < 0 {
|
||||
response.Error(c, http.StatusBadRequest, "Size must be greater than or equal to 0")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var video model.Video
|
||||
if err := h.db.WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, http.StatusNotFound, "Video not found")
|
||||
return
|
||||
}
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to update video")
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := h.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(req.UserID)).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, http.StatusBadRequest, "User not found")
|
||||
return
|
||||
}
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to update video")
|
||||
return
|
||||
}
|
||||
|
||||
oldSize := video.Size
|
||||
oldUserID := video.UserID
|
||||
status := normalizeAdminVideoStatus(req.Status)
|
||||
processingStatus := strings.ToUpper(status)
|
||||
video.UserID = user.ID
|
||||
video.Name = strings.TrimSpace(req.Title)
|
||||
video.Title = strings.TrimSpace(req.Title)
|
||||
video.Description = adminStringPtr(req.Description)
|
||||
video.URL = strings.TrimSpace(req.URL)
|
||||
video.Size = req.Size
|
||||
video.Duration = req.Duration
|
||||
video.Format = strings.TrimSpace(req.Format)
|
||||
video.Status = &status
|
||||
video.ProcessingStatus = &processingStatus
|
||||
|
||||
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if oldUserID == user.ID {
|
||||
delta := video.Size - oldSize
|
||||
if delta != 0 {
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used + ?, 0)", delta)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", oldUserID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", oldSize)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", user.ID).UpdateColumn("storage_used", gorm.Expr("storage_used + ?", video.Size)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if oldUserID != user.ID {
|
||||
if err := tx.Model(&model.VideoAdConfig{}).Where("video_id = ?", video.ID).Update("user_id", user.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := h.saveAdminVideoAdConfig(tx, video.ID, user.ID, req.AdTemplateID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
if strings.Contains(err.Error(), "Ad template not found") {
|
||||
response.Error(c, http.StatusBadRequest, "Ad template not found")
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to update video", "error", err)
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to update video")
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := h.loadAdminVideoPayload(c, video)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to update video")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"video": payload})
|
||||
}
|
||||
|
||||
// @Summary Delete Video (Admin)
|
||||
// @Description Delete any video by ID (admin only)
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param id path string true "Video ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 404 {object} response.Response
|
||||
// @Router /admin/videos/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
func (h *Handler) DeleteVideo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var video model.Video
|
||||
if err := h.db.WithContext(c.Request.Context()).Where("id = ?", id).First(&video).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
response.Error(c, http.StatusNotFound, "Video not found")
|
||||
return
|
||||
}
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to find video")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.db.WithContext(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("video_id = ?", video.ID).Delete(&model.VideoAdConfig{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("id = ?", video.ID).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.User{}).Where("id = ?", video.UserID).UpdateColumn("storage_used", gorm.Expr("GREATEST(storage_used - ?, 0)", video.Size)).Error
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to delete video", "error", err)
|
||||
response.Error(c, http.StatusInternalServerError, "Failed to delete video")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Video deleted"})
|
||||
}
|
||||
Reference in New Issue
Block a user