From 1858fd6f12f99b97094957c1f47de5abf3e9036e Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Tue, 14 Apr 2026 05:49:23 +0700 Subject: [PATCH] .update ffmpeg + linux arm build --- .github/workflows/build.yml | 97 ++++++++++-------- backend/ffmpeg.go | 199 +++++++++++++++++------------------- frontend/src/App.tsx | 29 ++---- 3 files changed, 162 insertions(+), 163 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e09dd5..68355d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,13 +81,13 @@ jobs: - name: Prepare artifacts run: | mkdir -p dist - Copy-Item -Path "build\bin\SpotiFLAC.exe" -Destination "dist\SpotiFLAC.exe" + Compress-Archive -Path "build\bin\SpotiFLAC.exe" -DestinationPath "dist\SpotiFLAC-windows.zip" -Force - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: windows-portable - path: dist/SpotiFLAC.exe + name: windows-bundle + path: dist/SpotiFLAC-windows.zip retention-days: 7 build-macos: @@ -147,36 +147,33 @@ jobs: - name: Build application run: wails build -platform darwin/universal - - name: Create DMG + - name: Create macOS bundle run: | mkdir -p dist - # Install create-dmg if not available - brew install create-dmg || true - - # Create DMG - create-dmg \ - --volname "SpotiFLAC" \ - --window-pos 200 120 \ - --window-size 600 400 \ - --icon-size 100 \ - --icon "SpotiFLAC.app" 175 120 \ - --hide-extension "SpotiFLAC.app" \ - --app-drop-link 425 120 \ - "dist/SpotiFLAC.dmg" \ - "build/bin/SpotiFLAC.app" || \ - # Fallback to hdiutil if create-dmg fails - hdiutil create -volname SpotiFLAC -srcfolder build/bin/SpotiFLAC.app -ov -format UDZO dist/SpotiFLAC.dmg + ditto -c -k --sequesterRsrc --keepParent "build/bin/SpotiFLAC.app" "dist/SpotiFLAC-macos-bundle.zip" - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: macos-portable - path: dist/SpotiFLAC.dmg + name: macos-bundle + path: dist/SpotiFLAC-macos-bundle.zip retention-days: 7 build-linux: - name: Build Linux - runs-on: ubuntu-24.04 + name: Build Linux (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + goarch: amd64 + runner: ubuntu-24.04 + appimage_arch: x86_64 + - arch: arm64 + goarch: arm64 + runner: ubuntu-24.04-arm + appimage_arch: aarch64 steps: - name: Checkout code uses: actions/checkout@v4 @@ -222,10 +219,15 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libfuse2 imagemagick upx-ucl + PACKAGES="libgtk-3-dev libwebkit2gtk-4.1-dev libfuse2 imagemagick" + if [ "${{ matrix.goarch }}" = "amd64" ]; then + PACKAGES="$PACKAGES upx-ucl" + fi + sudo apt-get install -y $PACKAGES # Create symlink for webkit2gtk-4.0 -> webkit2gtk-4.1 (Ubuntu 24.04 compatibility) - sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.0.pc + MULTIARCH="$(dpkg-architecture -qDEB_HOST_MULTIARCH)" + sudo ln -sf "/usr/lib/${MULTIARCH}/pkgconfig/webkit2gtk-4.1.pc" "/usr/lib/${MULTIARCH}/pkgconfig/webkit2gtk-4.0.pc" - name: Install Wails CLI run: go install github.com/wailsapp/wails/v2/cmd/wails@latest @@ -237,9 +239,10 @@ jobs: pnpm run generate-icon - name: Build application - run: wails build -platform linux/amd64 + run: wails build -platform linux/${{ matrix.goarch }} - name: Compress with UPX + if: matrix.goarch == 'amd64' run: | upx --best --lzma build/bin/SpotiFLAC @@ -248,13 +251,13 @@ jobs: uses: actions/cache@v4 with: path: appimagetool - key: appimagetool-x86_64-v1 + key: appimagetool-${{ matrix.appimage_arch }}-v1 - name: Download appimagetool if: steps.cache-appimagetool.outputs.cache-hit != 'true' run: | - wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage || \ - wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage + wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${{ matrix.appimage_arch }}.AppImage || \ + wget --timeout=30 --tries=5 --retry-connrefused --waitretry=5 -O appimagetool https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${{ matrix.appimage_arch }}.AppImage - name: Make appimagetool executable run: chmod +x appimagetool @@ -309,13 +312,13 @@ jobs: # Create AppImage mkdir -p dist - ARCH=x86_64 ./appimagetool --no-appstream AppDir dist/SpotiFLAC.AppImage + ARCH=${{ matrix.appimage_arch }} ./appimagetool --no-appstream AppDir dist/SpotiFLAC-linux-${{ matrix.arch }}.AppImage - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: linux-portable - path: dist/SpotiFLAC.AppImage + name: linux-appimage-${{ matrix.arch }} + path: dist/SpotiFLAC-linux-${{ matrix.arch }}.AppImage retention-days: 7 create-release: @@ -343,6 +346,13 @@ jobs: - name: Display structure of downloaded files run: ls -R artifacts + - name: Create Linux bundle + run: | + mkdir -p release/SpotiFLAC-linux-bundle + cp artifacts/linux-appimage-amd64/*.AppImage release/SpotiFLAC-linux-bundle/ + cp artifacts/linux-appimage-arm64/*.AppImage release/SpotiFLAC-linux-bundle/ + tar -czf release/SpotiFLAC-linux-bundle.tar.gz -C release SpotiFLAC-linux-bundle + - name: Create Release uses: softprops/action-gh-release@v2 with: @@ -354,15 +364,20 @@ jobs: ## Downloads - - `SpotiFLAC.exe` - Windows - - `SpotiFLAC.dmg` - macOS - - `SpotiFLAC.AppImage` - Linux + - `SpotiFLAC-windows.zip` - amd64 + - `SpotiFLAC-macos-bundle.zip` - amd64 + arm64 + - `SpotiFLAC-linux-bundle.tar.gz` - amd64 + arm64v8
Linux Requirements The AppImage requires `webkit2gtk-4.1` to be installed on your system: + Choose the correct AppImage after extracting the bundle: + + - `SpotiFLAC-linux-amd64.AppImage` - amd64 + - `SpotiFLAC-linux-arm64.AppImage` - arm64v8 + **Ubuntu/Debian:** ```bash sudo apt install libwebkit2gtk-4.1-0 @@ -380,14 +395,14 @@ jobs: After installing the dependency, make the AppImage executable: ```bash - chmod +x SpotiFLAC.AppImage - ./SpotiFLAC.AppImage + tar -xzf SpotiFLAC-linux-bundle.tar.gz + chmod +x SpotiFLAC-linux-*.AppImage ```
files: | - artifacts/windows-portable/*.exe - artifacts/macos-portable/*.dmg - artifacts/linux-portable/*.AppImage + artifacts/windows-bundle/*.zip + artifacts/macos-bundle/*.zip + release/SpotiFLAC-linux-bundle.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/backend/ffmpeg.go b/backend/ffmpeg.go index b9cd3e3..f228bb4 100644 --- a/backend/ffmpeg.go +++ b/backend/ffmpeg.go @@ -83,6 +83,37 @@ func GetFFmpegDir() (string, error) { return EnsureAppDir() } +func resolveSystemExecutable(executableName string) string { + if runtime.GOOS == "darwin" { + candidates := []string{ + "/opt/homebrew/bin/" + executableName, + "/usr/local/bin/" + executableName, + } + for _, candidate := range candidates { + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + } + + if runtime.GOOS != "windows" { + path, err := exec.Command("which", executableName).Output() + if err == nil { + trimmed := strings.TrimSpace(string(path)) + if trimmed != "" { + return trimmed + } + } + } + + path, err := exec.LookPath(executableName) + if err == nil { + return path + } + + return "" +} + func GetFFmpegPath() (string, error) { ffmpegDir, err := GetFFmpegDir() if err != nil { @@ -94,38 +125,15 @@ func GetFFmpegPath() (string, error) { ffmpegName = "ffmpeg.exe" } + if path := resolveSystemExecutable(ffmpegName); path != "" { + return path, nil + } + localPath := filepath.Join(ffmpegDir, ffmpegName) if _, err := os.Stat(localPath); err == 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) - if err == nil { - return path, nil - } - return localPath, nil } @@ -140,38 +148,15 @@ func GetFFprobePath() (string, error) { ffprobeName = "ffprobe.exe" } + if path := resolveSystemExecutable(ffprobeName); path != "" { + return path, nil + } + localPath := filepath.Join(ffmpegDir, ffprobeName) if _, err := os.Stat(localPath); err == 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) - if err == nil { - return path, nil - } - return localPath, fmt.Errorf("ffprobe not found in app directory or system path") } @@ -205,7 +190,11 @@ func IsFFmpegInstalled() (bool, error) { setHideWindow(cmd) err = cmd.Run() - return err == nil, nil + if err != nil { + return false, nil + } + + return IsFFprobeInstalled() } func GetBrewPath() string { @@ -255,10 +244,38 @@ func InstallFFmpegWithBrew(progressCallback func(int, string)) error { 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" -) +const ffmpegReleaseBaseURL = "https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.1" + +func buildFFmpegReleaseURL(assetName string) string { + return ffmpegReleaseBaseURL + "/" + assetName +} + +func getFFmpegDownloadURLs() ([]string, []string, error) { + switch runtime.GOOS { + case "windows": + return []string{buildFFmpegReleaseURL("ffmpeg-windows.zip")}, []string{buildFFmpegReleaseURL("ffprobe-windows.zip")}, nil + case "linux": + switch runtime.GOARCH { + case "amd64": + return []string{buildFFmpegReleaseURL("ffmpeg-linux-amd64.zip")}, []string{buildFFmpegReleaseURL("ffprobe-linux-amd64.zip")}, nil + case "arm64": + return []string{buildFFmpegReleaseURL("ffmpeg-linux-arm64v8.zip")}, []string{buildFFmpegReleaseURL("ffprobe-linux-arm64v8.zip")}, nil + default: + return nil, nil, fmt.Errorf("unsupported Linux architecture: %s", runtime.GOARCH) + } + case "darwin": + switch runtime.GOARCH { + case "amd64": + return []string{buildFFmpegReleaseURL("ffmpeg-macos-amd64.zip")}, []string{buildFFmpegReleaseURL("ffprobe-macos-amd64.zip")}, nil + case "arm64": + return []string{buildFFmpegReleaseURL("ffmpeg-macos-arm64.zip")}, []string{buildFFmpegReleaseURL("ffprobe-macos-arm64.zip")}, nil + default: + return nil, nil, fmt.Errorf("unsupported macOS architecture: %s", runtime.GOARCH) + } + default: + return nil, nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } +} func DownloadFFmpeg(progressCallback func(int)) error { @@ -276,57 +293,30 @@ func DownloadFFmpeg(progressCallback func(int)) error { return fmt.Errorf("failed to create ffmpeg directory: %w", err) } - if runtime.GOOS == "darwin" { - ffmpegInstalled, _ := IsFFmpegInstalled() - ffprobeInstalled, _ := IsFFprobeInstalled() + ffmpegInstalled, _ := IsFFmpegInstalled() + ffprobeInstalled, _ := IsFFprobeInstalled() - isARM := runtime.GOARCH == "arm64" + ffmpegURLs, ffprobeURLs, err := getFFmpegDownloadURLs() + if err != nil { + return err + } - var macFFmpegURLs []string - var macFFprobeURLs []string - - if isARM { - - macFFmpegURLs = []string{"https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffmpeg-macos-arm64.zip"} - macFFprobeURLs = []string{"https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffprobe-macos-arm64.zip"} - } else { - - macFFmpegURLs = []string{"https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffmpeg-macos-intel.zip"} - macFFprobeURLs = []string{"https://github.com/afkarxyz/ffmpeg-binaries/releases/download/v8.0/ffprobe-macos-intel.zip"} + if !ffmpegInstalled && !ffprobeInstalled { + if err := downloadWithFallback(ffmpegURLs, ffmpegDir, progressCallback, 0, 50); err != nil { + return err } - - if !ffmpegInstalled && !ffprobeInstalled { - if err := downloadWithFallback(macFFmpegURLs, ffmpegDir, progressCallback, 0, 50); err != nil { - return err - } - if err := downloadWithFallback(macFFprobeURLs, ffmpegDir, progressCallback, 50, 100); err != nil { - return err - } - } else if !ffmpegInstalled { - if err := downloadWithFallback(macFFmpegURLs, ffmpegDir, progressCallback, 0, 100); err != nil { - return err - } - } else if !ffprobeInstalled { - if err := downloadWithFallback(macFFprobeURLs, ffmpegDir, progressCallback, 0, 100); err != nil { - return err - } + if err := downloadWithFallback(ffprobeURLs, ffmpegDir, progressCallback, 50, 100); err != nil { + return err } return nil } - var url string - switch runtime.GOOS { - case "windows": - url = ffmpegWindowsURL - case "linux": - url = ffmpegLinuxURL - default: - return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + if !ffmpegInstalled { + return downloadWithFallback(ffmpegURLs, ffmpegDir, progressCallback, 0, 100) } - fmt.Printf("[FFmpeg] Downloading from: %s\n", url) - if err := downloadAndExtract(url, ffmpegDir, progressCallback, 0, 100); err != nil { - return err + if !ffprobeInstalled { + return downloadWithFallback(ffprobeURLs, ffmpegDir, progressCallback, 0, 100) } return nil @@ -452,10 +442,13 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres } fmt.Printf("[FFmpeg] Extracting...\n") - if strings.HasSuffix(url, ".tar.xz") || runtime.GOOS == "linux" { + if strings.HasSuffix(url, ".tar.xz") { return extractTarXz(tmpFile.Name(), destDir) } - return extractZip(tmpFile.Name(), destDir) + if strings.HasSuffix(url, ".zip") { + return extractZip(tmpFile.Name(), destDir) + } + return fmt.Errorf("unsupported archive format for %s", url) } func extractZip(zipPath, destDir string) error { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2b62e7f..bb1ae62 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 } from "@/lib/settings"; import { applyTheme } from "@/lib/themes"; -import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetBrewPath, GetRecentFetches, InstallFFmpegWithBrew, SaveRecentFetches } from "../wailsjs/go/main/App"; +import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetRecentFetches, SaveRecentFetches } 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"; @@ -153,7 +153,6 @@ 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(""); @@ -181,8 +180,6 @@ function App() { try { const installed = await CheckFFmpegInstalled(); setIsFFmpegInstalled(installed); - const brew = await GetBrewPath(); - setBrewPath(brew); } catch (err) { console.error("Failed to check FFmpeg:", err); @@ -263,7 +260,7 @@ function App() { localStorage.removeItem(HISTORY_KEY); } }, [persistRecentHistory]); - const handleInstallFFmpeg = async (useBrew: boolean = false) => { + const handleInstallFFmpeg = async () => { setIsInstallingFFmpeg(true); setFfmpegInstallProgress(0); setFfmpegInstallStatus("starting"); @@ -280,11 +277,11 @@ function App() { EventsOn("ffmpeg:status", (status: string) => { setFfmpegInstallStatus(status); }); - const response = useBrew ? await InstallFFmpegWithBrew() : await DownloadFFmpeg(); + const response = await DownloadFFmpeg(); EventsOff("ffmpeg:progress"); EventsOff("ffmpeg:status"); if (response.success) { - toast.success(useBrew ? "FFmpeg installed successfully via Homebrew!" : "FFmpeg installed successfully!"); + toast.success("FFmpeg installed successfully!"); setIsFFmpegInstalled(true); } else { @@ -664,13 +661,9 @@ function App() { FFmpeg Required - {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. - )} + SpotiFLAC checks your system for FFmpeg and FFprobe first. + If they are not available, the required binaries will be downloaded from GitHub. + This setup downloads about 30-40MB of data. @@ -698,15 +691,13 @@ function App() { )} )} - + {!isInstallingFFmpeg && ()} - {brewPath ? () : ()} +