package util import ( "bytes" "fmt" "io" "math" "os" "os/exec" "path/filepath" "strconv" "strings" "github.com/redmask-hb/GoSimplePrint/goPrint" uuid "github.com/satori/go.uuid" ) var supportVideoTypes = []string{ ".ts", ".flv", ".rm", ".avi", ".mp4", ".mov", ".mpg", ".mkv", ".m4v", ".rmvb", ".3gp", ".3g2", ".webm", ".wmv", } func getVideoDuration(ffmpegExecPath string, videoPath string) (float64, error) { // ffprobe命令,-v error 用于减少输出信息,-show_entries format=duration -of compact=p=0,nk=1 用于只输出时长 cmd := exec.Command(ffmpegExecPath, "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath) var stderr bytes.Buffer cmd.Stderr = &stderr output, err := cmd.Output() if err != nil { return 0, fmt.Errorf("ffprobe failed with error: %v, stderr: %q", err, stderr.String()) } // 解析输出的时长字符串为浮点数 durationStr := strings.TrimSpace(string(output)) duration, err := strconv.ParseFloat(durationStr, 64) if err != nil { return 0, fmt.Errorf("failed to parse duration: %q, error: %v", durationStr, err) } return duration, nil } func getVideoResolution(ffmpegExecPath string, filePath string) (width int, height int, err error) { // 构建ffprobe命令 cmd := exec.Command(ffmpegExecPath, "-v", "error", "-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", filePath) // 执行命令并捕获输出 output, err := cmd.Output() if err != nil { return 0, 0, fmt.Errorf("failed to run ffprobe: %w", err) } // 解析输出字符串,格式应为 "宽度,高度" resolutionStr := strings.TrimSpace(string(output)) parts := strings.Split(resolutionStr, ",") if len(parts) == 2 { width = String2int(parts[0]) height = String2int(parts[1]) return width, height, nil } parts = strings.Split(resolutionStr, "x") if len(parts) == 2 { width = String2int(parts[0]) height = String2int(parts[1]) return width, height, nil } parts = strings.Split(resolutionStr, "\r\n\r\n\r\n\r\n") if len(parts) == 2 { tempHw := parts[0] parts = strings.Split(tempHw, "x") if len(parts) == 2 { width = String2int(parts[0]) height = String2int(parts[1]) return width, height, nil } } parts = strings.Split(resolutionStr, "x") if len(parts) == 3 { width = String2int(parts[0]) height = String2int(parts[1]) return width, height, nil } resolutionStr = strings.ReplaceAll(resolutionStr, "\r", "") resolutionStr = strings.ReplaceAll(resolutionStr, "\n", "") resolutionStr = strings.ReplaceAll(resolutionStr, "N/AxN/A", "") parts = strings.Split(resolutionStr, "x") if len(parts) == 2 { width = String2int(parts[0]) height = String2int(parts[1]) return width, height, nil } return 0, 0, fmt.Errorf("invalid resolution format: %s", resolutionStr) } // ReadVideoDuration 获取视频的时长,单位秒 func ReadVideoDuration(videoFilePath string) int { duration, err := getVideoDuration("./ffprobe.exe", videoFilePath) if err != nil { fmt.Println("=== Error getting video duration:", err) return 0 } //fmt.Printf("=== Video duration: %.2f seconds\n", duration) return int(math.Floor(duration)) // 向下取整 } // ReadVideoWidthHeight 获取视频的分辨率 func ReadVideoWidthHeight(videoFilePath string) (int, int, error) { width, height, err := getVideoResolution("./ffprobe.exe", videoFilePath) if err != nil { fmt.Printf("=== Error getting resolution: %v\n", err) return 0, 0, err } //fmt.Printf("=== Video resolution: %dx%d\n", width, height) return width, height, nil } // IsSupportVideo 判断是否属于支持的视频 func IsSupportVideo(videoType string) bool { for _, supportVideoType := range supportVideoTypes { if strings.EqualFold(videoType, supportVideoType) { return true } } return false } // DoMoveFileToDir 批量移动文件到目录 func DoMoveFileToDir(filePathList []string, videoDirPath string) { total := len(filePathList) var count = 0 bar := goPrint.NewBar(100) bar.SetNotice("=== 移动文件到目录:") bar.SetGraph(">") pathSeparator := string(os.PathSeparator) for _, videoFilePath := range filePathList { moveFileToDir(videoFilePath, videoDirPath+pathSeparator) count = count + 1 bar.PrintBar(CalcPercentage(count, total)) } bar.PrintEnd("=== Finish") } // 移动文件到目录,跨文件系统时自动 fallback 为 copy+delete,目标文件名使用 uuid+原文件名防止同名覆盖 func moveFileToDir(sourceFilePath string, targetDirectory string) bool { fileName := strings.ReplaceAll(uuid.NewV4().String(), "-", "") + "_" + filepath.Base(sourceFilePath) targetFilePath := targetDirectory + fileName err := os.Rename(sourceFilePath, targetFilePath) if err == nil { return true } // 跨文件系统等场景:fallback copy + delete if err := copyFile(sourceFilePath, targetFilePath); err != nil { fmt.Printf("=== 复制文件失败, 源: %s, 目标: %s, 错误: %v\n", sourceFilePath, targetFilePath, err) return false } if err := os.Remove(sourceFilePath); err != nil { fmt.Printf("=== 删除源文件失败: %s, 错误: %v\n", sourceFilePath, err) // 复制成功但删除失败,文件已存在于两处,返回 true 表示目标已就位 } return true } // copyFile 逐块复制文件 func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } return out.Close() }