Compare commits

...

2 Commits

5 changed files with 207 additions and 82 deletions

View File

@ -39,6 +39,11 @@ func formatBytes(b int64) string {
}
}
// FormatBytes exposes human-readable bytes with binary units.
func FormatBytes(b int64) string {
return formatBytes(b)
}
// DeltaBytes renders size plus delta vs reference.
func DeltaBytes(newBytes, refBytes int64) string {
if newBytes <= 0 {
@ -62,7 +67,7 @@ func DeltaBitrate(newBps, refBps int) string {
if newBps <= 0 {
return "--"
}
br := formatBitrate(newBps)
br := formatBitrateHuman(newBps)
if refBps <= 0 || refBps == newBps {
return br
}
@ -83,3 +88,14 @@ func formatPercent(val float64) string {
}
return fmt.Sprintf("%.1f%%", val)
}
func formatBitrateHuman(bps int) string {
if bps <= 0 {
return "--"
}
kbps := float64(bps) / 1000.0
if kbps >= 1000 {
return fmt.Sprintf("%.1f Mbps", kbps/1000.0)
}
return fmt.Sprintf("%.0f kbps", kbps)
}

214
main.go
View File

@ -183,6 +183,25 @@ func defaultBitrate(codec string, width int, sourceBitrate int) string {
}
}
// effectiveHardwareAccel resolves "auto" to a best-effort hardware encoder for the platform.
func effectiveHardwareAccel(cfg convertConfig) string {
accel := strings.ToLower(cfg.HardwareAccel)
if accel != "" && accel != "auto" {
return accel
}
switch runtime.GOOS {
case "windows":
// Prefer NVENC, then Intel (QSV), then AMD (AMF)
return "nvenc"
case "darwin":
return "videotoolbox"
default: // linux and others
// Prefer NVENC, then Intel (QSV), then VAAPI
return "nvenc"
}
}
// openLogViewer opens a simple dialog showing the log content. If live is true, it auto-refreshes.
func (s *appState) openLogViewer(title, path string, live bool) {
if strings.TrimSpace(path) == "" {
@ -319,7 +338,7 @@ type convertConfig struct {
TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom
FrameRate string // Source, 24, 30, 60, or custom
PixelFormat string // yuv420p, yuv422p, yuv444p
HardwareAccel string // none, nvenc, amf, vaapi, qsv, videotoolbox
HardwareAccel string // auto, none, nvenc, amf, vaapi, qsv, videotoolbox
TwoPass bool // Enable two-pass encoding for VBR
H264Profile string // baseline, main, high (for H.264 compatibility)
H264Level string // 3.0, 3.1, 4.0, 4.1, 5.0, 5.1 (for H.264 compatibility)
@ -1334,10 +1353,6 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
cfg := job.Config
inputPath := cfg["inputPath"].(string)
outputPath := cfg["outputPath"].(string)
sourceBitrate := 0
if v, ok := cfg["sourceBitrate"].(float64); ok {
sourceBitrate = int(v)
}
// If a direct conversion is running, wait until it finishes before starting queued jobs.
for s.convertBusy {
@ -1517,7 +1532,7 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
sourceWidth, _ := cfg["sourceWidth"].(int)
sourceHeight, _ := cfg["sourceHeight"].(int)
// Get source bitrate if present
sourceBitrate = 0
sourceBitrate := 0
if v, ok := cfg["sourceBitrate"].(float64); ok {
sourceBitrate = int(v)
}
@ -1617,6 +1632,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
} else if bitrateMode == "CBR" {
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
args = append(args, "-b:v", videoBitrate, "-minrate", videoBitrate, "-maxrate", videoBitrate, "-bufsize", videoBitrate)
} else {
vb := defaultBitrate(videoCodec, sourceWidth, sourceBitrate)
args = append(args, "-b:v", vb, "-minrate", vb, "-maxrate", vb, "-bufsize", vb)
}
} else if bitrateMode == "VBR" {
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
@ -2073,7 +2091,7 @@ func runGUI() {
TargetResolution: "Source",
FrameRate: "Source",
PixelFormat: "yuv420p",
HardwareAccel: "none",
HardwareAccel: "auto",
TwoPass: false,
H264Profile: "main",
H264Level: "4.0",
@ -2746,7 +2764,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
TargetResolution: "Source",
FrameRate: "Source",
PixelFormat: "yuv420p",
HardwareAccel: "none",
HardwareAccel: "auto",
AudioCodec: "AAC",
AudioBitrate: "192k",
AudioChannels: "Source",
@ -2865,6 +2883,30 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
// Simple bitrate selector (shares presets)
simpleBitrateSelect := widget.NewSelect(bitratePresetLabels, func(value string) {
state.convert.BitratePreset = value
if applyBitratePreset != nil {
applyBitratePreset(value)
}
})
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
// Simple resolution selector (separate widget to avoid double-parent issues)
resolutionSelectSimple := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
state.convert.TargetResolution = value
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
})
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
// Simple aspect selector (separate widget)
targetAspectSelectSimple := widget.NewSelect(aspectTargets, func(value string) {
logging.Debug(logging.CatUI, "target aspect set to %s (simple)", value)
state.convert.OutputAspect = value
updateAspectBoxVisibility()
})
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
// Target File Size with smart presets + manual entry
targetFileSizeEntry = widget.NewEntry()
targetFileSizeEntry.SetPlaceHolder("e.g., 25MB, 100MB, 8MB")
@ -3028,11 +3070,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
updateEncodingControls()
// Target Resolution
// Target Resolution (advanced)
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
state.convert.TargetResolution = value
logging.Debug(logging.CatUI, "target resolution set to %s", value)
})
if state.convert.TargetResolution == "" {
state.convert.TargetResolution = "Source"
}
resolutionSelect.SetSelected(state.convert.TargetResolution)
// Frame Rate with hint
@ -3110,11 +3155,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
})
pixelFormatSelect.SetSelected(state.convert.PixelFormat)
// Hardware Acceleration
hwAccelSelect := widget.NewSelect([]string{"none", "nvenc", "amf", "vaapi", "qsv", "videotoolbox"}, func(value string) {
// Hardware Acceleration with hint
hwAccelHint := widget.NewLabel("Auto picks the best GPU path; if encode fails, switch to none (software).")
hwAccelHint.Wrapping = fyne.TextWrapWord
hwAccelSelect := widget.NewSelect([]string{"auto", "none", "nvenc", "amf", "vaapi", "qsv", "videotoolbox"}, func(value string) {
state.convert.HardwareAccel = value
logging.Debug(logging.CatUI, "hardware accel set to %s", value)
})
if state.convert.HardwareAccel == "" {
state.convert.HardwareAccel = "auto"
}
hwAccelSelect.SetSelected(state.convert.HardwareAccel)
// Two-Pass encoding
@ -3217,7 +3267,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
widget.NewLabel("Choose slower for better compression, faster for speed"),
widget.NewLabelWithStyle("Encoder Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
simplePresetSelect,
widget.NewLabel("Aspect ratio will match source video"),
widget.NewSeparator(),
widget.NewLabelWithStyle("Bitrate (simple presets)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
simpleBitrateSelect,
widget.NewLabelWithStyle("Target Resolution", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
resolutionSelectSimple,
widget.NewLabelWithStyle("Target Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
targetAspectSelectSimple,
targetAspectHint,
layout.NewSpacer(),
)
@ -3261,6 +3318,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
pixelFormatSelect,
widget.NewLabelWithStyle("Hardware Acceleration", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
hwAccelSelect,
hwAccelHint,
twoPassCheck,
widget.NewSeparator(),
@ -3328,23 +3386,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
tabs.OnSelected = func(item *container.TabItem) {
if item.Text == "Simple" {
state.convert.Mode = "Simple"
// Lock aspect ratio to Source in Simple mode
state.convert.OutputAspect = "Source"
targetAspectSelect.SetSelected("Source")
updateAspectBoxVisibility()
logging.Debug(logging.CatUI, "convert mode selected: Simple (aspect locked to Source)")
logging.Debug(logging.CatUI, "convert mode selected: Simple")
} else {
state.convert.Mode = "Advanced"
logging.Debug(logging.CatUI, "convert mode selected: Advanced")
}
}
// Ensure Simple mode starts with Source aspect
if state.convert.Mode == "Simple" {
state.convert.OutputAspect = "Source"
targetAspectSelect.SetSelected("Source")
}
optionsRect := canvas.NewRectangle(utils.MustHex("#13182B"))
optionsRect.CornerRadius = 8
optionsRect.StrokeColor = gridColor
@ -5191,27 +5239,28 @@ func detectBestH265Encoder() string {
// determineVideoCodec maps user-friendly codec names to FFmpeg codec names
func determineVideoCodec(cfg convertConfig) string {
accel := effectiveHardwareAccel(cfg)
switch cfg.VideoCodec {
case "H.264":
if cfg.HardwareAccel == "nvenc" {
if accel == "nvenc" {
return "h264_nvenc"
} else if cfg.HardwareAccel == "amf" {
} else if accel == "amf" {
return "h264_amf"
} else if cfg.HardwareAccel == "qsv" {
} else if accel == "qsv" {
return "h264_qsv"
} else if cfg.HardwareAccel == "videotoolbox" {
} else if accel == "videotoolbox" {
return "h264_videotoolbox"
}
// When set to "none" or empty, use software encoder
return "libx264"
case "H.265":
if cfg.HardwareAccel == "nvenc" {
if accel == "nvenc" {
return "hevc_nvenc"
} else if cfg.HardwareAccel == "amf" {
} else if accel == "amf" {
return "hevc_amf"
} else if cfg.HardwareAccel == "qsv" {
} else if accel == "qsv" {
return "hevc_qsv"
} else if cfg.HardwareAccel == "videotoolbox" {
} else if accel == "videotoolbox" {
return "hevc_videotoolbox"
}
// When set to "none" or empty, use software encoder
@ -5219,13 +5268,13 @@ func determineVideoCodec(cfg convertConfig) string {
case "VP9":
return "libvpx-vp9"
case "AV1":
if cfg.HardwareAccel == "amf" {
if accel == "amf" {
return "av1_amf"
} else if cfg.HardwareAccel == "nvenc" {
} else if accel == "nvenc" {
return "av1_nvenc"
} else if cfg.HardwareAccel == "qsv" {
} else if accel == "qsv" {
return "av1_qsv"
} else if cfg.HardwareAccel == "vaapi" {
} else if accel == "vaapi" {
return "av1_vaapi"
}
// When set to "none" or empty, use software encoder
@ -5307,6 +5356,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
}
src := s.source
cfg := s.convert
sourceBitrate := src.Bitrate
isDVD := cfg.SelectedFormat.Ext == ".mpg"
outDir := filepath.Dir(src.Path)
outName := cfg.OutputFile()
@ -5358,16 +5408,13 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
args = append(args, "-i", cfg.CoverArtPath)
}
// Hardware acceleration for decoding
// Note: NVENC and AMF don't need -hwaccel for encoding, only for decoding
if cfg.HardwareAccel != "none" && cfg.HardwareAccel != "" {
switch cfg.HardwareAccel {
// Hardware acceleration for decoding (best-effort)
if accel := effectiveHardwareAccel(cfg); accel != "none" && accel != "" {
switch accel {
case "nvenc":
// For NVENC, we don't add -hwaccel flags
// The h264_nvenc/hevc_nvenc encoder handles GPU encoding directly
// NVENC encoders handle GPU directly; no hwaccel flag needed
case "amf":
// For AMD AMF, we don't add -hwaccel flags
// The h264_amf/hevc_amf/av1_amf encoders handle GPU encoding directly
// AMF encoders handle GPU directly
case "vaapi":
args = append(args, "-hwaccel", "vaapi")
case "qsv":
@ -5375,7 +5422,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
case "videotoolbox":
args = append(args, "-hwaccel", "videotoolbox")
}
logging.Debug(logging.CatFFMPEG, "hardware acceleration: %s", cfg.HardwareAccel)
logging.Debug(logging.CatFFMPEG, "hardware acceleration: %s", accel)
}
// Video filters.
@ -6699,30 +6746,27 @@ func buildCompareView(state *appState) fyne.CanvasObject {
// File Info section
comparisonText.WriteString("━━━ FILE INFO ━━━\n")
var file1SizeBytes int64
file1Size := getField(state.compareFile1, func(src *videoSource) string {
if fi, err := os.Stat(src.Path); err == nil {
sizeMB := float64(fi.Size()) / (1024 * 1024)
if sizeMB >= 1024 {
return fmt.Sprintf("%.2f GB", sizeMB/1024)
}
return fmt.Sprintf("%.2f MB", sizeMB)
file1SizeBytes = fi.Size()
return utils.FormatBytes(fi.Size())
}
return "Unknown"
})
file2Size := getField(state.compareFile2, func(src *videoSource) string {
if fi, err := os.Stat(src.Path); err == nil {
sizeMB := float64(fi.Size()) / (1024 * 1024)
if sizeMB >= 1024 {
return fmt.Sprintf("%.2f GB", sizeMB/1024)
if file1SizeBytes > 0 {
return utils.DeltaBytes(fi.Size(), file1SizeBytes)
}
return fmt.Sprintf("%.2f MB", sizeMB)
return utils.FormatBytes(fi.Size())
}
return "Unknown"
})
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n", "File Size:", file1Size, file2Size))
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
"Format:",
"Format Family:",
getField(state.compareFile1, func(s *videoSource) string { return s.Format }),
getField(state.compareFile2, func(s *videoSource) string { return s.Format })))
@ -6747,7 +6791,12 @@ func buildCompareView(state *appState) fyne.CanvasObject {
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
"Bitrate:",
getField(state.compareFile1, func(s *videoSource) string { return formatBitrate(s.Bitrate) }),
getField(state.compareFile2, func(s *videoSource) string { return formatBitrate(s.Bitrate) })))
getField(state.compareFile2, func(s *videoSource) string {
if state.compareFile1 != nil {
return utils.DeltaBitrate(s.Bitrate, state.compareFile1.Bitrate)
}
return formatBitrate(s.Bitrate)
})))
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
"Pixel Format:",
getField(state.compareFile1, func(s *videoSource) string { return s.PixelFormat }),
@ -6848,22 +6897,38 @@ func buildCompareView(state *appState) fyne.CanvasObject {
file2Info.Wrapping = fyne.TextWrapWord
file2Info.TextStyle = fyne.TextStyle{} // non-selectable label
// Helper function to format metadata
formatMetadata := func(src *videoSource) string {
fileSize := "Unknown"
// Helper function to format metadata (optionally comparing to a reference video)
formatMetadata := func(src *videoSource, ref *videoSource) string {
var (
fileSize = "Unknown"
refSize int64 = 0
)
if fi, err := os.Stat(src.Path); err == nil {
sizeMB := float64(fi.Size()) / (1024 * 1024)
if sizeMB >= 1024 {
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
if ref != nil {
if rfi, err := os.Stat(ref.Path); err == nil {
refSize = rfi.Size()
}
}
if refSize > 0 {
fileSize = utils.DeltaBytes(fi.Size(), refSize)
} else {
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
fileSize = utils.FormatBytes(fi.Size())
}
}
var bitrateStr string
if src.Bitrate > 0 {
bitrateStr = formatBitrate(src.Bitrate)
} else {
var (
bitrateStr = "--"
refBitrate = 0
)
if ref != nil {
refBitrate = ref.Bitrate
}
if src.Bitrate > 0 {
if refBitrate > 0 {
bitrateStr = utils.DeltaBitrate(src.Bitrate, refBitrate)
} else {
bitrateStr = formatBitrate(src.Bitrate)
}
}
return fmt.Sprintf(
@ -6944,7 +7009,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
filename := filepath.Base(state.compareFile1.Path)
displayName := truncateFilename(filename, 35)
file1Label.SetText(fmt.Sprintf("File 1: %s", displayName))
file1Info.SetText(formatMetadata(state.compareFile1))
file1Info.SetText(formatMetadata(state.compareFile1, state.compareFile2))
// Build video player with compact size for side-by-side
file1VideoContainer.Objects = []fyne.CanvasObject{
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile1, nil),
@ -6965,7 +7030,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
filename := filepath.Base(state.compareFile2.Path)
displayName := truncateFilename(filename, 35)
file2Label.SetText(fmt.Sprintf("File 2: %s", displayName))
file2Info.SetText(formatMetadata(state.compareFile2))
file2Info.SetText(formatMetadata(state.compareFile2, state.compareFile1))
// Build video player with compact size for side-by-side
file2VideoContainer.Objects = []fyne.CanvasObject{
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile2, nil),
@ -7030,7 +7095,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
if state.compareFile1 == nil {
return
}
metadata := formatMetadata(state.compareFile1)
metadata := formatMetadata(state.compareFile1, state.compareFile2)
state.window.Clipboard().SetContent(metadata)
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
})
@ -7047,7 +7112,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
if state.compareFile2 == nil {
return
}
metadata := formatMetadata(state.compareFile2)
metadata := formatMetadata(state.compareFile2, state.compareFile1)
state.window.Clipboard().SetContent(metadata)
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
})
@ -7159,19 +7224,14 @@ func buildInspectView(state *appState) fyne.CanvasObject {
formatMetadata := func(src *videoSource) string {
fileSize := "Unknown"
if fi, err := os.Stat(src.Path); err == nil {
sizeMB := float64(fi.Size()) / (1024 * 1024)
if sizeMB >= 1024 {
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
} else {
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
}
fileSize = utils.FormatBytes(fi.Size())
}
return fmt.Sprintf(
"━━━ FILE INFO ━━━\n"+
"Path: %s\n"+
"File Size: %s\n"+
"Format: %s\n"+
"Format Family: %s\n"+
"\n━━━ VIDEO ━━━\n"+
"Codec: %s\n"+
"Resolution: %dx%d\n"+

View File

@ -6,8 +6,8 @@ set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_OUTPUT="$PROJECT_ROOT/VideoTools"
# Extract app version from main.go
APP_VERSION="$(grep -E 'appVersion\s*=\s*\"' "$PROJECT_ROOT/main.go" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
# Extract app version from main.go (avoid grep warnings on Git Bash)
APP_VERSION="$(grep -m1 'appVersion' "$PROJECT_ROOT/main.go" | sed -E 's/.*\"([^\"]+)\".*/\1/')"
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
echo "════════════════════════════════════════════════════════════════"
@ -64,5 +64,10 @@ if go build -o "$BUILD_OUTPUT" .; then
else
echo "❌ Build failed! (VideoTools $APP_VERSION)"
echo "Diagnostics: version=$APP_VERSION os=$(uname -s) arch=$(uname -m) go=$(go version | awk '{print $3}')"
echo ""
echo "Help: check the Go error messages above."
echo " - Undefined symbol/identifier: usually a missing variable or typo in source; see the referenced file:line."
echo " - \"C compiler not found\": install a C toolchain (e.g., build-essential on Ubuntu, Xcode CLT on macOS)."
echo " - Cache permission denied: run 'rm -rf ~/.cache/go-build' or 'chown -R $USER ~/.cache/go-build'."
exit 1
fi

View File

@ -5,8 +5,8 @@ set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Extract app version from main.go
APP_VERSION="$(grep -E 'appVersion[[:space:]]*=[[:space:]]*\"' "$PROJECT_ROOT/main.go" | head -1 | sed -E 's/.*\"([^\"]+)\".*/\1/')"
# Extract app version from main.go (avoid grep warnings on Git Bash)
APP_VERSION="$(grep -m1 'appVersion' "$PROJECT_ROOT/main.go" | sed -E 's/.*\"([^\"]+)\".*/\1/')"
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
echo "════════════════════════════════════════════════════════════════"
@ -97,6 +97,11 @@ case "$OS" in
else
echo "❌ Build failed! (VideoTools $APP_VERSION)"
diagnostics
echo ""
echo "Help: check the Go error messages above."
echo " - Undefined symbol/identifier: usually a missing variable or typo in source; see the referenced file:line."
echo " - \"C compiler not found\": install MinGW-w64 or MSYS2 toolchain so gcc is in PATH."
echo " - Cache permission denied: delete or chown the Go build cache (e.g., %LOCALAPPDATA%\\go-build on Windows)."
exit 1
fi
;;

39
scripts/clear-go-cache.sh Normal file
View File

@ -0,0 +1,39 @@
#!/bin/bash
# Convenience script to clear Go build/module caches.
# Safe to run on Linux/macOS and Windows Git Bash.
set -e
echo "════════════════════════════════════════════════════════════════"
echo " VideoTools Go Cache Cleaner"
echo "════════════════════════════════════════════════════════════════"
echo ""
if ! command -v go >/dev/null 2>&1; then
echo "⚠️ Go is not installed or not in PATH; skipping go clean."
else
echo "🧹 Running: go clean -cache -modcache -testcache"
go clean -cache -modcache -testcache || true
echo "✓ Go clean complete"
fi
OS="$(uname -s)"
case "$OS" in
CYGWIN*|MINGW*|MSYS*)
# Windows paths under Git Bash
CACHE_DIR="${LOCALAPPDATA:-$APPDATA}/go-build"
;;
*)
CACHE_DIR="${GOCACHE:-$HOME/.cache/go-build}"
;;
esac
if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR" ]; then
echo "🗑️ Removing build cache dir: $CACHE_DIR"
rm -rf "$CACHE_DIR" || sudo rm -rf "$CACHE_DIR" || true
else
echo " No cache directory found at $CACHE_DIR (nothing to remove)."
fi
echo ""
echo "✅ Done. Re-run ./scripts/build.sh to rebuild VideoTools."