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()
|
return backend.IsFFprobeInstalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetFFmpegPath() (string, error) {
|
|
||||||
return backend.GetFFmpegPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadFFmpegRequest struct{}
|
type DownloadFFmpegRequest struct{}
|
||||||
|
|
||||||
type DownloadFFmpegResponse 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 {
|
type ConvertAudioRequest struct {
|
||||||
InputFiles []string `json:"input_files"`
|
InputFiles []string `json:"input_files"`
|
||||||
OutputFormat string `json:"output_format"`
|
OutputFormat string `json:"output_format"`
|
||||||
|
|||||||
@@ -81,6 +81,28 @@ func GetFFmpegPath() (string, error) {
|
|||||||
return localPath, nil
|
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)
|
path, err := exec.LookPath(ffmpegName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return path, nil
|
return path, nil
|
||||||
@@ -105,6 +127,28 @@ func GetFFprobePath() (string, error) {
|
|||||||
return localPath, nil
|
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)
|
path, err := exec.LookPath(ffprobeName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return path, nil
|
return path, nil
|
||||||
@@ -146,6 +190,53 @@ func IsFFmpegInstalled() (bool, error) {
|
|||||||
return err == nil, nil
|
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 (
|
const (
|
||||||
ffmpegWindowsURL = "https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffmpeg-windows-amd64.zip"
|
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"
|
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 { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings";
|
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings";
|
||||||
import { applyTheme } from "@/lib/themes";
|
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 { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime";
|
||||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||||
import { TitleBar } from "@/components/TitleBar";
|
import { TitleBar } from "@/components/TitleBar";
|
||||||
@@ -65,6 +65,7 @@ function App() {
|
|||||||
const downloadQueue = useDownloadQueueDialog();
|
const downloadQueue = useDownloadQueueDialog();
|
||||||
const downloadProgress = useDownloadProgress();
|
const downloadProgress = useDownloadProgress();
|
||||||
const [isFFmpegInstalled, setIsFFmpegInstalled] = useState<boolean | null>(null);
|
const [isFFmpegInstalled, setIsFFmpegInstalled] = useState<boolean | null>(null);
|
||||||
|
const [brewPath, setBrewPath] = useState<string>("");
|
||||||
const [isInstallingFFmpeg, setIsInstallingFFmpeg] = useState(false);
|
const [isInstallingFFmpeg, setIsInstallingFFmpeg] = useState(false);
|
||||||
const [ffmpegInstallProgress, setFfmpegInstallProgress] = useState(0);
|
const [ffmpegInstallProgress, setFfmpegInstallProgress] = useState(0);
|
||||||
const [ffmpegInstallStatus, setFfmpegInstallStatus] = useState("");
|
const [ffmpegInstallStatus, setFfmpegInstallStatus] = useState("");
|
||||||
@@ -92,6 +93,8 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
const installed = await CheckFFmpegInstalled();
|
const installed = await CheckFFmpegInstalled();
|
||||||
setIsFFmpegInstalled(installed);
|
setIsFFmpegInstalled(installed);
|
||||||
|
const brew = await GetBrewPath();
|
||||||
|
setBrewPath(brew);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error("Failed to check FFmpeg:", err);
|
console.error("Failed to check FFmpeg:", err);
|
||||||
@@ -170,7 +173,7 @@ function App() {
|
|||||||
console.error("Failed to load history:", err);
|
console.error("Failed to load history:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleInstallFFmpeg = async () => {
|
const handleInstallFFmpeg = async (useBrew: boolean = false) => {
|
||||||
setIsInstallingFFmpeg(true);
|
setIsInstallingFFmpeg(true);
|
||||||
setFfmpegInstallProgress(0);
|
setFfmpegInstallProgress(0);
|
||||||
setFfmpegInstallStatus("starting");
|
setFfmpegInstallStatus("starting");
|
||||||
@@ -187,11 +190,11 @@ function App() {
|
|||||||
EventsOn("ffmpeg:status", (status: string) => {
|
EventsOn("ffmpeg:status", (status: string) => {
|
||||||
setFfmpegInstallStatus(status);
|
setFfmpegInstallStatus(status);
|
||||||
});
|
});
|
||||||
const response = await DownloadFFmpeg();
|
const response = useBrew ? await InstallFFmpegWithBrew() : await DownloadFFmpeg();
|
||||||
EventsOff("ffmpeg:progress");
|
EventsOff("ffmpeg:progress");
|
||||||
EventsOff("ffmpeg:status");
|
EventsOff("ffmpeg:status");
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success("FFmpeg installed successfully!");
|
toast.success(useBrew ? "FFmpeg installed successfully via Homebrew!" : "FFmpeg installed successfully!");
|
||||||
setIsFFmpegInstalled(true);
|
setIsFFmpegInstalled(true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -522,14 +525,23 @@ function App() {
|
|||||||
|
|
||||||
|
|
||||||
<Dialog open={isFFmpegInstalled === false} onOpenChange={() => { }}>
|
<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">
|
<DialogHeader className="space-y-2">
|
||||||
<DialogTitle className="text-lg font-bold tracking-tight">
|
<DialogTitle className="text-lg font-bold tracking-tight">
|
||||||
FFmpeg Required
|
FFmpeg Required
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-sm text-foreground/70 leading-relaxed font-normal">
|
<DialogDescription className="text-sm text-foreground/70 leading-relaxed font-normal">
|
||||||
FFmpeg is essential for SpotiFLAC to function properly.
|
{brewPath ? (
|
||||||
This setup will download about <span className="text-foreground font-semibold">100-200MB</span> of data.
|
<>
|
||||||
|
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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -557,13 +569,20 @@ function App() {
|
|||||||
</div>)}
|
</div>)}
|
||||||
</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()}>
|
{!isInstallingFFmpeg && (<Button variant="outline" className="flex-1 h-11 text-sm font-bold transition-colors" onClick={() => Quit()}>
|
||||||
Exit
|
Exit
|
||||||
</Button>)}
|
</Button>)}
|
||||||
<Button className={`${isInstallingFFmpeg ? 'w-full' : 'flex-1'} h-11 text-sm font-bold shadow-lg shadow-primary/10`} onClick={handleInstallFFmpeg} disabled={isInstallingFFmpeg}>
|
{brewPath ? (
|
||||||
{isInstallingFFmpeg ? "Installing..." : "Install now"}
|
<Button className="flex-1 h-11 text-sm font-bold shadow-lg shadow-primary/10" onClick={() => handleInstallFFmpeg(true)} disabled={isInstallingFFmpeg}>
|
||||||
</Button>
|
{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>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user