v.0.0.2 Убран duration из клипов
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
admin 2025-09-02 23:03:30 +03:00
parent 10466568d4
commit 7bfc7eb9a8
8 changed files with 38 additions and 122 deletions

View File

@ -7,7 +7,6 @@ type Clip struct {
Title string `json:"title"` Title string `json:"title"`
VideoURL string `json:"video_url"` VideoURL string `json:"video_url"`
ThumbnailURL string `json:"thumbnail_url"` ThumbnailURL string `json:"thumbnail_url"`
Duration int `json:"duration"` // seconds
AuthorID int `json:"author_id"` AuthorID int `json:"author_id"`
LikesCount int `json:"likes_count"` LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"` CommentsCount int `json:"comments_count"`

View File

@ -4,13 +4,10 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os/exec" "os/exec"
"strconv"
"strings"
) )
type VideoProcessor interface { type VideoProcessor interface {
GenerateThumbnail(videoData []byte) ([]byte, error) GenerateThumbnail(videoData []byte) ([]byte, error)
GetDuration(videoData []byte) (int, error)
TrimVideo(videoData []byte, maxDuration int) ([]byte, error) TrimVideo(videoData []byte, maxDuration int) ([]byte, error)
} }
@ -23,23 +20,24 @@ func NewVideoProcessor() VideoProcessor {
func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) { func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) {
cmd := exec.Command("ffmpeg", cmd := exec.Command("ffmpeg",
"-i", "pipe:0", // читаем из stdin "-i", "pipe:0",
"-ss", "00:00:01", "-ss", "00:00:01",
"-vframes", "1", "-vframes", "1",
"-q:v", "2", "-q:v", "2",
"-f", "image2pipe", // вывод в pipe "-f", "image2pipe",
"-c:v", "mjpeg", "-c:v", "mjpeg",
"pipe:1", // пишем в stdout "pipe:1",
) )
cmd.Stdin = bytes.NewReader(videoData) cmd.Stdin = bytes.NewReader(videoData)
var output bytes.Buffer var output bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &output cmd.Stdout = &output
cmd.Stderr = &output // capture stderr for error messages cmd.Stderr = &stderr
err := cmd.Run() err := cmd.Run()
if err != nil { 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 { if output.Len() == 0 {
@ -49,48 +47,11 @@ func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) {
return output.Bytes(), nil 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) { 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", cmd := exec.Command("ffmpeg",
"-i", "pipe:0", "-i", "pipe:0",
"-t", strconv.Itoa(maxDuration), "-t", fmt.Sprintf("%d", maxDuration),
"-c", "copy", "-c", "copy",
"-f", "mp4", "-f", "mp4",
"pipe:1", "pipe:1",
@ -98,12 +59,13 @@ func (vp *videoProcessor) TrimVideo(videoData []byte, maxDuration int) ([]byte,
cmd.Stdin = bytes.NewReader(videoData) cmd.Stdin = bytes.NewReader(videoData)
var output bytes.Buffer var output bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &output cmd.Stdout = &output
cmd.Stderr = &output cmd.Stderr = &stderr
err = cmd.Run() err := cmd.Run()
if err != nil { 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 { if output.Len() == 0 {

View File

@ -181,7 +181,6 @@ func (h *GRPCHandler) clipToProto(clip *domain.Clip) *proto.Clip {
Title: clip.Title, Title: clip.Title,
VideoUrl: clip.VideoURL, VideoUrl: clip.VideoURL,
ThumbnailUrl: clip.ThumbnailURL, ThumbnailUrl: clip.ThumbnailURL,
Duration: int32(clip.Duration),
AuthorId: int32(clip.AuthorID), AuthorId: int32(clip.AuthorID),
LikesCount: int32(clip.LikesCount), LikesCount: int32(clip.LikesCount),
CommentsCount: int32(clip.CommentsCount), CommentsCount: int32(clip.CommentsCount),

View File

@ -37,8 +37,8 @@ func NewClipRepository(db *sql.DB) ClipRepository {
func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error { func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error {
query := ` query := `
INSERT INTO clips (title, video_url, thumbnail_url, duration, author_id, likes_count, comments_count, created_at, updated_at) 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, $9) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id RETURNING id
` `
@ -46,7 +46,6 @@ func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error {
clip.Title, clip.Title,
clip.VideoURL, clip.VideoURL,
clip.ThumbnailURL, clip.ThumbnailURL,
clip.Duration,
clip.AuthorID, clip.AuthorID,
clip.LikesCount, clip.LikesCount,
clip.CommentsCount, 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) { func (r *clipRepository) GetByID(ctx context.Context, id int) (*domain.Clip, error) {
query := ` 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 likes_count, comments_count, created_at, updated_at
FROM clips FROM clips
WHERE id = $1 AND deleted_at IS NULL 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.Title,
&clip.VideoURL, &clip.VideoURL,
&clip.ThumbnailURL, &clip.ThumbnailURL,
&clip.Duration,
&clip.AuthorID, &clip.AuthorID,
&clip.LikesCount, &clip.LikesCount,
&clip.CommentsCount, &clip.CommentsCount,
@ -104,7 +102,7 @@ func (r *clipRepository) GetByAuthorID(ctx context.Context, authorID, limit, off
// Получаем клипы // Получаем клипы
query := ` 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 likes_count, comments_count, created_at, updated_at
FROM clips FROM clips
WHERE author_id = $1 AND deleted_at IS NULL 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.Title,
&clip.VideoURL, &clip.VideoURL,
&clip.ThumbnailURL, &clip.ThumbnailURL,
&clip.Duration,
&clip.AuthorID, &clip.AuthorID,
&clip.LikesCount, &clip.LikesCount,
&clip.CommentsCount, &clip.CommentsCount,
@ -157,7 +154,7 @@ func (r *clipRepository) GetAll(ctx context.Context, limit, offset int) ([]*doma
// Получаем клипы // Получаем клипы
query := ` 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 likes_count, comments_count, created_at, updated_at
FROM clips FROM clips
WHERE deleted_at IS NULL WHERE deleted_at IS NULL
@ -179,7 +176,6 @@ func (r *clipRepository) GetAll(ctx context.Context, limit, offset int) ([]*doma
&clip.Title, &clip.Title,
&clip.VideoURL, &clip.VideoURL,
&clip.ThumbnailURL, &clip.ThumbnailURL,
&clip.Duration,
&clip.AuthorID, &clip.AuthorID,
&clip.LikesCount, &clip.LikesCount,
&clip.CommentsCount, &clip.CommentsCount,
@ -282,6 +278,7 @@ func (r *clipRepository) DecrementCommentsCount(ctx context.Context, clipID int)
return nil return nil
} }
func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, string, error) { func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, string, error) {
query := ` query := `
SELECT video_url, thumbnail_url SELECT video_url, thumbnail_url
@ -301,10 +298,9 @@ func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, s
return videoURL, thumbnailURL, nil return videoURL, thumbnailURL, nil
} }
// GetClipWithURLs возвращает клип с URL
func (r *clipRepository) GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) { func (r *clipRepository) GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) {
query := ` 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 likes_count, comments_count, created_at, updated_at
FROM clips FROM clips
WHERE id = $1 AND deleted_at IS NULL 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.Title,
&clip.VideoURL, &clip.VideoURL,
&clip.ThumbnailURL, &clip.ThumbnailURL,
&clip.Duration,
&clip.AuthorID, &clip.AuthorID,
&clip.LikesCount, &clip.LikesCount,
&clip.CommentsCount, &clip.CommentsCount,

View File

@ -54,8 +54,6 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque
videoErrChan := make(chan error, 1) videoErrChan := make(chan error, 1)
thumbnailChan := make(chan string, 1) thumbnailChan := make(chan string, 1)
thumbnailErrChan := make(chan error, 1) thumbnailErrChan := make(chan error, 1)
durationChan := make(chan int, 1)
durationErrChan := make(chan error, 1)
// Горутина для загрузки видео в S3 // Горутина для загрузки видео в S3
go func() { go func() {
@ -83,19 +81,8 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque
thumbnailChan <- thumbnailURL thumbnailChan <- thumbnailURL
}() }()
// Горутина для получения длительности // 3. Ждем результаты операций
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 videoURL, thumbnailURL string
var duration int
// Ждем загрузки видео // Ждем загрузки видео
select { select {
@ -117,19 +104,6 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque
return nil, fmt.Errorf("%s: operation cancelled", op) 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 // 4. Модерируем обрезанное видео по URL
videoAllowed, err := s.modClient.CheckVideoUrl(ctx, videoURL) videoAllowed, err := s.modClient.CheckVideoUrl(ctx, videoURL)
if err != nil { if err != nil {
@ -148,7 +122,6 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque
Title: req.Title, Title: req.Title,
VideoURL: videoURL, VideoURL: videoURL,
ThumbnailURL: thumbnailURL, ThumbnailURL: thumbnailURL,
Duration: duration,
AuthorID: req.UserID, AuthorID: req.UserID,
LikesCount: 0, LikesCount: 0,
CommentsCount: 0, CommentsCount: 0,

View File

@ -1,10 +1,9 @@
-- Клипы -- Клипы
CREATE TABLE clips ( CREATE TABLE clips (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL, title VARCHAR(255),
video_url TEXT NOT NULL, video_url TEXT NOT NULL,
thumbnail_url TEXT NOT NULL, thumbnail_url TEXT NOT NULL,
duration INT NOT NULL,
author_id INTEGER NOT NULL, author_id INTEGER NOT NULL,
likes_count INTEGER DEFAULT 0, likes_count INTEGER DEFAULT 0,
comments_count INTEGER DEFAULT 0, comments_count INTEGER DEFAULT 0,

View File

@ -1129,12 +1129,11 @@ type Clip struct {
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` 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"` 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"` 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,5,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"`
AuthorId int32 `protobuf:"varint,6,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"`
LikesCount int32 `protobuf:"varint,7,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"`
CommentsCount int32 `protobuf:"varint,8,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"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,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"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1197,13 +1196,6 @@ func (x *Clip) GetThumbnailUrl() string {
return "" return ""
} }
func (x *Clip) GetDuration() int32 {
if x != nil {
return x.Duration
}
return 0
}
func (x *Clip) GetAuthorId() int32 { func (x *Clip) GetAuthorId() int32 {
if x != nil { if x != nil {
return x.AuthorId return x.AuthorId
@ -1466,22 +1458,20 @@ const file_clip_proto_rawDesc = "" +
"\x14DeleteCommentRequest\x12\x1d\n" + "\x14DeleteCommentRequest\x12\x1d\n" +
"\n" + "\n" +
"comment_id\x18\x01 \x01(\x05R\tcommentId\x12\x17\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" + "\x04Clip\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x14\n" + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x14\n" +
"\x05title\x18\x02 \x01(\tR\x05title\x12\x1b\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1b\n" +
"\tvideo_url\x18\x03 \x01(\tR\bvideoUrl\x12#\n" + "\tvideo_url\x18\x03 \x01(\tR\bvideoUrl\x12#\n" +
"\rthumbnail_url\x18\x04 \x01(\tR\fthumbnailUrl\x12\x1a\n" + "\rthumbnail_url\x18\x04 \x01(\tR\fthumbnailUrl\x12\x1b\n" +
"\bduration\x18\x05 \x01(\x05R\bduration\x12\x1b\n" + "\tauthor_id\x18\x05 \x01(\x05R\bauthorId\x12\x1f\n" +
"\tauthor_id\x18\x06 \x01(\x05R\bauthorId\x12\x1f\n" + "\vlikes_count\x18\x06 \x01(\x05R\n" +
"\vlikes_count\x18\a \x01(\x05R\n" +
"likesCount\x12%\n" + "likesCount\x12%\n" +
"\x0ecomments_count\x18\b \x01(\x05R\rcommentsCount\x129\n" + "\x0ecomments_count\x18\a \x01(\x05R\rcommentsCount\x129\n" +
"\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" + "\n" +
"updated_at\x18\n" + "updated_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x87\x01\n" +
" \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x87\x01\n" +
"\bClipLike\x12\x0e\n" + "\bClipLike\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x17\n" + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x17\n" +
"\aclip_id\x18\x02 \x01(\x05R\x06clipId\x12\x17\n" + "\aclip_id\x18\x02 \x01(\x05R\x06clipId\x12\x17\n" +

View File

@ -138,12 +138,11 @@ message Clip {
string title = 2; string title = 2;
string video_url = 3; string video_url = 3;
string thumbnail_url = 4; string thumbnail_url = 4;
int32 duration = 5; int32 author_id = 5;
int32 author_id = 6; int32 likes_count = 6;
int32 likes_count = 7; int32 comments_count = 7;
int32 comments_count = 8; google.protobuf.Timestamp created_at = 8;
google.protobuf.Timestamp created_at = 9; google.protobuf.Timestamp updated_at = 9;
google.protobuf.Timestamp updated_at = 10;
} }
message ClipLike { message ClipLike {