218 lines
6.7 KiB
Go
218 lines
6.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"tailly_clips/internal/domain"
|
|
"tailly_clips/internal/ffmpeg"
|
|
"tailly_clips/internal/moderation"
|
|
"tailly_clips/internal/repository"
|
|
"tailly_clips/internal/storage"
|
|
"time"
|
|
)
|
|
|
|
type ModerationClient interface {
|
|
CheckVideoUrl(ctx context.Context, videoUrl string) (bool, error)
|
|
Close()
|
|
}
|
|
type ClipService interface {
|
|
CreateClip(ctx context.Context, req domain.CreateClipRequest) (*domain.Clip, error)
|
|
GetClip(ctx context.Context, clipID int) (*domain.Clip, error)
|
|
GetUserClips(ctx context.Context, userID, limit, offset int) ([]*domain.Clip, int, error)
|
|
GetClips(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error)
|
|
DeleteClip(ctx context.Context, clipID, userID int) error
|
|
}
|
|
|
|
type clipService struct {
|
|
repo repository.ClipRepository
|
|
storage storage.Storage
|
|
videoProcessor ffmpeg.VideoProcessor
|
|
modClient *moderation.ModerationClient
|
|
}
|
|
|
|
func NewClipService(repo repository.ClipRepository, storage storage.Storage, processor ffmpeg.VideoProcessor, modClient *moderation.ModerationClient) ClipService {
|
|
return &clipService{
|
|
repo: repo,
|
|
storage: storage,
|
|
videoProcessor: processor,
|
|
modClient: modClient,
|
|
}
|
|
}
|
|
|
|
func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipRequest) (*domain.Clip, error) {
|
|
const op = "service/clipService.CreateClip"
|
|
|
|
// 1. Обрезаем видео до 30 секунд в памяти
|
|
trimmedVideoData, err := s.videoProcessor.TrimVideo(req.VideoData, 30)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: failed to trim video: %w", op, err)
|
|
}
|
|
|
|
// 2. Параллельно выполняем операции
|
|
videoURLChan := make(chan string, 1)
|
|
videoErrChan := make(chan error, 1)
|
|
thumbnailChan := make(chan string, 1)
|
|
thumbnailErrChan := make(chan error, 1)
|
|
durationChan := make(chan int, 1)
|
|
durationErrChan := make(chan error, 1)
|
|
|
|
// Горутина для загрузки видео в S3
|
|
go func() {
|
|
videoURL, err := s.storage.UploadVideo(ctx, trimmedVideoData, req.FileName, req.UserID)
|
|
if err != nil {
|
|
videoErrChan <- fmt.Errorf("%s: failed to upload video: %w", op, err)
|
|
return
|
|
}
|
|
videoURLChan <- videoURL
|
|
}()
|
|
|
|
// Горутина для генерации превью
|
|
go func() {
|
|
thumbnailData, err := s.videoProcessor.GenerateThumbnail(trimmedVideoData)
|
|
if err != nil {
|
|
thumbnailErrChan <- fmt.Errorf("%s: failed to generate thumbnail: %w", op, err)
|
|
return
|
|
}
|
|
|
|
thumbnailURL, err := s.storage.UploadThumbnail(ctx, thumbnailData, req.UserID)
|
|
if err != nil {
|
|
thumbnailErrChan <- fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
|
|
return
|
|
}
|
|
thumbnailChan <- thumbnailURL
|
|
}()
|
|
|
|
// Горутина для получения длительности
|
|
go func() {
|
|
duration, err := s.videoProcessor.GetDuration(trimmedVideoData)
|
|
if err != nil {
|
|
durationErrChan <- fmt.Errorf("%s: failed to get duration: %w", op, err)
|
|
return
|
|
}
|
|
durationChan <- duration
|
|
}()
|
|
|
|
// 3. Ждем результаты всех операций
|
|
var videoURL, thumbnailURL string
|
|
var duration int
|
|
|
|
// Ждем загрузки видео
|
|
select {
|
|
case videoURL = <-videoURLChan:
|
|
case err := <-videoErrChan:
|
|
return nil, err
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("%s: operation cancelled", op)
|
|
}
|
|
|
|
// Ждем генерации превью
|
|
select {
|
|
case thumbnailURL = <-thumbnailChan:
|
|
case err := <-thumbnailErrChan:
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
return nil, err
|
|
case <-ctx.Done():
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
return nil, fmt.Errorf("%s: operation cancelled", op)
|
|
}
|
|
|
|
// Ждем получения длительности
|
|
select {
|
|
case duration = <-durationChan:
|
|
case err := <-durationErrChan:
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
|
return nil, err
|
|
case <-ctx.Done():
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
|
return nil, fmt.Errorf("%s: operation cancelled", op)
|
|
}
|
|
|
|
// 4. Модерируем обрезанное видео по URL
|
|
videoAllowed, err := s.modClient.CheckVideoUrl(ctx, videoURL)
|
|
if err != nil {
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
|
return nil, fmt.Errorf("%s: video moderation failed: %w", op, err)
|
|
}
|
|
if !videoAllowed {
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
|
return nil, fmt.Errorf("%s: video rejected by moderation service", op)
|
|
}
|
|
|
|
// 5. Создаем клип в БД
|
|
clip := &domain.Clip{
|
|
Title: req.Title,
|
|
VideoURL: videoURL,
|
|
ThumbnailURL: thumbnailURL,
|
|
Duration: duration,
|
|
AuthorID: req.UserID,
|
|
LikesCount: 0,
|
|
CommentsCount: 0,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, clip); err != nil {
|
|
s.storage.DeleteVideo(ctx, videoURL)
|
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
|
return nil, fmt.Errorf("failed to create clip: %w", err)
|
|
}
|
|
|
|
return clip, nil
|
|
}
|
|
|
|
func (s *clipService) GetClip(ctx context.Context, clipID int) (*domain.Clip, error) {
|
|
clip, err := s.repo.GetByID(ctx, clipID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get clip: %w", err)
|
|
}
|
|
return clip, nil
|
|
}
|
|
|
|
func (s *clipService) GetUserClips(ctx context.Context, userID, limit, offset int) ([]*domain.Clip, int, error) {
|
|
clips, totalCount, err := s.repo.GetByAuthorID(ctx, userID, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to get user clips: %w", err)
|
|
}
|
|
return clips, totalCount, nil
|
|
}
|
|
|
|
func (s *clipService) GetClips(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) {
|
|
clips, totalCount, err := s.repo.GetAll(ctx, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to get clips: %w", err)
|
|
}
|
|
return clips, totalCount, nil
|
|
}
|
|
|
|
func (s *clipService) DeleteClip(ctx context.Context, clipID, userID int) error {
|
|
// Получаем клип для проверки прав и получения URL
|
|
clip, err := s.repo.GetByID(ctx, clipID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get clip: %w", err)
|
|
}
|
|
|
|
// Проверяем права доступа
|
|
if clip.AuthorID != userID {
|
|
return fmt.Errorf("user %d is not the author of clip %d", userID, clipID)
|
|
}
|
|
|
|
// Удаляем файлы из S3
|
|
fileURLs := []string{clip.VideoURL, clip.ThumbnailURL}
|
|
if err := s.storage.BulkDelete(ctx, fileURLs); err != nil {
|
|
return fmt.Errorf("failed to delete files from S3: %w", err)
|
|
}
|
|
|
|
// Удаляем клип из БД
|
|
if err := s.repo.Delete(ctx, clipID); err != nil {
|
|
// Логируем ошибку но не возвращаем её, так как файлы уже удалены
|
|
log.Printf("WARNING: Failed to delete clip %d from database after S3 deletion: %v", clipID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|