v.0.0.2 Убран duration из клипов
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
10466568d4
commit
7bfc7eb9a8
@ -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"`
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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" +
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user