1Panel/agent/utils/convert/convert.go

181 lines
4.7 KiB
Go

package convert
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/global"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
type FormatOption struct {
Type string
Codec string
}
var FormatMap = map[string]FormatOption{
// images
"png": {Type: "image", Codec: "png"},
"jpg": {Type: "image", Codec: "mjpeg"},
"jpeg": {Type: "image", Codec: "mjpeg"},
"webp": {Type: "image", Codec: "libwebp"},
"gif": {Type: "image", Codec: "gif"},
"bmp": {Type: "image", Codec: "bmp"},
"tiff": {Type: "image", Codec: "tiff"},
// videos
"mp4": {Type: "video", Codec: "libx264"},
"avi": {Type: "video", Codec: "libx264"},
"mov": {Type: "video", Codec: "libx264"},
"mkv": {Type: "video", Codec: "libx264"},
// audios
"mp3": {Type: "audio", Codec: "libmp3lame"},
"wav": {Type: "audio", Codec: "pcm_s16le"},
"flac": {Type: "audio", Codec: "flac"},
"aac": {Type: "audio", Codec: "aac"},
}
func hasFfmpeg() (string, bool) {
ffmpegPath, err := exec.LookPath("ffmpeg")
return ffmpegPath, err == nil
}
func MediaFile(inputFile, outputFile, outputFormat string, deleteSource bool) (state string, err error) {
status := "FAILED"
msg := ""
ffmpegPath, flag := hasFfmpeg()
if !flag {
return status, fmt.Errorf("ffmpeg not found, cannot convert file")
}
logFile, logErr := os.OpenFile(filepath.Join(global.Dir.ConvertLogDir, "convert.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
allLogFile, allErr := os.OpenFile(filepath.Join(global.Dir.ConvertLogDir, "convert-all.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logErr != nil || allErr != nil {
return status, fmt.Errorf("cannot open log file: %w", err)
}
defer logFile.Close()
args, fileType, err := buildFFmpegArgs(inputFile, outputFile, outputFormat)
if err != nil {
return status, fmt.Errorf("FFmpeg args failed: %w", err)
}
ctx := context.Background()
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
cmdr := exec.CommandContext(ctx, ffmpegPath, args...)
cmdr.Env = append(os.Environ(),
"PATH="+filepath.Dir(ffmpegPath)+":"+os.Getenv("PATH"),
"LD_LIBRARY_PATH=/usr/local/lib:"+os.Getenv("LD_LIBRARY_PATH"),
)
var buf bytes.Buffer
cmdr.Stdout = &buf
cmdr.Stderr = &buf
err = cmdr.Run()
logStr := buf.String()
stat, statErr := os.Stat(outputFile)
if err != nil || statErr != nil || stat.Size() == 0 {
status = "FAILED"
msg = extractFFmpegError(logStr)
_ = os.Remove(outputFile)
} else {
status = "SUCCESS"
msg = "SUCCESS"
}
entry := response.FileConvertLog{
Date: time.Now().Format("2006-01-02 15:04:05"),
Type: fileType,
Log: fmt.Sprintf("%s -> %s", inputFile, outputFile),
Status: status,
Message: msg,
}
_ = appendJSONLog(logFile, entry)
allLogEntry := fmt.Sprintf("[%s] %s %s -> %s [%s]: %s\n",
time.Now().Format("2006-01-02 15:04:05"),
fileType, inputFile, outputFile, status, logStr)
_ = appendLog(allLogFile, allLogEntry)
if err == nil && deleteSource {
_ = os.Remove(inputFile)
}
return status, nil
}
func buildFFmpegArgs(inputFile, outputFile, outputFormat string) ([]string, string, error) {
args := []string{"-y", "-i", inputFile}
opt, ok := FormatMap[outputFormat]
if !ok {
return nil, "", fmt.Errorf("unsupported format: %s", outputFormat)
}
switch opt.Type {
case "image":
switch outputFormat {
case "webp":
args = append(args, "-c:v", "libwebp", "-lossless", "0", "-q:v", "75")
case "png", "gif", "jpg", "jpeg", "bmp", "tiff":
args = append(args, "-c:v", opt.Codec)
}
case "video":
args = append(args, "-c:v", opt.Codec, "-preset", "fast", "-crf", "23", "-c:a", "aac", "-b:a", "192k")
case "audio":
args = append(args, "-c:a", opt.Codec, "-b:a", "192k")
default:
return nil, opt.Type, fmt.Errorf("unsupported media type: %s", opt.Type)
}
args = append(args, outputFile)
return args, opt.Type, nil
}
func appendLog(f *os.File, content string) error {
_, err := f.WriteString(content)
return err
}
func appendJSONLog(f *os.File, entry response.FileConvertLog) error {
data, err := json.Marshal(entry)
if err != nil {
return err
}
if _, err := f.WriteString(string(data) + "\n"); err != nil {
return err
}
return nil
}
func extractFFmpegError(logStr string) string {
priority := []string{"Error", "Invalid", "failed", "No "}
matches := make(map[string]string)
lines := strings.Split(strings.TrimSpace(logStr), "\n")
for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
for _, kw := range priority {
if _, ok := matches[kw]; !ok && strings.Contains(line, kw) {
matches[kw] = line
}
}
}
for _, kw := range priority {
if line, ok := matches[kw]; ok {
return line
}
}
if len(lines) > 0 {
return lines[len(lines)-1]
}
return ""
}