draft
This commit is contained in:
127
internal/modules/videos/handler.go
Normal file
127
internal/modules/videos/handler.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package videos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
appv1 "stream.api/internal/gen/proto/app/v1"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
appv1.UnimplementedVideosServiceServer
|
||||
module *Module
|
||||
}
|
||||
|
||||
var _ appv1.VideosServiceServer = (*Handler)(nil)
|
||||
|
||||
func NewHandler(module *Module) *Handler { return &Handler{module: module} }
|
||||
|
||||
func (h *Handler) GetUploadUrl(ctx context.Context, req *appv1.GetUploadUrlRequest) (*appv1.GetUploadUrlResponse, error) {
|
||||
result, err := h.module.runtime.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.module.GetUploadURL(ctx, GetUploadURLCommand{UserID: result.UserID, Filename: req.GetFilename()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentGetUploadURLResponse(payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) CreateVideo(ctx context.Context, req *appv1.CreateVideoRequest) (*appv1.CreateVideoResponse, error) {
|
||||
result, err := h.module.runtime.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.module.CreateVideo(ctx, CreateVideoCommand{UserID: result.UserID, Title: req.GetTitle(), Description: req.GetDescription(), URL: req.GetUrl(), Size: req.GetSize(), Duration: req.GetDuration(), Format: req.GetFormat()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentCreateVideoResponse(*payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) ListVideos(ctx context.Context, req *appv1.ListVideosRequest) (*appv1.ListVideosResponse, error) {
|
||||
result, err := h.module.runtime.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.module.ListVideos(ctx, ListVideosQuery{UserID: result.UserID, Page: req.GetPage(), Limit: req.GetLimit(), Search: req.GetSearch(), StatusFilter: req.GetStatus()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentListVideosResponse(payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) GetVideo(ctx context.Context, req *appv1.GetVideoRequest) (*appv1.GetVideoResponse, error) {
|
||||
result, err := h.module.runtime.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.module.GetVideo(ctx, GetVideoQuery{UserID: result.UserID, ID: strings.TrimSpace(req.GetId())})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentGetVideoResponse(*payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateVideo(ctx context.Context, req *appv1.UpdateVideoRequest) (*appv1.UpdateVideoResponse, error) {
|
||||
result, err := h.module.runtime.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := h.module.UpdateVideo(ctx, UpdateVideoCommand{UserID: result.UserID, ID: strings.TrimSpace(req.GetId()), Title: req.GetTitle(), Description: req.Description, URL: req.GetUrl(), Size: req.GetSize(), Duration: req.GetDuration(), Format: req.Format, Status: req.Status})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentUpdateVideoResponse(*payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteVideo(ctx context.Context, req *appv1.DeleteVideoRequest) (*appv1.MessageResponse, error) {
|
||||
result, err := h.module.runtime.Authenticate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := h.module.DeleteVideo(ctx, DeleteVideoCommand{UserID: result.UserID, ID: strings.TrimSpace(req.GetId())}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &appv1.MessageResponse{Message: "Video deleted successfully"}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ListAdminVideos(ctx context.Context, req *appv1.ListAdminVideosRequest) (*appv1.ListAdminVideosResponse, error) {
|
||||
payload, err := h.module.ListAdminVideos(ctx, ListAdminVideosQuery{Page: req.GetPage(), Limit: req.GetLimit(), Search: req.GetSearch(), UserID: req.GetUserId(), StatusFilter: req.GetStatus()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentListAdminVideosResponse(payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) GetAdminVideo(ctx context.Context, req *appv1.GetAdminVideoRequest) (*appv1.GetAdminVideoResponse, error) {
|
||||
payload, err := h.module.GetAdminVideo(ctx, GetAdminVideoQuery{ID: strings.TrimSpace(req.GetId())})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentGetAdminVideoResponse(*payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) CreateAdminVideo(ctx context.Context, req *appv1.CreateAdminVideoRequest) (*appv1.CreateAdminVideoResponse, error) {
|
||||
payload, err := h.module.CreateAdminVideo(ctx, CreateAdminVideoCommand{UserID: req.GetUserId(), Title: req.GetTitle(), Description: req.Description, URL: req.GetUrl(), Size: req.GetSize(), Duration: req.GetDuration(), Format: req.GetFormat(), AdTemplateID: req.AdTemplateId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentCreateAdminVideoResponse(*payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateAdminVideo(ctx context.Context, req *appv1.UpdateAdminVideoRequest) (*appv1.UpdateAdminVideoResponse, error) {
|
||||
payload, err := h.module.UpdateAdminVideo(ctx, UpdateAdminVideoCommand{ID: strings.TrimSpace(req.GetId()), UserID: req.GetUserId(), Title: req.GetTitle(), Description: req.Description, URL: req.GetUrl(), Size: req.GetSize(), Duration: req.GetDuration(), Format: req.GetFormat(), Status: req.GetStatus(), AdTemplateID: req.AdTemplateId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return presentUpdateAdminVideoResponse(*payload), nil
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteAdminVideo(ctx context.Context, req *appv1.DeleteAdminVideoRequest) (*appv1.MessageResponse, error) {
|
||||
if err := h.module.DeleteAdminVideo(ctx, DeleteAdminVideoCommand{ID: strings.TrimSpace(req.GetId())}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &appv1.MessageResponse{Message: "Video deleted"}, nil
|
||||
}
|
||||
550
internal/modules/videos/module.go
Normal file
550
internal/modules/videos/module.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package videos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/modules/common"
|
||||
videodomain "stream.api/internal/video"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
runtime *common.Runtime
|
||||
}
|
||||
|
||||
func New(runtime *common.Runtime) *Module {
|
||||
return &Module{runtime: runtime}
|
||||
}
|
||||
|
||||
func (m *Module) GetUploadURL(ctx context.Context, cmd GetUploadURLCommand) (*GetUploadURLResult, error) {
|
||||
storageProvider := m.runtime.StorageProvider()
|
||||
if storageProvider == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "Storage provider is not configured")
|
||||
}
|
||||
filename := strings.TrimSpace(cmd.Filename)
|
||||
if filename == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Filename is required")
|
||||
}
|
||||
fileID := uuid.New().String()
|
||||
key := fmt.Sprintf("videos/%s/%s-%s", cmd.UserID, fileID, filename)
|
||||
uploadURL, err := storageProvider.GeneratePresignedURL(key, 15*time.Minute)
|
||||
if err != nil {
|
||||
m.runtime.Logger().Error("Failed to generate upload URL", "error", err)
|
||||
return nil, status.Error(codes.Internal, "Storage error")
|
||||
}
|
||||
return &GetUploadURLResult{UploadURL: uploadURL, Key: key, FileID: fileID}, nil
|
||||
}
|
||||
|
||||
func (m *Module) CreateVideo(ctx context.Context, cmd CreateVideoCommand) (*VideoView, error) {
|
||||
videoService := m.runtime.VideoService()
|
||||
if videoService == nil {
|
||||
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
|
||||
}
|
||||
title := strings.TrimSpace(cmd.Title)
|
||||
if title == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "Title is required")
|
||||
}
|
||||
videoURL := strings.TrimSpace(cmd.URL)
|
||||
if videoURL == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "URL is required")
|
||||
}
|
||||
description := strings.TrimSpace(cmd.Description)
|
||||
created, err := videoService.CreateVideo(ctx, videodomain.CreateVideoInput{UserID: cmd.UserID, Title: title, Description: &description, URL: videoURL, Size: cmd.Size, Duration: cmd.Duration, Format: strings.TrimSpace(cmd.Format)})
|
||||
if err != nil {
|
||||
m.runtime.Logger().Error("Failed to create video", "error", err)
|
||||
switch {
|
||||
case errors.Is(err, videodomain.ErrJobServiceUnavailable):
|
||||
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
|
||||
default:
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
}
|
||||
jobID := created.Job.ID
|
||||
return &VideoView{Video: created.Video, JobID: &jobID}, nil
|
||||
}
|
||||
|
||||
func (m *Module) ListVideos(ctx context.Context, queryValue ListVideosQuery) (*ListVideosResult, error) {
|
||||
page := queryValue.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit := queryValue.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
offset := int((page - 1) * limit)
|
||||
db := m.runtime.DB().WithContext(ctx).Model(&model.Video{}).Where("user_id = ?", queryValue.UserID)
|
||||
if search := strings.TrimSpace(queryValue.Search); search != "" {
|
||||
like := "%" + search + "%"
|
||||
db = db.Where("title ILIKE ? OR description ILIKE ?", like, like)
|
||||
}
|
||||
if st := strings.TrimSpace(queryValue.StatusFilter); st != "" && !strings.EqualFold(st, "all") {
|
||||
db = db.Where("status = ?", common.NormalizeVideoStatusValue(st))
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
m.runtime.Logger().Error("Failed to count videos", "error", err)
|
||||
return nil, status.Error(codes.Internal, "Failed to fetch videos")
|
||||
}
|
||||
var videos []model.Video
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(int(limit)).Find(&videos).Error; err != nil {
|
||||
m.runtime.Logger().Error("Failed to list videos", "error", err)
|
||||
return nil, status.Error(codes.Internal, "Failed to fetch videos")
|
||||
}
|
||||
items := make([]VideoView, 0, len(videos))
|
||||
for i := range videos {
|
||||
payload, err := m.BuildVideo(ctx, &videos[i])
|
||||
if err != nil {
|
||||
m.runtime.Logger().Error("Failed to build video payload", "error", err, "video_id", videos[i].ID)
|
||||
return nil, status.Error(codes.Internal, "Failed to fetch videos")
|
||||
}
|
||||
items = append(items, payload)
|
||||
}
|
||||
return &ListVideosResult{Items: items, Total: total, Page: page, Limit: limit}, nil
|
||||
}
|
||||
|
||||
func (m *Module) GetVideo(ctx context.Context, queryValue GetVideoQuery) (*VideoView, error) {
|
||||
id := strings.TrimSpace(queryValue.ID)
|
||||
if id == "" {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
_ = m.runtime.DB().WithContext(ctx).Model(&model.Video{}).Where("id = ? AND user_id = ?", id, queryValue.UserID).UpdateColumn("views", gorm.Expr("views + ?", 1)).Error
|
||||
var video model.Video
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ? AND user_id = ?", id, queryValue.UserID).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
m.runtime.Logger().Error("Failed to fetch video", "error", err)
|
||||
return nil, status.Error(codes.Internal, "Failed to fetch video")
|
||||
}
|
||||
payload, err := m.BuildVideo(ctx, &video)
|
||||
if err != nil {
|
||||
m.runtime.Logger().Error("Failed to build video payload", "error", err, "video_id", video.ID)
|
||||
return nil, status.Error(codes.Internal, "Failed to fetch video")
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func (m *Module) UpdateVideo(ctx context.Context, cmd UpdateVideoCommand) (*VideoView, error) {
|
||||
id := strings.TrimSpace(cmd.ID)
|
||||
if id == "" {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
updates := map[string]any{}
|
||||
if title := strings.TrimSpace(cmd.Title); title != "" {
|
||||
updates["name"] = title
|
||||
updates["title"] = title
|
||||
}
|
||||
if cmd.Description != nil {
|
||||
desc := strings.TrimSpace(*cmd.Description)
|
||||
updates["description"] = common.NullableTrimmedString(&desc)
|
||||
}
|
||||
if urlValue := strings.TrimSpace(cmd.URL); urlValue != "" {
|
||||
updates["url"] = urlValue
|
||||
}
|
||||
if cmd.Size > 0 {
|
||||
updates["size"] = cmd.Size
|
||||
}
|
||||
if cmd.Duration > 0 {
|
||||
updates["duration"] = cmd.Duration
|
||||
}
|
||||
if cmd.Format != nil {
|
||||
updates["format"] = strings.TrimSpace(*cmd.Format)
|
||||
}
|
||||
if cmd.Status != nil {
|
||||
updates["status"] = common.NormalizeVideoStatusValue(*cmd.Status)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "No changes provided")
|
||||
}
|
||||
res := m.runtime.DB().WithContext(ctx).Model(&model.Video{}).Where("id = ? AND user_id = ?", id, cmd.UserID).Updates(updates)
|
||||
if res.Error != nil {
|
||||
m.runtime.Logger().Error("Failed to update video", "error", res.Error)
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
var video model.Video
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ? AND user_id = ?", id, cmd.UserID).First(&video).Error; err != nil {
|
||||
m.runtime.Logger().Error("Failed to reload video", "error", err)
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
payload, err := m.BuildVideo(ctx, &video)
|
||||
if err != nil {
|
||||
m.runtime.Logger().Error("Failed to build video payload", "error", err, "video_id", video.ID)
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func (m *Module) DeleteVideo(ctx context.Context, cmd DeleteVideoCommand) error {
|
||||
id := strings.TrimSpace(cmd.ID)
|
||||
if id == "" {
|
||||
return status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
var video model.Video
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ? AND user_id = ?", id, cmd.UserID).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
m.runtime.Logger().Error("Failed to load video", "error", err)
|
||||
return status.Error(codes.Internal, "Failed to delete video")
|
||||
}
|
||||
storageProvider := m.runtime.StorageProvider()
|
||||
if storageProvider != nil && common.ShouldDeleteStoredObject(video.URL) {
|
||||
if err := storageProvider.Delete(video.URL); err != nil {
|
||||
if parsedKey := common.ExtractObjectKey(video.URL); parsedKey != "" && parsedKey != video.URL {
|
||||
if deleteErr := storageProvider.Delete(parsedKey); deleteErr != nil {
|
||||
m.runtime.Logger().Error("Failed to delete video object", "error", deleteErr, "video_id", video.ID)
|
||||
return status.Error(codes.Internal, "Failed to delete video")
|
||||
}
|
||||
} else {
|
||||
m.runtime.Logger().Error("Failed to delete video object", "error", err, "video_id", video.ID)
|
||||
return status.Error(codes.Internal, "Failed to delete video")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := m.runtime.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("id = ? AND user_id = ?", video.ID, cmd.UserID).Delete(&model.Video{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.User{}).Where("id = ?", cmd.UserID).UpdateColumn("storage_used", gorm.Expr("storage_used - ?", video.Size)).Error
|
||||
}); err != nil {
|
||||
m.runtime.Logger().Error("Failed to delete video", "error", err)
|
||||
return status.Error(codes.Internal, "Failed to delete video")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) ListAdminVideos(ctx context.Context, queryValue ListAdminVideosQuery) (*ListAdminVideosResult, error) {
|
||||
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, limit, offset := common.AdminPageLimitOffset(queryValue.Page, queryValue.Limit)
|
||||
limitInt := int(limit)
|
||||
db := m.runtime.DB().WithContext(ctx).Model(&model.Video{})
|
||||
if search := strings.TrimSpace(queryValue.Search); search != "" {
|
||||
like := "%" + search + "%"
|
||||
db = db.Where("title ILIKE ?", like)
|
||||
}
|
||||
if userID := strings.TrimSpace(queryValue.UserID); userID != "" {
|
||||
db = db.Where("user_id = ?", userID)
|
||||
}
|
||||
if statusFilter := strings.TrimSpace(queryValue.StatusFilter); statusFilter != "" && !strings.EqualFold(statusFilter, "all") {
|
||||
db = db.Where("status = ?", common.NormalizeVideoStatusValue(statusFilter))
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to list videos")
|
||||
}
|
||||
var videos []model.Video
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(limitInt).Find(&videos).Error; err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to list videos")
|
||||
}
|
||||
items := make([]AdminVideoView, 0, len(videos))
|
||||
for i := range videos {
|
||||
payload, err := m.BuildAdminVideo(ctx, &videos[i])
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to list videos")
|
||||
}
|
||||
items = append(items, payload)
|
||||
}
|
||||
return &ListAdminVideosResult{Items: items, Total: total, Page: page, Limit: limit}, nil
|
||||
}
|
||||
|
||||
func (m *Module) GetAdminVideo(ctx context.Context, queryValue GetAdminVideoQuery) (*AdminVideoView, error) {
|
||||
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := strings.TrimSpace(queryValue.ID)
|
||||
if id == "" {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
var video model.Video
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to get video")
|
||||
}
|
||||
payload, err := m.BuildAdminVideo(ctx, &video)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to get video")
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func (m *Module) CreateAdminVideo(ctx context.Context, cmd CreateAdminVideoCommand) (*AdminVideoView, error) {
|
||||
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoService := m.runtime.VideoService()
|
||||
if videoService == nil {
|
||||
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
|
||||
}
|
||||
userID := strings.TrimSpace(cmd.UserID)
|
||||
title := strings.TrimSpace(cmd.Title)
|
||||
videoURL := strings.TrimSpace(cmd.URL)
|
||||
if userID == "" || title == "" || videoURL == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "User ID, title, and URL are required")
|
||||
}
|
||||
if cmd.Size < 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Size must be greater than or equal to 0")
|
||||
}
|
||||
created, err := videoService.CreateVideo(ctx, videodomain.CreateVideoInput{UserID: userID, Title: title, Description: cmd.Description, URL: videoURL, Size: cmd.Size, Duration: cmd.Duration, Format: strings.TrimSpace(cmd.Format), AdTemplateID: cmd.AdTemplateID})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, videodomain.ErrUserNotFound):
|
||||
return nil, status.Error(codes.InvalidArgument, "User not found")
|
||||
case errors.Is(err, videodomain.ErrAdTemplateNotFound):
|
||||
return nil, status.Error(codes.InvalidArgument, "Ad template not found")
|
||||
case errors.Is(err, videodomain.ErrJobServiceUnavailable):
|
||||
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
|
||||
default:
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
}
|
||||
payload, err := m.BuildAdminVideo(ctx, created.Video)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to create video")
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func (m *Module) UpdateAdminVideo(ctx context.Context, cmd UpdateAdminVideoCommand) (*AdminVideoView, error) {
|
||||
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := strings.TrimSpace(cmd.ID)
|
||||
userID := strings.TrimSpace(cmd.UserID)
|
||||
title := strings.TrimSpace(cmd.Title)
|
||||
videoURL := strings.TrimSpace(cmd.URL)
|
||||
if id == "" {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
if userID == "" || title == "" || videoURL == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "User ID, title, and URL are required")
|
||||
}
|
||||
if cmd.Size < 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Size must be greater than or equal to 0")
|
||||
}
|
||||
var video model.Video
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
var user model.User
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Error(codes.InvalidArgument, "User not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
oldSize := video.Size
|
||||
oldUserID := video.UserID
|
||||
statusValue := common.NormalizeVideoStatusValue(cmd.Status)
|
||||
processingStatus := strings.ToUpper(statusValue)
|
||||
video.UserID = user.ID
|
||||
video.Name = title
|
||||
video.Title = title
|
||||
video.Description = common.NullableTrimmedString(cmd.Description)
|
||||
video.URL = videoURL
|
||||
video.Size = cmd.Size
|
||||
video.Duration = cmd.Duration
|
||||
video.Format = strings.TrimSpace(cmd.Format)
|
||||
video.Status = model.StringPtr(statusValue)
|
||||
video.ProcessingStatus = model.StringPtr(processingStatus)
|
||||
video.StorageType = model.StringPtr(common.DetectStorageType(videoURL))
|
||||
err := m.runtime.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
|
||||
}
|
||||
}
|
||||
return m.saveAdminVideoAdConfig(ctx, tx, &video, user.ID, cmd.AdTemplateID)
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Ad template not found") {
|
||||
return nil, status.Error(codes.InvalidArgument, "Ad template not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
payload, err := m.BuildAdminVideo(ctx, &video)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to update video")
|
||||
}
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func (m *Module) DeleteAdminVideo(ctx context.Context, cmd DeleteAdminVideoCommand) error {
|
||||
if _, err := m.runtime.RequireAdmin(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
id := strings.TrimSpace(cmd.ID)
|
||||
if id == "" {
|
||||
return status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
var video model.Video
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("id = ?", id).First(&video).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return status.Error(codes.NotFound, "Video not found")
|
||||
}
|
||||
return status.Error(codes.Internal, "Failed to find video")
|
||||
}
|
||||
err := m.runtime.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
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 {
|
||||
return status.Error(codes.Internal, "Failed to delete video")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) BuildVideo(ctx context.Context, video *model.Video) (VideoView, error) {
|
||||
if video == nil {
|
||||
return VideoView{}, nil
|
||||
}
|
||||
jobID, err := m.LoadLatestVideoJobID(ctx, video.ID)
|
||||
if err != nil {
|
||||
return VideoView{}, err
|
||||
}
|
||||
return VideoView{Video: video, JobID: jobID}, nil
|
||||
}
|
||||
|
||||
func (m *Module) BuildAdminVideo(ctx context.Context, video *model.Video) (AdminVideoView, error) {
|
||||
if video == nil {
|
||||
return AdminVideoView{}, nil
|
||||
}
|
||||
statusValue := common.StringValue(video.Status)
|
||||
if statusValue == "" {
|
||||
statusValue = "ready"
|
||||
}
|
||||
jobID, err := m.LoadLatestVideoJobID(ctx, video.ID)
|
||||
if err != nil {
|
||||
return AdminVideoView{}, err
|
||||
}
|
||||
ownerEmail, err := m.loadAdminUserEmail(ctx, video.UserID)
|
||||
if err != nil {
|
||||
return AdminVideoView{}, err
|
||||
}
|
||||
adTemplateID, adTemplateName, err := m.loadAdminVideoAdTemplateDetails(ctx, video)
|
||||
if err != nil {
|
||||
return AdminVideoView{}, err
|
||||
}
|
||||
var createdAt *string
|
||||
if video.CreatedAt != nil {
|
||||
formatted := video.CreatedAt.UTC().Format(time.RFC3339)
|
||||
createdAt = &formatted
|
||||
}
|
||||
updated := video.UpdatedAt.UTC().Format(time.RFC3339)
|
||||
updatedAt := &updated
|
||||
return AdminVideoView{ID: video.ID, UserID: video.UserID, Title: video.Title, Description: common.NullableTrimmedString(video.Description), URL: video.URL, Status: strings.ToLower(statusValue), Size: video.Size, Duration: video.Duration, Format: video.Format, CreatedAt: createdAt, UpdatedAt: updatedAt, ProcessingStatus: common.NullableTrimmedString(video.ProcessingStatus), JobID: jobID, OwnerEmail: ownerEmail, AdTemplateID: adTemplateID, AdTemplateName: adTemplateName}, nil
|
||||
}
|
||||
|
||||
func (m *Module) LoadLatestVideoJobID(ctx context.Context, videoID string) (*string, error) {
|
||||
videoID = strings.TrimSpace(videoID)
|
||||
if videoID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var job model.Job
|
||||
if err := m.runtime.DB().WithContext(ctx).Where("config::jsonb ->> 'video_id' = ?", videoID).Order("created_at DESC").First(&job).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return common.StringPointerOrNil(job.ID), nil
|
||||
}
|
||||
|
||||
func (m *Module) saveAdminVideoAdConfig(ctx context.Context, tx *gorm.DB, video *model.Video, userID string, adTemplateID *string) error {
|
||||
if video == nil || adTemplateID == nil {
|
||||
return nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(*adTemplateID)
|
||||
if trimmed == "" {
|
||||
if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
video.AdID = nil
|
||||
return nil
|
||||
}
|
||||
var template model.AdTemplate
|
||||
if err := tx.WithContext(ctx).Select("id").Where("id = ? AND user_id = ?", trimmed, userID).First(&template).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("Ad template not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.Video{}).Where("id = ?", video.ID).Update("ad_id", template.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
video.AdID = &template.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) loadAdminUserEmail(ctx context.Context, userID string) (*string, error) {
|
||||
var user model.User
|
||||
if err := m.runtime.DB().WithContext(ctx).Select("id, email").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return common.NullableTrimmedString(&user.Email), nil
|
||||
}
|
||||
|
||||
func (m *Module) loadAdminVideoAdTemplateDetails(ctx context.Context, video *model.Video) (*string, *string, error) {
|
||||
if video == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
adTemplateID := common.NullableTrimmedString(video.AdID)
|
||||
if adTemplateID == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
adTemplateName, err := m.loadAdminAdTemplateName(ctx, *adTemplateID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return adTemplateID, adTemplateName, nil
|
||||
}
|
||||
|
||||
func (m *Module) loadAdminAdTemplateName(ctx context.Context, adTemplateID string) (*string, error) {
|
||||
var template model.AdTemplate
|
||||
if err := m.runtime.DB().WithContext(ctx).Select("id, name").Where("id = ?", adTemplateID).First(&template).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return common.NullableTrimmedString(&template.Name), nil
|
||||
}
|
||||
92
internal/modules/videos/presenter.go
Normal file
92
internal/modules/videos/presenter.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package videos
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
appv1 "stream.api/internal/gen/proto/app/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"stream.api/internal/modules/common"
|
||||
)
|
||||
|
||||
func presentGetUploadURLResponse(result *GetUploadURLResult) *appv1.GetUploadUrlResponse {
|
||||
return &appv1.GetUploadUrlResponse{UploadUrl: result.UploadURL, Key: result.Key, FileId: result.FileID}
|
||||
}
|
||||
|
||||
func presentVideo(view VideoView) *appv1.Video {
|
||||
if view.JobID != nil {
|
||||
return common.ToProtoVideo(view.Video, *view.JobID)
|
||||
}
|
||||
return common.ToProtoVideo(view.Video)
|
||||
}
|
||||
|
||||
func presentCreateVideoResponse(view VideoView) *appv1.CreateVideoResponse {
|
||||
return &appv1.CreateVideoResponse{Video: presentVideo(view)}
|
||||
}
|
||||
|
||||
func presentListVideosResponse(result *ListVideosResult) *appv1.ListVideosResponse {
|
||||
items := make([]*appv1.Video, 0, len(result.Items))
|
||||
for _, item := range result.Items {
|
||||
items = append(items, presentVideo(item))
|
||||
}
|
||||
return &appv1.ListVideosResponse{Videos: items, Total: result.Total, Page: result.Page, Limit: result.Limit}
|
||||
}
|
||||
|
||||
func presentGetVideoResponse(view VideoView) *appv1.GetVideoResponse {
|
||||
return &appv1.GetVideoResponse{Video: presentVideo(view)}
|
||||
}
|
||||
|
||||
func presentUpdateVideoResponse(view VideoView) *appv1.UpdateVideoResponse {
|
||||
return &appv1.UpdateVideoResponse{Video: presentVideo(view)}
|
||||
}
|
||||
|
||||
func presentAdminVideo(view AdminVideoView) *appv1.AdminVideo {
|
||||
return &appv1.AdminVideo{
|
||||
Id: view.ID,
|
||||
UserId: view.UserID,
|
||||
Title: view.Title,
|
||||
Description: view.Description,
|
||||
Url: view.URL,
|
||||
Status: view.Status,
|
||||
Size: view.Size,
|
||||
Duration: view.Duration,
|
||||
Format: view.Format,
|
||||
CreatedAt: parseRFC3339ToProto(view.CreatedAt),
|
||||
UpdatedAt: parseRFC3339ToProto(view.UpdatedAt),
|
||||
ProcessingStatus: view.ProcessingStatus,
|
||||
JobId: view.JobID,
|
||||
OwnerEmail: view.OwnerEmail,
|
||||
AdTemplateId: view.AdTemplateID,
|
||||
AdTemplateName: view.AdTemplateName,
|
||||
}
|
||||
}
|
||||
|
||||
func presentListAdminVideosResponse(result *ListAdminVideosResult) *appv1.ListAdminVideosResponse {
|
||||
items := make([]*appv1.AdminVideo, 0, len(result.Items))
|
||||
for _, item := range result.Items {
|
||||
items = append(items, presentAdminVideo(item))
|
||||
}
|
||||
return &appv1.ListAdminVideosResponse{Videos: items, Total: result.Total, Page: result.Page, Limit: result.Limit}
|
||||
}
|
||||
|
||||
func presentGetAdminVideoResponse(view AdminVideoView) *appv1.GetAdminVideoResponse {
|
||||
return &appv1.GetAdminVideoResponse{Video: presentAdminVideo(view)}
|
||||
}
|
||||
|
||||
func presentCreateAdminVideoResponse(view AdminVideoView) *appv1.CreateAdminVideoResponse {
|
||||
return &appv1.CreateAdminVideoResponse{Video: presentAdminVideo(view)}
|
||||
}
|
||||
|
||||
func presentUpdateAdminVideoResponse(view AdminVideoView) *appv1.UpdateAdminVideoResponse {
|
||||
return &appv1.UpdateAdminVideoResponse{Video: presentAdminVideo(view)}
|
||||
}
|
||||
|
||||
func parseRFC3339ToProto(value *string) *timestamppb.Timestamp {
|
||||
if value == nil || *value == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, *value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(parsed.UTC())
|
||||
}
|
||||
132
internal/modules/videos/types.go
Normal file
132
internal/modules/videos/types.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package videos
|
||||
|
||||
import "stream.api/internal/database/model"
|
||||
|
||||
type GetUploadURLCommand struct {
|
||||
UserID string
|
||||
Filename string
|
||||
}
|
||||
|
||||
type GetUploadURLResult struct {
|
||||
UploadURL string
|
||||
Key string
|
||||
FileID string
|
||||
}
|
||||
|
||||
type CreateVideoCommand struct {
|
||||
UserID string
|
||||
Title string
|
||||
Description string
|
||||
URL string
|
||||
Size int64
|
||||
Duration int32
|
||||
Format string
|
||||
}
|
||||
|
||||
type VideoView struct {
|
||||
Video *model.Video
|
||||
JobID *string
|
||||
}
|
||||
|
||||
type ListVideosQuery struct {
|
||||
UserID string
|
||||
Page int32
|
||||
Limit int32
|
||||
Search string
|
||||
StatusFilter string
|
||||
}
|
||||
|
||||
type ListVideosResult struct {
|
||||
Items []VideoView
|
||||
Total int64
|
||||
Page int32
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type GetVideoQuery struct {
|
||||
UserID string
|
||||
ID string
|
||||
}
|
||||
|
||||
type UpdateVideoCommand struct {
|
||||
UserID string
|
||||
ID string
|
||||
Title string
|
||||
Description *string
|
||||
URL string
|
||||
Size int64
|
||||
Duration int32
|
||||
Format *string
|
||||
Status *string
|
||||
}
|
||||
|
||||
type DeleteVideoCommand struct {
|
||||
UserID string
|
||||
ID string
|
||||
}
|
||||
|
||||
type AdminVideoView struct {
|
||||
ID string
|
||||
UserID string
|
||||
Title string
|
||||
Description *string
|
||||
URL string
|
||||
Status string
|
||||
Size int64
|
||||
Duration int32
|
||||
Format string
|
||||
CreatedAt *string
|
||||
UpdatedAt *string
|
||||
ProcessingStatus *string
|
||||
JobID *string
|
||||
OwnerEmail *string
|
||||
AdTemplateID *string
|
||||
AdTemplateName *string
|
||||
}
|
||||
|
||||
type ListAdminVideosQuery struct {
|
||||
Page int32
|
||||
Limit int32
|
||||
Search string
|
||||
UserID string
|
||||
StatusFilter string
|
||||
}
|
||||
|
||||
type ListAdminVideosResult struct {
|
||||
Items []AdminVideoView
|
||||
Total int64
|
||||
Page int32
|
||||
Limit int32
|
||||
}
|
||||
|
||||
type GetAdminVideoQuery struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type CreateAdminVideoCommand struct {
|
||||
UserID string
|
||||
Title string
|
||||
Description *string
|
||||
URL string
|
||||
Size int64
|
||||
Duration int32
|
||||
Format string
|
||||
AdTemplateID *string
|
||||
}
|
||||
|
||||
type UpdateAdminVideoCommand struct {
|
||||
ID string
|
||||
UserID string
|
||||
Title string
|
||||
Description *string
|
||||
URL string
|
||||
Size int64
|
||||
Duration int32
|
||||
Format string
|
||||
Status string
|
||||
AdTemplateID *string
|
||||
}
|
||||
|
||||
type DeleteAdminVideoCommand struct {
|
||||
ID string
|
||||
}
|
||||
Reference in New Issue
Block a user