diff --git a/internal/domain/models.go b/internal/domain/models.go index a3f7dc4..9e86b33 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -7,7 +7,6 @@ type Clip struct { Title string `json:"title"` VideoURL string `json:"video_url"` ThumbnailURL string `json:"thumbnail_url"` - Duration int `json:"duration"` // seconds AuthorID int `json:"author_id"` LikesCount int `json:"likes_count"` CommentsCount int `json:"comments_count"` diff --git a/internal/ffmpeg/processor.go b/internal/ffmpeg/processor.go index 712b1c2..72c883e 100644 --- a/internal/ffmpeg/processor.go +++ b/internal/ffmpeg/processor.go @@ -4,13 +4,10 @@ import ( "bytes" "fmt" "os/exec" - "strconv" - "strings" ) type VideoProcessor interface { GenerateThumbnail(videoData []byte) ([]byte, error) - GetDuration(videoData []byte) (int, error) TrimVideo(videoData []byte, maxDuration int) ([]byte, error) } @@ -23,23 +20,24 @@ func NewVideoProcessor() VideoProcessor { func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) { cmd := exec.Command("ffmpeg", - "-i", "pipe:0", // читаем из stdin + "-i", "pipe:0", "-ss", "00:00:01", "-vframes", "1", "-q:v", "2", - "-f", "image2pipe", // вывод в pipe + "-f", "image2pipe", "-c:v", "mjpeg", - "pipe:1", // пишем в stdout + "pipe:1", ) cmd.Stdin = bytes.NewReader(videoData) var output bytes.Buffer + var stderr bytes.Buffer cmd.Stdout = &output - cmd.Stderr = &output // capture stderr for error messages + cmd.Stderr = &stderr err := cmd.Run() if err != nil { - return nil, fmt.Errorf("ffmpeg failed: %s, error: %w", output.String(), err) + return nil, fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err) } if output.Len() == 0 { @@ -49,48 +47,11 @@ func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) { return output.Bytes(), nil } -func (vp *videoProcessor) GetDuration(videoData []byte) (int, error) { - cmd := exec.Command("ffprobe", - "-i", "pipe:0", - "-show_entries", "format=duration", - "-v", "quiet", - "-of", "csv=p=0", - ) - - cmd.Stdin = bytes.NewReader(videoData) - output, err := cmd.Output() - if err != nil { - debugCmd := exec.Command("ffprobe", "-i", "pipe:0") - debugCmd.Stdin = bytes.NewReader(videoData) - debugOutput, _ := debugCmd.CombinedOutput() - return 0, fmt.Errorf("ffprobe failed: %w, debug output: %s", err, string(debugOutput)) - } - - durationStr := strings.TrimSpace(string(output)) - duration, err := strconv.ParseFloat(durationStr, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse duration '%s': %w", durationStr, err) - } - - return int(duration), nil -} - func (vp *videoProcessor) TrimVideo(videoData []byte, maxDuration int) ([]byte, error) { - // Сначала получаем длительность - duration, err := vp.GetDuration(videoData) - if err != nil { - return nil, fmt.Errorf("failed to get video duration: %w", err) - } - - // Если видео короче или равно лимиту, возвращаем оригинал - if duration <= maxDuration { - return videoData, nil - } - - // Обрезаем видео + // Просто обрезаем видео без проверки длительности cmd := exec.Command("ffmpeg", "-i", "pipe:0", - "-t", strconv.Itoa(maxDuration), + "-t", fmt.Sprintf("%d", maxDuration), "-c", "copy", "-f", "mp4", "pipe:1", @@ -98,12 +59,13 @@ func (vp *videoProcessor) TrimVideo(videoData []byte, maxDuration int) ([]byte, cmd.Stdin = bytes.NewReader(videoData) var output bytes.Buffer + var stderr bytes.Buffer cmd.Stdout = &output - cmd.Stderr = &output + cmd.Stderr = &stderr - err = cmd.Run() + err := cmd.Run() if err != nil { - return nil, fmt.Errorf("ffmpeg trim failed: %s, error: %w", output.String(), err) + return nil, fmt.Errorf("ffmpeg trim failed: %s, error: %w", stderr.String(), err) } if output.Len() == 0 { diff --git a/internal/handler/grpc_handler.go b/internal/handler/grpc_handler.go index 9a1cecc..3e170f1 100644 --- a/internal/handler/grpc_handler.go +++ b/internal/handler/grpc_handler.go @@ -181,7 +181,6 @@ func (h *GRPCHandler) clipToProto(clip *domain.Clip) *proto.Clip { Title: clip.Title, VideoUrl: clip.VideoURL, ThumbnailUrl: clip.ThumbnailURL, - Duration: int32(clip.Duration), AuthorId: int32(clip.AuthorID), LikesCount: int32(clip.LikesCount), CommentsCount: int32(clip.CommentsCount), diff --git a/internal/repository/clip_repository.go b/internal/repository/clip_repository.go index ceb534d..5485f5a 100644 --- a/internal/repository/clip_repository.go +++ b/internal/repository/clip_repository.go @@ -37,8 +37,8 @@ func NewClipRepository(db *sql.DB) ClipRepository { func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error { query := ` - INSERT INTO clips (title, video_url, thumbnail_url, duration, author_id, likes_count, comments_count, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + INSERT INTO clips (title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id ` @@ -46,7 +46,6 @@ func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error { clip.Title, clip.VideoURL, clip.ThumbnailURL, - clip.Duration, clip.AuthorID, clip.LikesCount, clip.CommentsCount, @@ -63,7 +62,7 @@ func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error { func (r *clipRepository) GetByID(ctx context.Context, id int) (*domain.Clip, error) { query := ` - SELECT id, title, video_url, thumbnail_url, duration, author_id, + SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE id = $1 AND deleted_at IS NULL @@ -75,7 +74,6 @@ func (r *clipRepository) GetByID(ctx context.Context, id int) (*domain.Clip, err &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, - &clip.Duration, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, @@ -104,7 +102,7 @@ func (r *clipRepository) GetByAuthorID(ctx context.Context, authorID, limit, off // Получаем клипы query := ` - SELECT id, title, video_url, thumbnail_url, duration, author_id, + SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE author_id = $1 AND deleted_at IS NULL @@ -126,7 +124,6 @@ func (r *clipRepository) GetByAuthorID(ctx context.Context, authorID, limit, off &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, - &clip.Duration, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, @@ -157,7 +154,7 @@ func (r *clipRepository) GetAll(ctx context.Context, limit, offset int) ([]*doma // Получаем клипы query := ` - SELECT id, title, video_url, thumbnail_url, duration, author_id, + SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE deleted_at IS NULL @@ -179,7 +176,6 @@ func (r *clipRepository) GetAll(ctx context.Context, limit, offset int) ([]*doma &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, - &clip.Duration, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, @@ -282,6 +278,7 @@ func (r *clipRepository) DecrementCommentsCount(ctx context.Context, clipID int) return nil } + func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, string, error) { query := ` SELECT video_url, thumbnail_url @@ -301,10 +298,9 @@ func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, s return videoURL, thumbnailURL, nil } -// GetClipWithURLs возвращает клип с URL func (r *clipRepository) GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) { query := ` - SELECT id, title, video_url, thumbnail_url, duration, author_id, + SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE id = $1 AND deleted_at IS NULL @@ -316,7 +312,6 @@ func (r *clipRepository) GetClipWithURLs(ctx context.Context, clipID int) (*doma &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, - &clip.Duration, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, diff --git a/internal/service/clip_service.go b/internal/service/clip_service.go index fc8b2ad..da9d097 100644 --- a/internal/service/clip_service.go +++ b/internal/service/clip_service.go @@ -54,8 +54,6 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque 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() { @@ -83,19 +81,8 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque 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. Ждем результаты всех операций + // 3. Ждем результаты операций var videoURL, thumbnailURL string - var duration int // Ждем загрузки видео select { @@ -117,19 +104,6 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque 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 { @@ -148,7 +122,6 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque Title: req.Title, VideoURL: videoURL, ThumbnailURL: thumbnailURL, - Duration: duration, AuthorID: req.UserID, LikesCount: 0, CommentsCount: 0, diff --git a/migrations/0001_initial_clips_schema.up.sql b/migrations/0001_initial_clips_schema.up.sql index 3432d4c..6917be5 100644 --- a/migrations/0001_initial_clips_schema.up.sql +++ b/migrations/0001_initial_clips_schema.up.sql @@ -1,10 +1,9 @@ -- Клипы CREATE TABLE clips ( id SERIAL PRIMARY KEY, - title VARCHAR(255) NOT NULL, + title VARCHAR(255), video_url TEXT NOT NULL, thumbnail_url TEXT NOT NULL, - duration INT NOT NULL, author_id INTEGER NOT NULL, likes_count INTEGER DEFAULT 0, comments_count INTEGER DEFAULT 0, diff --git a/proto/clip.pb.go b/proto/clip.pb.go index 85ff12d..5a1159a 100644 --- a/proto/clip.pb.go +++ b/proto/clip.pb.go @@ -1129,12 +1129,11 @@ type Clip struct { Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` VideoUrl string `protobuf:"bytes,3,opt,name=video_url,json=videoUrl,proto3" json:"video_url,omitempty"` ThumbnailUrl string `protobuf:"bytes,4,opt,name=thumbnail_url,json=thumbnailUrl,proto3" json:"thumbnail_url,omitempty"` - Duration int32 `protobuf:"varint,5,opt,name=duration,proto3" json:"duration,omitempty"` - AuthorId int32 `protobuf:"varint,6,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` - LikesCount int32 `protobuf:"varint,7,opt,name=likes_count,json=likesCount,proto3" json:"likes_count,omitempty"` - CommentsCount int32 `protobuf:"varint,8,opt,name=comments_count,json=commentsCount,proto3" json:"comments_count,omitempty"` - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + AuthorId int32 `protobuf:"varint,5,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` + LikesCount int32 `protobuf:"varint,6,opt,name=likes_count,json=likesCount,proto3" json:"likes_count,omitempty"` + CommentsCount int32 `protobuf:"varint,7,opt,name=comments_count,json=commentsCount,proto3" json:"comments_count,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1197,13 +1196,6 @@ func (x *Clip) GetThumbnailUrl() string { return "" } -func (x *Clip) GetDuration() int32 { - if x != nil { - return x.Duration - } - return 0 -} - func (x *Clip) GetAuthorId() int32 { if x != nil { return x.AuthorId @@ -1466,22 +1458,20 @@ const file_clip_proto_rawDesc = "" + "\x14DeleteCommentRequest\x12\x1d\n" + "\n" + "comment_id\x18\x01 \x01(\x05R\tcommentId\x12\x17\n" + - "\auser_id\x18\x02 \x01(\x05R\x06userId\"\xe5\x02\n" + + "\auser_id\x18\x02 \x01(\x05R\x06userId\"\xc9\x02\n" + "\x04Clip\x12\x0e\n" + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1b\n" + "\tvideo_url\x18\x03 \x01(\tR\bvideoUrl\x12#\n" + - "\rthumbnail_url\x18\x04 \x01(\tR\fthumbnailUrl\x12\x1a\n" + - "\bduration\x18\x05 \x01(\x05R\bduration\x12\x1b\n" + - "\tauthor_id\x18\x06 \x01(\x05R\bauthorId\x12\x1f\n" + - "\vlikes_count\x18\a \x01(\x05R\n" + + "\rthumbnail_url\x18\x04 \x01(\tR\fthumbnailUrl\x12\x1b\n" + + "\tauthor_id\x18\x05 \x01(\x05R\bauthorId\x12\x1f\n" + + "\vlikes_count\x18\x06 \x01(\x05R\n" + "likesCount\x12%\n" + - "\x0ecomments_count\x18\b \x01(\x05R\rcommentsCount\x129\n" + + "\x0ecomments_count\x18\a \x01(\x05R\rcommentsCount\x129\n" + "\n" + - "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "\n" + - "updated_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x87\x01\n" + + "updated_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x87\x01\n" + "\bClipLike\x12\x0e\n" + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x17\n" + "\aclip_id\x18\x02 \x01(\x05R\x06clipId\x12\x17\n" + diff --git a/proto/clip.proto b/proto/clip.proto index 31535f7..99325c9 100644 --- a/proto/clip.proto +++ b/proto/clip.proto @@ -138,12 +138,11 @@ message Clip { string title = 2; string video_url = 3; string thumbnail_url = 4; - int32 duration = 5; - int32 author_id = 6; - int32 likes_count = 7; - int32 comments_count = 8; - google.protobuf.Timestamp created_at = 9; - google.protobuf.Timestamp updated_at = 10; + int32 author_id = 5; + int32 likes_count = 6; + int32 comments_count = 7; + google.protobuf.Timestamp created_at = 8; + google.protobuf.Timestamp updated_at = 9; } message ClipLike {