.update ffmpeg + linux arm build

This commit is contained in:
afkarxyz
2026-04-14 05:49:23 +07:00
parent 42d25abe0c
commit 1858fd6f12
3 changed files with 162 additions and 163 deletions
+56 -41
View File
@@ -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
<details>
<summary><b>Linux Requirements</b></summary>
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
```
</details>
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 }}
+89 -96
View File
@@ -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()
isARM := runtime.GOARCH == "arm64"
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"}
ffmpegURLs, ffprobeURLs, err := getFFmpegDownloadURLs()
if err != nil {
return err
}
if !ffmpegInstalled && !ffprobeInstalled {
if err := downloadWithFallback(macFFmpegURLs, ffmpegDir, progressCallback, 0, 50); err != nil {
if err := downloadWithFallback(ffmpegURLs, ffmpegDir, progressCallback, 0, 50); err != nil {
return err
}
if err := downloadWithFallback(macFFprobeURLs, ffmpegDir, progressCallback, 50, 100); err != nil {
if err := downloadWithFallback(ffprobeURLs, 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
}
}
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,11 +442,14 @@ 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)
}
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 {
r, err := zip.OpenReader(zipPath)
+10 -19
View File
@@ -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<boolean | null>(null);
const [brewPath, setBrewPath] = useState<string>("");
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
</DialogTitle>
<DialogDescription className="text-sm text-foreground/70 leading-relaxed font-normal">
{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.
</>)}
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 <span className="text-foreground font-semibold">30-40MB</span> of data.
</DialogDescription>
</DialogHeader>
@@ -698,15 +691,13 @@ function App() {
</div>)}
</div>)}
<DialogFooter className={`flex-row gap-3 pt-2 ${brewPath ? 'flex-col' : ''}`}>
<DialogFooter className="flex-row gap-3 pt-2">
{!isInstallingFFmpeg && (<Button variant="outline" className="flex-1 h-11 text-sm font-bold transition-colors" onClick={() => Quit()}>
Exit
</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}>
<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>)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>