From 02fd1dc817bce92ca372ccca1088ec2ab545ab9c Mon Sep 17 00:00:00 2001 From: m4xx1m Date: Thu, 12 Nov 2026 17:18:06 +0300 Subject: [PATCH] feat: initial CLI converter (v1.2) --- .vscode/launch.json | 19 +++++ ffprobe.go | 36 +++++++++ go.mod | 3 + install.ps1 | 39 +++++++++ main.go | 187 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 284 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 ffprobe.go create mode 100644 go.mod create mode 100644 install.ps1 create mode 100644 main.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..33d3e73 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "debug", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go", + "cwd": "${cwd}", + "console": "integratedTerminal", + // "args": ["--debug", "-i", "C:\\Users\\m4xx1m\\Videos\\NVIDIA\\Desktop\\Desktop 2025.07.09 - 13.10.33.02.mp4"] + "args": ["-i", "E:\\records\\Derail Valley\\Derail Valley 2025.08.14 - 02.18.46.02.mp4"] + }, + ] +} \ No newline at end of file diff --git a/ffprobe.go b/ffprobe.go new file mode 100644 index 0000000..1a5ce0c --- /dev/null +++ b/ffprobe.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "os/exec" +) + +type ffprobeOutput struct { + Streams []struct { + CodecType string `json:"codec_type"` + } `json:"streams"` +} + +func ffprobe(inputFile string) (*ffprobeOutput, error) { + var err error + var out ffprobeOutput + + args := []string{ + "-v", "quiet", + "-print_format", "json", + "-show_streams", + inputFile, + } + proc := exec.Command("ffprobe", args...) + stdout, err := proc.Output() + if err != nil { + return nil, err + } + // fmt.Println("read", string(stdout)) + + err = json.Unmarshal(stdout, &out) + if err != nil { + return nil, err + } + return &out, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..afccd6a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module convert + +go 1.17 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..3e48c51 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,39 @@ +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 + +function pressEnterToExit { + Write-Host "Press Enter to exit... " -NoNewline + [void][System.Console]::ReadLine() +} + +$installPath = Join-Path $HOME "programs\convert" +New-Item -Force -ItemType Directory $installPath | Out-Null + +$executablePath = Join-Path $installPath convert.exe +go build -o $executablePath + +if ($LASTEXITCODE -eq 0) { + Write-Host "✅ built: $executablePath" +} else { + Write-Host "❌ build error" + pressEnterToExit + exit $LASTEXITCODE +} + +reg add "HKCU\Software\Classes\SystemFileAssociations\.mp4\shell\convert" /ve /d "convert" /f > $null +if ($LASTEXITCODE -ne 0) { + Write-Host "❌ reg error" + pressEnterToExit + exit $LASTEXITCODE +} + +$command = $executablePath + " -i `"%1`"" +reg add "HKCU\Software\Classes\SystemFileAssociations\.mp4\shell\convert\command" /ve /d $command /f > $null +if ($LASTEXITCODE -ne 0) { + Write-Host "❌ reg error" + pressEnterToExit + exit $LASTEXITCODE +} + +Write-Host "✅ reg values added" +pressEnterToExit diff --git a/main.go b/main.go new file mode 100644 index 0000000..c2f970e --- /dev/null +++ b/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +const DEFAULT_PRESET = "slow" +const DEFAULT_CRF = "20" + +func scanArgFromConsole(text string, v *string) error { + fmt.Print(text) + if _, err := fmt.Scanln(v); err != nil && err.Error() == "EOF" { + return err + } + return nil +} + +func pressEnterToExit(code int) { + fmt.Print("\npress enter to exit... ") + _, _ = fmt.Scanln() + os.Exit(code) +} + +func main() { + var err error + var debug bool + var vfArgs []string + var audioFilters []string + var videoFilters []string + var inputVideo, ss, to, an, crf, preset, mergeAudioStreams, speedUp string + + ffmpegExecutable := "ffmpeg" + ffmpegArgs := append(make([]string, 0, 30), "-hide_banner") + inputArgs := []string{ + "-hwaccel", "nvdec", + "-threads", "1", + } + postArgs := []string{ + "-c:v", "h264", + "-profile:v", "high", + "-pix_fmt", "yuv420p", + "-tune", "film", + "-movflags", "+faststart", + "-r", "60", + } + + flag.StringVar(&inputVideo, "i", "", "input video path") + flag.BoolVar(&debug, "debug", false, "debug mode (default params)") + flag.Parse() + + // check video exists + if _, err := os.Stat(inputVideo); err != nil { + fmt.Println(err) + pressEnterToExit(1) + } + + stat, err := ffprobe(inputVideo) + if err != nil { + fmt.Println(err) + return + } + + if !debug { + err = func() error { + if err = scanArgFromConsole("ss > ", &ss); err != nil { + return err + } + if err = scanArgFromConsole("to > ", &to); err != nil { + return err + } + if err = scanArgFromConsole("an > ", &an); err != nil { + return err + } + if err = scanArgFromConsole(fmt.Sprintf("crf [%v] > ", DEFAULT_CRF), &crf); err != nil { + return err + } + if err = scanArgFromConsole("speed up > ", &speedUp); err != nil { + return err + } + if err = scanArgFromConsole(fmt.Sprintf("preset [%v] > ", DEFAULT_PRESET), &preset); err != nil { + return err + } + if err = scanArgFromConsole("merge audio streams > ", &mergeAudioStreams); err != nil { + return err + } + return nil + }() + if err != nil { + os.Exit(0) + } + } + + for _, v := range []*string{&ss, &to, &an, &crf, &preset, &mergeAudioStreams, &speedUp} { + *v = strings.ToLower(strings.TrimSpace(*v)) + } + + if len(ss) != 0 { + inputArgs = append(inputArgs, "-ss", ss) + } + + if len(to) != 0 { + inputArgs = append(inputArgs, "-to", to) + } + + if len(an) != 0 { + postArgs = append(postArgs, "-an") + } else { + audioFilters = append(audioFilters, "amix=inputs=2:normalize=0") + } + + if len(crf) == 0 { + crf = DEFAULT_CRF + } + + if len(preset) == 0 { + preset = DEFAULT_PRESET + } + + postArgs = append(postArgs, "-preset", preset) + postArgs = append(postArgs, "-crf", crf) + + // speed up video and audio(?) + if len(speedUp) != 0 { + videoFilters = append(videoFilters, fmt.Sprintf("setpts=1/%s*PTS", speedUp)) + if len(an) == 0 { + audioFilters = append(audioFilters, fmt.Sprintf("atempo=%s", speedUp)) + } + } + + if len(videoFilters) != 0 { + vfArgs = append(vfArgs, "[0:v:0]"+strings.Join(videoFilters, ",")+"[v]") + postArgs = append(postArgs, "-map", "[v]") + } else { + postArgs = append(postArgs, "-map", "0:v:0") + } + + if len(audioFilters) != 0 { + vfArgs = append(vfArgs, "[0:a]"+strings.Join(audioFilters, ",")+"[a]") + postArgs = append(postArgs, "-map", "[a]") + } else { + postArgs = append(postArgs, "-map", "0:a") + } + + // merge filter complex args + if len(vfArgs) != 0 { + postArgs = append(postArgs, "-filter_complex", strings.Join(vfArgs, ";")) + } + + // out file name + outDir, _ := filepath.Split(inputVideo) + outFile := filepath.Join(outDir, strconv.FormatInt(time.Now().Unix(), 10)+".mp4") + + // merge args to one slice + ffmpegArgs = append(ffmpegArgs, inputArgs...) + ffmpegArgs = append(ffmpegArgs, "-i", inputVideo) + ffmpegArgs = append(ffmpegArgs, postArgs...) + ffmpegArgs = append(ffmpegArgs, outFile) + + fmt.Printf(" +running %v: %v + +", ffmpegExecutable, strings.Join(ffmpegArgs, " ")) + + proc := exec.Command(ffmpegExecutable, ffmpegArgs...) + proc.Stderr = os.Stderr + err = proc.Start() + if err != nil { + fmt.Println(err) + pressEnterToExit(1) + } + err = proc.Wait() + if err != nil { + fmt.Println(err) + pressEnterToExit(1) + } + if proc.ProcessState.ExitCode() != 0 { + fmt.Println(err) + pressEnterToExit(1) + } +}