feat: add compress video logics
This commit is contained in:
285
backend/app/service/commands/compress.go
Normal file
285
backend/app/service/commands/compress.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"quyun/app/service"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/samber/lo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func defaultProviders() container.Providers {
|
||||
return service.Default(container.Providers{}...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("compress"),
|
||||
atom.Short("compress videos"),
|
||||
atom.RunE(Serve),
|
||||
atom.Providers(
|
||||
defaultProviders().With(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dig.In
|
||||
}
|
||||
|
||||
var compressed []compressedDB
|
||||
|
||||
type compressedDB struct {
|
||||
Name string `json:"name"`
|
||||
Md5 string `json:"md5"`
|
||||
}
|
||||
|
||||
func Serve(cmd *cobra.Command, args []string) error {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
if len(args) == 0 {
|
||||
return errors.New("usage: compress <path>")
|
||||
}
|
||||
|
||||
if err := commandCompress(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandCompress(dir string) error {
|
||||
dstPath := filepath.Join(dir, "compressed")
|
||||
// check if the directory exists
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return errors.Wrapf(err, "failed to create directory: %s", dir)
|
||||
}
|
||||
}
|
||||
if err := readDB(dir); err != nil {
|
||||
return errors.Wrap(err, "failed to read db")
|
||||
}
|
||||
defer writeDB(dir)
|
||||
|
||||
dirFiles, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read directory: %s", dir)
|
||||
}
|
||||
// loop through the files
|
||||
for _, file := range dirFiles {
|
||||
// check if the file is a video
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// get the file name
|
||||
fileName := file.Name()
|
||||
filePath := filepath.Join(dir, fileName)
|
||||
dstPath := filepath.Join(dstPath, fileName)
|
||||
|
||||
fileMd5, err := calculateMD5(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := lo.Find(compressed, func(c compressedDB) bool { return c.Md5 == fileMd5 }); ok {
|
||||
log.Infof("file %s already compressed", fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
// get the file path
|
||||
if err := doCompress(filePath, dstPath); err != nil {
|
||||
log.Errorf("failed to compress file: %s", fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
compressed = append(compressed, compressedDB{Name: fileName, Md5: fileMd5})
|
||||
log.Infof("file %s compressed", fileName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDB(path string) error {
|
||||
dbFile := filepath.Join(path, ".compressed.json")
|
||||
// check if the file exists
|
||||
if _, err := os.Stat(dbFile); os.IsNotExist(err) {
|
||||
// create the file
|
||||
file, err := os.Create(dbFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to create file: %s", dbFile)
|
||||
}
|
||||
defer file.Close()
|
||||
// write the file
|
||||
_, err = file.WriteString("[]")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to write file: %s", dbFile)
|
||||
}
|
||||
}
|
||||
|
||||
// read the db
|
||||
file, err := os.ReadFile(dbFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read file: %s", dbFile)
|
||||
}
|
||||
err = json.Unmarshal(file, &compressed)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to unmarshal file: %s", dbFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDB(path string) error {
|
||||
dbFile := filepath.Join(path, ".compressed.json")
|
||||
|
||||
// write the db
|
||||
data, err := json.Marshal(compressed)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal file: %s", dbFile)
|
||||
}
|
||||
err = os.WriteFile(dbFile, data, 0o644)
|
||||
if err != nil {
|
||||
log.Errorf("failed to write file: %s", dbFile)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateMD5 calculates the md5 of a file
|
||||
func calculateMD5(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open file: %s", filePath)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := md5.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", errors.Wrapf(err, "failed to copy file: %s", filePath)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// doCompress compresses the video file
|
||||
func doCompress(srcPath, dstPath string) error {
|
||||
// get file ext
|
||||
ext := filepath.Ext(srcPath)
|
||||
if ext == "" {
|
||||
return errors.New("file has no extension")
|
||||
}
|
||||
|
||||
// check if the file is a .mp3
|
||||
if ext == ".mp3" {
|
||||
return compressAudio(srcPath, dstPath)
|
||||
}
|
||||
|
||||
// check if the file is a video
|
||||
if ext != ".mp4" && ext != ".avi" && ext != ".mkv" {
|
||||
return errors.New("file is not a video")
|
||||
}
|
||||
|
||||
return compressVideo(srcPath, dstPath)
|
||||
}
|
||||
|
||||
// copyFile copies the file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open file: %s", src)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to create file: %s", dst)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return errors.Wrapf(err, "failed to copy file: %s", src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// compress audio
|
||||
func compressAudio(srcPath, dstPath string) error {
|
||||
log.Infof("compress audio from %s to %s", srcPath, dstPath)
|
||||
if err := copyFile(srcPath, dstPath); err != nil {
|
||||
return errors.Wrapf(err, "failed to copy file: %s", srcPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// compress video
|
||||
func compressVideo(srcPath, dstPath string) error {
|
||||
// if file size < 100M then copy
|
||||
fileInfo, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get file info: %s", srcPath)
|
||||
}
|
||||
if fileInfo.Size() < 100*1024*1024 { // 100 MB
|
||||
log.Infof("file size < 100M, copy file from %s to %s", srcPath, dstPath)
|
||||
return copyFile(srcPath, dstPath)
|
||||
}
|
||||
|
||||
width, height, err := getVideoSize(srcPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get video size: %s", srcPath)
|
||||
}
|
||||
log.Infof("video size: %dx%d", width, height)
|
||||
|
||||
cmd := exec.Command("ffmpeg", "-i", srcPath, "-c:v", "libx264", "-crf", "23", "-r", "25", "-c:a", "copy", dstPath)
|
||||
if width > 1920 || height > 1080 {
|
||||
cmd = exec.Command("ffmpeg", "-i", srcPath, "-c:v", "libx264", "-crf", "23", "-r", "25", "-c:a", "copy", "-vf", "scale=1920:1080", dstPath)
|
||||
}
|
||||
log.Infof("executing command: %s", cmd.String())
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrapf(err, "failed to execute compress command: %s", cmd.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVideoSize(filePath string) (int, int, error) {
|
||||
cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=height,width", "-of", "default=noprint_wrappers=1:nokey=1", filePath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrapf(err, "failed to execute command: %s", cmd.String())
|
||||
}
|
||||
lines := strings.Split(string(out), "\n")
|
||||
|
||||
if len(lines) < 2 {
|
||||
return 0, 0, errors.New("failed to get video size")
|
||||
}
|
||||
|
||||
// parse the height and width
|
||||
width, err := strconv.Atoi(strings.TrimSpace(lines[0]))
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(err, "failed to parse height")
|
||||
}
|
||||
|
||||
height, err := strconv.Atoi(strings.TrimSpace(lines[1]))
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(err, "failed to parse width")
|
||||
}
|
||||
return width, height, nil
|
||||
}
|
||||
35
backend/app/service/commands/compress_test.go
Normal file
35
backend/app/service/commands/compress_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rogeecn/fabfile"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_getVideoSize(t *testing.T) {
|
||||
Convey("getVideoSize", t, func() {
|
||||
Convey("test getVideoSize", func() {
|
||||
file := fabfile.MustFind("fixtures/camera.mp4")
|
||||
width, height, err := getVideoSize(file)
|
||||
So(err, ShouldBeNil)
|
||||
So(width, ShouldEqual, 1924)
|
||||
So(height, ShouldEqual, 1080)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_compressVideo(t *testing.T) {
|
||||
Convey("compressVideo", t, func() {
|
||||
Convey("test compressVideo", func() {
|
||||
srcFile := fabfile.MustFind("fixtures/camera.mp4")
|
||||
dstFile := filepath.Join(os.TempDir(), "camera.mp4")
|
||||
t.Log("dstFile", dstFile)
|
||||
|
||||
err := compressVideo(srcFile, dstFile)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user