package jobs import ( "bytes" "context" "os" "os/exec" "strings" "time" "backend/app/http/medias" "backend/app/http/posts" "backend/app/http/storages" "backend/database/fields" "backend/database/models/qvyun_v2/public/model" "backend/pkg/utils" "backend/pkg/utils/fs" _ "git.ipao.vip/rogeecn/atom" _ "git.ipao.vip/rogeecn/atom/contracts" "github.com/pkg/errors" . "github.com/riverqueue/river" "github.com/samber/lo" "github.com/sirupsen/logrus" ) var ( _ JobArgs = (*PostVideoCutJob)(nil) _ JobArgsWithInsertOpts = (*PostVideoCutJob)(nil) ) type PostVideoCutJob struct { PostID int64 TenantID int64 UserID int64 Hash string } // InsertOpts implements JobArgsWithInsertOpts. func (s PostVideoCutJob) InsertOpts() InsertOpts { return InsertOpts{ Queue: QueueDefault, Priority: PriorityDefault, UniqueOpts: UniqueOpts{ ByArgs: true, }, } } func (PostVideoCutJob) Kind() string { return "PostVideoCutJob" } // worker var _ Worker[PostVideoCutJob] = (*PostVideoCutJobWorker)(nil) // @provider(job) type PostVideoCutJobWorker struct { WorkerDefaults[PostVideoCutJob] log *logrus.Entry `inject:"false"` postSvc *posts.Service mediaSvc *medias.Service storageSvc *storages.Service } func (w *PostVideoCutJobWorker) Prepare() error { w.log = logrus.WithField("worker", "PostVideoCutJobWorker") return nil } func (w *PostVideoCutJobWorker) NextRetry(job *Job[PostVideoCutJob]) time.Time { return time.Now().Add(5 * time.Second) } func (w *PostVideoCutJobWorker) Work(ctx context.Context, job *Job[PostVideoCutJob]) error { post, err := w.postSvc.GetPostByID(ctx, job.Args.PostID) if err != nil { return errors.Wrapf(err, "get post(%d) failed", job.Args.PostID) } media, err := w.mediaSvc.GetMediaByHash(ctx, job.Args.TenantID, job.Args.UserID, job.Args.Hash) if err != nil { return errors.Wrapf(err, "get media by hash(%s) failed", job.Args.Hash) } videoPath := media.Path // 获取全长度的音频 _, ok := lo.Find(post.Assets.Data, func(asset fields.MediaAsset) bool { return asset.Type == fields.MediaAssetTypeAudio && asset.Mark != nil && *asset.Mark == "audio-preview" }) if ok { return nil } previewVideoPath := strings.Replace(videoPath, ".mp4", "-preview.mp4", -1) duration := lo.ToPtr("00:01:00") if err := w.extractVideoFromVideo(ctx, videoPath, previewVideoPath, duration); err != nil { return errors.Wrapf(err, "extract preview video from video failed") } fileMd5, err := utils.FileMd5(previewVideoPath) if err != nil { return errors.Wrapf(err, "get preview video(%s) file md5 failed", previewVideoPath) } if err := os.Rename(previewVideoPath, strings.Replace(videoPath, job.Args.Hash, fileMd5, 1)); err != nil { return errors.Wrapf(err, "rename video(%s) file failed", videoPath) } storage, err := w.storageSvc.GetDefault(ctx) if err != nil { return err } // save to medias _, err = w.mediaSvc.Create(ctx, &model.Medias{ TenantID: job.Args.TenantID, UserID: job.Args.UserID, PostID: post.ID, StorageID: storage.ID, Hash: fileMd5, Name: post.Title, MimeType: "video/mp4", Size: fs.FileSize(previewVideoPath), Path: previewVideoPath, }) if err != nil { return errors.Wrapf(err, "create media failed") } assets := []fields.MediaAsset{ { Type: fields.MediaAssetTypeVideo, Hash: fileMd5, Mark: lo.ToPtr("video-preview"), }, } if err := w.postSvc.AttachAssets(ctx, job.Args.TenantID, job.Args.UserID, post.ID, assets); err != nil { return errors.Wrapf(err, "attach video(%s) to post(%d) failed", videoPath, post.ID) } post.Meta.WorkerMark = post.Meta.WorkerMark & 1 << 0 if err := w.postSvc.UpdateMeta(ctx, job.Args.TenantID, job.Args.UserID, post.ID, post.Meta); err != nil { return errors.Wrapf(err, "update post(%d) meta failed", post.ID) } return nil } // extractVideoFromVideo func (w *PostVideoCutJobWorker) extractVideoFromVideo(ctx context.Context, videoPath, previewVideoPath string, duration *string) error { args := []string{ "-i", videoPath, "-c:v", "copy", "-c:a", "copy", } if duration != nil { args = append(args, "-t", *duration) } args = append(args, previewVideoPath) w.log.Infof("extractVideoFromVideo: ffmpeg %s", strings.Join(args, " ")) cmd := exec.CommandContext(ctx, "ffmpeg", args...) var buf bytes.Buffer logWriter := utils.NewLogBuffer(func(line string) { w.log.Info(line) }) cmd.Stdout = utils.NewCombinedBuffer(&buf, logWriter) cmd.Stderr = utils.NewCombinedBuffer(&buf, logWriter) if err := cmd.Run(); err != nil { return errors.Wrapf(err, "extract video failed: %s\n%s", videoPath, buf.String()) } return nil }