diff --git a/app.go b/app.go index fc91334..a488c70 100644 --- a/app.go +++ b/app.go @@ -1041,10 +1041,6 @@ func (a *App) IsFFprobeInstalled() (bool, error) { return backend.IsFFprobeInstalled() } -func (a *App) GetFFmpegPath() (string, error) { - return backend.GetFFmpegPath() -} - type DownloadFFmpegRequest struct{} type DownloadFFmpegResponse struct { @@ -1073,6 +1069,41 @@ func (a *App) DownloadFFmpeg() DownloadFFmpegResponse { } } +func (a *App) GetBrewPath() string { + return backend.GetBrewPath() +} + +func (a *App) IsBrewFFmpegInstalled() (bool, error) { + return backend.IsBrewFFmpegInstalled() +} + +type InstallFFmpegWithBrewResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Error string `json:"error,omitempty"` +} + +func (a *App) InstallFFmpegWithBrew() InstallFFmpegWithBrewResponse { + runtime.EventsEmit(a.ctx, "ffmpeg:status", "Installing FFmpeg via Homebrew...") + err := backend.InstallFFmpegWithBrew(func(progress int, status string) { + runtime.EventsEmit(a.ctx, "ffmpeg:progress", progress) + runtime.EventsEmit(a.ctx, "ffmpeg:status", status) + }) + if err != nil { + runtime.EventsEmit(a.ctx, "ffmpeg:status", "failed") + return InstallFFmpegWithBrewResponse{ + Success: false, + Error: err.Error(), + } + } + + runtime.EventsEmit(a.ctx, "ffmpeg:status", "completed") + return InstallFFmpegWithBrewResponse{ + Success: true, + Message: "FFmpeg installed successfully via Homebrew", + } +} + type ConvertAudioRequest struct { InputFiles []string `json:"input_files"` OutputFormat string `json:"output_format"` diff --git a/backend/ffmpeg.go b/backend/ffmpeg.go index e9cdc75..7d824ed 100644 --- a/backend/ffmpeg.go +++ b/backend/ffmpeg.go @@ -81,6 +81,28 @@ func GetFFmpegPath() (string, error) { return localPath, nil } + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + homebrewPath := "/opt/homebrew/bin/" + ffmpegName + if _, err := os.Stat(homebrewPath); err == nil { + return homebrewPath, nil + } + } else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" { + homebrewPath := "/usr/local/bin/" + ffmpegName + if _, err := os.Stat(homebrewPath); err == nil { + return homebrewPath, nil + } + } + + if runtime.GOOS != "windows" { + path, err := exec.Command("which", ffmpegName).Output() + if err == nil { + trimmed := strings.TrimSpace(string(path)) + if trimmed != "" { + return trimmed, nil + } + } + } + path, err := exec.LookPath(ffmpegName) if err == nil { return path, nil @@ -105,6 +127,28 @@ func GetFFprobePath() (string, error) { return localPath, nil } + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + homebrewPath := "/opt/homebrew/bin/" + ffprobeName + if _, err := os.Stat(homebrewPath); err == nil { + return homebrewPath, nil + } + } else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" { + homebrewPath := "/usr/local/bin/" + ffprobeName + if _, err := os.Stat(homebrewPath); err == nil { + return homebrewPath, nil + } + } + + if runtime.GOOS != "windows" { + path, err := exec.Command("which", ffprobeName).Output() + if err == nil { + trimmed := strings.TrimSpace(string(path)) + if trimmed != "" { + return trimmed, nil + } + } + } + path, err := exec.LookPath(ffprobeName) if err == nil { return path, nil @@ -146,6 +190,53 @@ func IsFFmpegInstalled() (bool, error) { return err == nil, nil } +func GetBrewPath() string { + brewPaths := []string{ + "/opt/homebrew/bin/brew", + "/usr/local/bin/brew", + } + + for _, path := range brewPaths { + if _, err := os.Stat(path); err == nil { + return path + } + } + + return "" +} + +func IsBrewFFmpegInstalled() (bool, error) { + brewPath := GetBrewPath() + if brewPath == "" { + return false, nil + } + + cmd := exec.Command(brewPath, "list", "ffmpeg") + setHideWindow(cmd) + err := cmd.Run() + return err == nil, nil +} + +func InstallFFmpegWithBrew(progressCallback func(int, string)) error { + brewPath := GetBrewPath() + if brewPath == "" { + return fmt.Errorf("brew not found") + } + + progressCallback(10, "Installing FFmpeg via Homebrew...") + + cmd := exec.Command(brewPath, "install", "ffmpeg") + setHideWindow(cmd) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to install ffmpeg: %w - %s", err, string(output)) + } + + progressCallback(100, "done") + + return nil +} + const ( ffmpegWindowsURL = "https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffmpeg-windows-amd64.zip" ffmpegLinuxURL = "https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffmpeg-linux-amd64.tar.xz" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ca09792..b43315d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { Search, X, ArrowUp } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; -import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg } from "../wailsjs/go/main/App"; +import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetBrewPath, InstallFFmpegWithBrew } from "../wailsjs/go/main/App"; import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { TitleBar } from "@/components/TitleBar"; @@ -65,6 +65,7 @@ function App() { const downloadQueue = useDownloadQueueDialog(); const downloadProgress = useDownloadProgress(); const [isFFmpegInstalled, setIsFFmpegInstalled] = useState(null); + const [brewPath, setBrewPath] = useState(""); const [isInstallingFFmpeg, setIsInstallingFFmpeg] = useState(false); const [ffmpegInstallProgress, setFfmpegInstallProgress] = useState(0); const [ffmpegInstallStatus, setFfmpegInstallStatus] = useState(""); @@ -92,6 +93,8 @@ function App() { try { const installed = await CheckFFmpegInstalled(); setIsFFmpegInstalled(installed); + const brew = await GetBrewPath(); + setBrewPath(brew); } catch (err) { console.error("Failed to check FFmpeg:", err); @@ -170,7 +173,7 @@ function App() { console.error("Failed to load history:", err); } }; - const handleInstallFFmpeg = async () => { + const handleInstallFFmpeg = async (useBrew: boolean = false) => { setIsInstallingFFmpeg(true); setFfmpegInstallProgress(0); setFfmpegInstallStatus("starting"); @@ -187,11 +190,11 @@ function App() { EventsOn("ffmpeg:status", (status: string) => { setFfmpegInstallStatus(status); }); - const response = await DownloadFFmpeg(); + const response = useBrew ? await InstallFFmpegWithBrew() : await DownloadFFmpeg(); EventsOff("ffmpeg:progress"); EventsOff("ffmpeg:status"); if (response.success) { - toast.success("FFmpeg installed successfully!"); + toast.success(useBrew ? "FFmpeg installed successfully via Homebrew!" : "FFmpeg installed successfully!"); setIsFFmpegInstalled(true); } else { @@ -522,14 +525,23 @@ function App() { { }}> - + FFmpeg Required - FFmpeg is essential for SpotiFLAC to function properly. - This setup will download about 100-200MB of data. + {brewPath ? ( + <> + FFmpeg is essential for SpotiFLAC to function properly. + Homebrew detected. Recommended: brew install ffmpeg + + ) : ( + <> + FFmpeg is essential for SpotiFLAC to function properly. + This setup will download about 100-200MB of data. + + )} @@ -557,13 +569,20 @@ function App() { )} )} - + {!isInstallingFFmpeg && ()} - + {brewPath ? ( + + + ) : ( + + )}