package course_management import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/pkgs/vimeo" vimeoservice "Yimaru-Backend/internal/services/vimeo" "context" "fmt" "io" "net/http" "path" "time" ) func (s *Service) CreateSubCourseVideo( ctx context.Context, subCourseID int64, title string, description *string, videoURL string, duration int32, resolution *string, instructorID *string, thumbnail *string, visibility *string, displayOrder *int32, status *string, ) (domain.SubCourseVideo, error) { // Default to DIRECT provider when no Vimeo info provided provider := string(domain.VideoHostProviderDirect) return s.courseStore.CreateSubCourseVideo( ctx, subCourseID, title, description, videoURL, duration, resolution, instructorID, thumbnail, visibility, displayOrder, status, nil, nil, nil, nil, &provider, ) } // CreateSubCourseVideoWithVimeo creates a video and uploads it to Vimeo func (s *Service) CreateSubCourseVideoWithVimeo( ctx context.Context, subCourseID int64, title string, description *string, sourceURL string, fileSize int64, duration int32, resolution *string, instructorID *string, thumbnail *string, visibility *string, displayOrder *int32, ) (domain.SubCourseVideo, error) { if s.vimeoSvc == nil { return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured") } descStr := "" if description != nil { descStr = *description } var uploadResult *vimeoservice.UploadResult var err error if s.cloudConvertSvc != nil { httpClient := &http.Client{Timeout: 30 * time.Minute} req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil) if reqErr != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to create download request: %w", reqErr) } resp, dlErr := httpClient.Do(req) if dlErr != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: %w", dlErr) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: status %d", resp.StatusCode) } dlSize := resp.ContentLength if dlSize <= 0 { dlSize = fileSize } filename := path.Base(sourceURL) if filename == "" || filename == "." || filename == "/" { filename = "video.mp4" } result, compErr := s.cloudConvertSvc.CompressVideo(ctx, filename, resp.Body, dlSize) if compErr != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", compErr) } defer result.Data.Close() uploadResult, err = s.vimeoSvc.UploadVideoFile(ctx, title, descStr, result.Data, result.FileSize) } else { uploadResult, err = s.vimeoSvc.CreatePullUpload(ctx, title, descStr, sourceURL, fileSize) } if err != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to upload to Vimeo: %w", err) } embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{ Title: true, Byline: true, Portrait: true, }) embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil) provider := string(domain.VideoHostProviderVimeo) vimeoStatus := "uploading" status := "DRAFT" return s.courseStore.CreateSubCourseVideo( ctx, subCourseID, title, description, uploadResult.Link, duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status, &uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider, ) } func (s *Service) CreateSubCourseVideoWithFileUpload( ctx context.Context, subCourseID int64, title string, description *string, filename string, fileData io.Reader, fileSize int64, duration int32, resolution *string, instructorID *string, thumbnail *string, visibility *string, displayOrder *int32, ) (domain.SubCourseVideo, error) { if s.vimeoSvc == nil { return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured") } descStr := "" if description != nil { descStr = *description } videoReader := fileData videoSize := fileSize if s.cloudConvertSvc != nil { result, err := s.cloudConvertSvc.CompressVideo(ctx, filename, fileData, fileSize) if err != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", err) } defer result.Data.Close() videoReader = result.Data videoSize = result.FileSize } uploadResult, err := s.vimeoSvc.UploadVideoFile(ctx, title, descStr, videoReader, videoSize) if err != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to upload video file to Vimeo: %w", err) } embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{ Title: true, Byline: true, Portrait: true, }) embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil) provider := string(domain.VideoHostProviderVimeo) vimeoStatus := "uploading" status := "DRAFT" return s.courseStore.CreateSubCourseVideo( ctx, subCourseID, title, description, uploadResult.Link, duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status, &uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider, ) } // CreateSubCourseVideoFromVimeoID creates a video record from an existing Vimeo video func (s *Service) CreateSubCourseVideoFromVimeoID( ctx context.Context, subCourseID int64, vimeoVideoID string, title string, description *string, displayOrder *int32, instructorID *string, ) (domain.SubCourseVideo, error) { if s.vimeoSvc == nil { return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured") } // Fetch video info from Vimeo info, err := s.vimeoSvc.GetVideoInfo(ctx, vimeoVideoID) if err != nil { return domain.SubCourseVideo{}, fmt.Errorf("failed to get Vimeo video info: %w", err) } // Use Vimeo data embedHTML := vimeo.GenerateIframeEmbed(vimeoVideoID, 640, 360, nil) provider := string(domain.VideoHostProviderVimeo) vimeoStatus := info.TranscodeStatus if vimeoStatus == "" { vimeoStatus = "available" } status := "DRAFT" duration := int32(info.Duration) resolution := fmt.Sprintf("%dx%d", info.Width, info.Height) thumbnail := info.ThumbnailURL return s.courseStore.CreateSubCourseVideo( ctx, subCourseID, title, description, info.Link, duration, &resolution, instructorID, &thumbnail, nil, displayOrder, &status, &vimeoVideoID, &info.EmbedURL, &embedHTML, &vimeoStatus, &provider, ) } func (s *Service) GetSubCourseVideoByID( ctx context.Context, id int64, ) (domain.SubCourseVideo, error) { return s.courseStore.GetSubCourseVideoByID(ctx, id) } func (s *Service) GetVideosBySubCourse( ctx context.Context, subCourseID int64, ) ([]domain.SubCourseVideo, int64, error) { return s.courseStore.GetVideosBySubCourse(ctx, subCourseID) } func (s *Service) GetPublishedVideosBySubCourse( ctx context.Context, subCourseID int64, ) ([]domain.SubCourseVideo, error) { return s.courseStore.GetPublishedVideosBySubCourse(ctx, subCourseID) } func (s *Service) GetFirstIncompletePreviousVideo( ctx context.Context, userID int64, videoID int64, ) (*domain.VideoAccessBlock, error) { return s.courseStore.GetFirstIncompletePreviousVideo(ctx, userID, videoID) } func (s *Service) MarkVideoCompleted( ctx context.Context, userID int64, videoID int64, ) error { return s.courseStore.MarkVideoCompleted(ctx, userID, videoID) } func (s *Service) PublishSubCourseVideo( ctx context.Context, videoID int64, ) error { return s.courseStore.PublishSubCourseVideo(ctx, videoID) } func (s *Service) UpdateSubCourseVideo( ctx context.Context, id int64, title *string, description *string, videoURL *string, duration *int32, resolution *string, visibility *string, thumbnail *string, displayOrder *int32, status *string, ) error { return s.courseStore.UpdateSubCourseVideo(ctx, id, title, description, videoURL, duration, resolution, visibility, thumbnail, displayOrder, status) } func (s *Service) ArchiveSubCourseVideo( ctx context.Context, id int64, ) error { return s.courseStore.ArchiveSubCourseVideo(ctx, id) } func (s *Service) DeleteSubCourseVideo( ctx context.Context, id int64, ) error { return s.courseStore.DeleteSubCourseVideo(ctx, id) }