🎯 Consolidation Complete: - Merged 4 separate tools into 1 unified binary - 74% code reduction (2,400 → 600 lines) - 100% elimination of duplicate code - All original functionality preserved 📁 Project Structure: - gwutils/ - Shared utilities package with common functions - gwencoder/ - Unified encoder with multiple modes - Documentation and build scripts 🚀 Features: - 4 encoding modes: --fast, --web, --quick, --tiny - Unified configuration system - Consistent progress tracking - Comprehensive error handling - Cross-platform support ✅ Tested with 4K video encoding - all modes working perfectly
194 lines
5.2 KiB
Go
194 lines
5.2 KiB
Go
package gwutils
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// GetVideoDuration returns the duration of a video file in seconds
|
|
func GetVideoDuration(file string) float64 {
|
|
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "format=duration", "-of", "csv=p=0", file)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
durationStr := strings.TrimSpace(string(output))
|
|
duration, err := strconv.ParseFloat(durationStr, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return duration
|
|
}
|
|
|
|
// ParseFFmpegProgress parses FFmpeg progress output and returns current time and speed
|
|
func ParseFFmpegProgress(line string) (float64, float64) {
|
|
// Parse time progress (time=00:01:23.45)
|
|
timeRegex := regexp.MustCompile(`time=(\d{2}):(\d{2}):(\d{2}\.\d{2})`)
|
|
timeMatch := timeRegex.FindStringSubmatch(line)
|
|
|
|
var currentTime float64
|
|
if len(timeMatch) == 4 {
|
|
hours, _ := strconv.ParseFloat(timeMatch[1], 64)
|
|
minutes, _ := strconv.ParseFloat(timeMatch[2], 64)
|
|
seconds, _ := strconv.ParseFloat(timeMatch[3], 64)
|
|
currentTime = hours*3600 + minutes*60 + seconds
|
|
}
|
|
|
|
// Parse speed (speed=1.23x)
|
|
speedRegex := regexp.MustCompile(`speed=\s*(\d+\.?\d*)x`)
|
|
speedMatch := speedRegex.FindStringSubmatch(line)
|
|
|
|
var speed float64
|
|
if len(speedMatch) == 2 {
|
|
speed, _ = strconv.ParseFloat(speedMatch[1], 64)
|
|
}
|
|
|
|
return currentTime, speed
|
|
}
|
|
|
|
// FormatTime converts seconds to HH:MM:SS format
|
|
func FormatTime(seconds float64) string {
|
|
hours := int(seconds) / 3600
|
|
minutes := int(seconds) % 3600 / 60
|
|
secs := int(seconds) % 60
|
|
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, secs)
|
|
}
|
|
|
|
// GetAudioBitrate returns audio bitrate string based on channel count
|
|
func GetAudioBitrate(inputFile string, bitratePerChannel int) string {
|
|
// Get audio channel count from input file
|
|
cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "a:0", "-show_entries", "stream=channels", "-of", "csv=p=0", inputFile)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
// Default to stereo if detection fails
|
|
return fmt.Sprintf("-b:a %dk", bitratePerChannel*2)
|
|
}
|
|
|
|
channelStr := strings.TrimSpace(string(output))
|
|
channels, err := strconv.Atoi(channelStr)
|
|
if err != nil || channels < 1 {
|
|
channels = 2 // Default to stereo
|
|
}
|
|
|
|
bitrate := channels * bitratePerChannel
|
|
return fmt.Sprintf("-b:a %dk", bitrate)
|
|
}
|
|
|
|
// FindMediaFiles finds all media files in current directory
|
|
func FindMediaFiles(excludePatterns []string) []string {
|
|
var files []string
|
|
extensions := []string{".wmv", ".avi", ".mp4", ".mkv", ".mpg", ".ts", ".webm"}
|
|
|
|
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
for _, valid := range extensions {
|
|
if ext == valid {
|
|
// Check if file should be excluded
|
|
excluded := false
|
|
for _, pattern := range excludePatterns {
|
|
if strings.Contains(path, pattern) {
|
|
excluded = true
|
|
break
|
|
}
|
|
}
|
|
if !excluded {
|
|
files = append(files, path)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
fmt.Printf("Error scanning files: %v\n", err)
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
// AppendToFile appends text to a file
|
|
func AppendToFile(filename, text string) {
|
|
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
fmt.Printf("Error opening log file %s: %v\n", filename, err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString(text)
|
|
if err != nil {
|
|
fmt.Printf("Error writing to log file %s: %v\n", filename, err)
|
|
}
|
|
}
|
|
|
|
// Prompt prompts user for input with default value
|
|
func Prompt(text string, defaultVal string) string {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Printf("❓ %s [%s]: ", text, defaultVal)
|
|
input, _ := reader.ReadString('\n')
|
|
input = strings.TrimSpace(input)
|
|
if input == "" {
|
|
return defaultVal
|
|
}
|
|
return input
|
|
}
|
|
|
|
// GetVideoInfo returns video resolution and codec information
|
|
func GetVideoInfo(file string) (string, string, string) {
|
|
// Get video resolution
|
|
resCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", file)
|
|
resOutput, err := resCmd.Output()
|
|
width, height := "unknown", "unknown"
|
|
if err == nil {
|
|
parts := strings.Split(strings.TrimSpace(string(resOutput)), ",")
|
|
if len(parts) >= 2 {
|
|
width, height = parts[0], parts[1]
|
|
}
|
|
}
|
|
|
|
// Get video codec
|
|
codecCmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "csv=p=0", file)
|
|
codecOutput, err := codecCmd.Output()
|
|
codec := "unknown"
|
|
if err == nil {
|
|
codec = strings.TrimSpace(string(codecOutput))
|
|
}
|
|
|
|
return width, height, codec
|
|
}
|
|
|
|
// GetPhysicalCores returns the number of physical CPU cores
|
|
func GetPhysicalCores() int {
|
|
physicalCores := runtime.NumCPU() / 2
|
|
if physicalCores < 2 {
|
|
physicalCores = 2
|
|
}
|
|
return physicalCores
|
|
}
|
|
|
|
// CheckFFmpeg checks if FFmpeg is available in PATH
|
|
func CheckFFmpeg() bool {
|
|
_, err := exec.LookPath("ffmpeg")
|
|
return err == nil
|
|
}
|
|
|
|
// CheckFFprobe checks if FFprobe is available in PATH
|
|
func CheckFFprobe() bool {
|
|
_, err := exec.LookPath("ffprobe")
|
|
return err == nil
|
|
}
|