support for homebrew ffmpeg on macos (#649)
* support for homebrew ffmpeg on macos * Add build instructions * Revise README * Detect homebrew and install ffmpeg with homebrew --------- Co-authored-by: afkarxyz <mzamzamafkarhadiq@gmail.com>
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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"
|
||||
|
||||
+30
-11
@@ -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<boolean | null>(null);
|
||||
const [brewPath, setBrewPath] = useState<string>("");
|
||||
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() {
|
||||
|
||||
|
||||
<Dialog open={isFFmpegInstalled === false} onOpenChange={() => { }}>
|
||||
<DialogContent className="max-w-[360px] [&>button]:hidden p-6 gap-5">
|
||||
<DialogContent className="max-w-[450px] [&>button]:hidden p-6 gap-5">
|
||||
<DialogHeader className="space-y-2">
|
||||
<DialogTitle className="text-lg font-bold tracking-tight">
|
||||
FFmpeg Required
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm text-foreground/70 leading-relaxed font-normal">
|
||||
FFmpeg is essential for SpotiFLAC to function properly.
|
||||
This setup will download about <span className="text-foreground font-semibold">100-200MB</span> of data.
|
||||
{brewPath ? (
|
||||
<>
|
||||
FFmpeg is essential for SpotiFLAC to function properly.
|
||||
Homebrew detected. Recommended: <span className="text-foreground font-semibold">brew install ffmpeg</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
FFmpeg is essential for SpotiFLAC to function properly.
|
||||
This setup will download about <span className="text-foreground font-semibold">100-200MB</span> of data.
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -557,13 +569,20 @@ function App() {
|
||||
</div>)}
|
||||
</div>)}
|
||||
|
||||
<DialogFooter className="flex-row gap-3 pt-2">
|
||||
<DialogFooter className={`flex-row gap-3 pt-2 ${brewPath ? 'flex-col' : ''}`}>
|
||||
{!isInstallingFFmpeg && (<Button variant="outline" className="flex-1 h-11 text-sm font-bold transition-colors" onClick={() => Quit()}>
|
||||
Exit
|
||||
</Button>)}
|
||||
<Button className={`${isInstallingFFmpeg ? 'w-full' : 'flex-1'} h-11 text-sm font-bold shadow-lg shadow-primary/10`} onClick={handleInstallFFmpeg} disabled={isInstallingFFmpeg}>
|
||||
{isInstallingFFmpeg ? "Installing..." : "Install now"}
|
||||
</Button>
|
||||
{brewPath ? (
|
||||
<Button className="flex-1 h-11 text-sm font-bold shadow-lg shadow-primary/10" onClick={() => handleInstallFFmpeg(true)} disabled={isInstallingFFmpeg}>
|
||||
{isInstallingFFmpeg ? "Installing..." : "Install via Homebrew"}
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<Button className={`${isInstallingFFmpeg ? 'w-full' : 'flex-1'} h-11 text-sm font-bold shadow-lg shadow-primary/10`} onClick={() => handleInstallFFmpeg(false)} disabled={isInstallingFFmpeg}>
|
||||
{isInstallingFFmpeg ? "Installing..." : "Install now"}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user