tailly_clips/internal/service/clip_service.go
admin 57eba68496
Some checks failed
continuous-integration/drone/push Build is failing
v.0.0.1 Создан сервис клипов
2025-09-02 11:58:10 +03:00

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
}