Files
quyun/backend/app/service/commands/compress.go
2025-03-29 18:11:48 +08:00

300 lines
7.2 KiB
Go

package commands
import (
"crypto/md5"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"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")
}
log.Infof("start compressing %s", args[0])
if err := commandCompress(args[0]); err != nil {
log.WithError(err).Errorf("compressing %s failed", args[0])
return err
}
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
log.Infof("start compressing %s", args[0])
if err := commandCompress(args[0]); err != nil {
log.WithError(err).Errorf("compressing %s failed", args[0])
continue
}
log.Infof("compressing %s done", args[0])
}
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
}
log.Infof("process file %s", file.Name())
// 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.WithError(err).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
}