feat: auto-install ffmpeg and yt-dlp during GUI installation

Add "Installing media tools" step to the installer that automatically
installs ffmpeg and yt-dlp if not already present on the system.
Uses winget (Windows), Homebrew (macOS), or apt-get (Linux).
Non-critical step — installation continues even if this fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-03-09 21:25:45 +08:00
parent b07e47c517
commit 4ad39d2f17
4 changed files with 132 additions and 1 deletions

View File

@ -231,7 +231,8 @@ func (inst *Installer) runInstall(config InstallConfig) {
{"Extracting scripts", 48, true, inst.stepExtractScripts},
{"Configuring system", 55, false, inst.stepConfigureSystem},
{"Setting up USB driver", 62, false, inst.stepSetupLibusb},
{"Setting up Python environment", 72, false, inst.stepSetupPython},
{"Setting up Python environment", 70, false, inst.stepSetupPython},
{"Installing media tools", 78, false, inst.stepInstallFfmpeg},
{"Writing configuration", 85, true, inst.stepWriteConfig},
{"Verifying installation", 90, false, inst.stepVerify},
{"Setting up auto-start launcher", 95, false, inst.stepAutoRestart},
@ -519,6 +520,24 @@ func (inst *Installer) setupPythonVenv(installDir string) error {
return nil
}
func (inst *Installer) stepInstallFfmpeg(config InstallConfig) error {
needFfmpeg := true
if _, err := exec.LookPath("ffmpeg"); err == nil {
needFfmpeg = false
}
needYtdlp := true
if _, err := exec.LookPath("yt-dlp"); err == nil {
needYtdlp = false
}
if !needFfmpeg && !needYtdlp {
return nil // both already installed
}
return installFfmpeg(inst, needFfmpeg, needYtdlp)
}
func (inst *Installer) stepWriteConfig(config InstallConfig) error {
cfgDir := platformConfigDir()
if err := os.MkdirAll(cfgDir, 0755); err != nil {

View File

@ -138,6 +138,39 @@ func installAutoRestart(installDir string) error {
return nil
}
func installFfmpeg(inst *Installer, needFfmpeg, needYtdlp bool) error {
if _, err := exec.LookPath("brew"); err != nil {
return fmt.Errorf("Homebrew not found — install from https://brew.sh then run: brew install ffmpeg")
}
if needFfmpeg {
inst.emitProgress(ProgressEvent{
Step: "ffmpeg",
Message: "Installing ffmpeg via Homebrew (this may take a few minutes)...",
Percent: 79,
})
cmd := exec.Command("brew", "install", "ffmpeg")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("brew install ffmpeg failed: %s — %w", string(out), err)
}
}
if needYtdlp {
inst.emitProgress(ProgressEvent{
Step: "ffmpeg",
Message: "Installing yt-dlp via Homebrew...",
Percent: 80,
})
cmd := exec.Command("brew", "install", "yt-dlp")
if out, err := cmd.CombinedOutput(); err != nil {
// Non-fatal: yt-dlp is optional
_ = out
}
}
return nil
}
func removeAutoRestart() {
home, err := os.UserHomeDir()
if err != nil {

View File

@ -109,6 +109,38 @@ func installAutoRestart(installDir string) error {
return nil
}
func installFfmpeg(inst *Installer, needFfmpeg, needYtdlp bool) error {
if _, err := exec.LookPath("apt-get"); err != nil {
return fmt.Errorf("package manager not found — please install ffmpeg manually")
}
if needFfmpeg {
inst.emitProgress(ProgressEvent{
Step: "ffmpeg",
Message: "Installing ffmpeg via apt-get...",
Percent: 79,
})
cmd := exec.Command("pkexec", "apt-get", "install", "-y", "ffmpeg")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("apt install ffmpeg failed: %s — %w", string(out), err)
}
}
if needYtdlp {
inst.emitProgress(ProgressEvent{
Step: "ffmpeg",
Message: "Installing yt-dlp via pip...",
Percent: 80,
})
cmd := exec.Command("pip3", "install", "yt-dlp")
if out, err := cmd.CombinedOutput(); err != nil {
_ = out // Non-fatal
}
}
return nil
}
func removeAutoRestart() {
exec.Command("systemctl", "--user", "disable", "--now", systemdServiceName+".service").Run()

View File

@ -155,6 +155,53 @@ func installAutoRestart(installDir string) error {
return nil
}
func installFfmpeg(inst *Installer, needFfmpeg, needYtdlp bool) error {
if _, err := exec.LookPath("winget"); err != nil {
return fmt.Errorf("winget not found — please install ffmpeg manually from https://ffmpeg.org")
}
if needFfmpeg {
inst.emitProgress(ProgressEvent{
Step: "ffmpeg",
Message: "Installing ffmpeg via winget...",
Percent: 79,
})
cmd := exec.Command("winget", "install", "Gyan.FFmpeg",
"--accept-source-agreements", "--accept-package-agreements", "--silent")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("winget install ffmpeg failed: %s — %w", string(out), err)
}
// winget installs ffmpeg to a PATH that requires shell restart.
// Add common install location to current process PATH so subsequent checks work.
for _, dir := range []string{
filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "WinGet", "Links"),
`C:\ffmpeg\bin`,
filepath.Join(os.Getenv("ProgramFiles"), "FFmpeg", "bin"),
} {
if _, err := os.Stat(dir); err == nil {
os.Setenv("PATH", dir+";"+os.Getenv("PATH"))
}
}
}
if needYtdlp {
inst.emitProgress(ProgressEvent{
Step: "ffmpeg",
Message: "Installing yt-dlp via winget...",
Percent: 80,
})
cmd := exec.Command("winget", "install", "yt-dlp.yt-dlp",
"--accept-source-agreements", "--accept-package-agreements", "--silent")
if out, err := cmd.CombinedOutput(); err != nil {
// Non-fatal: yt-dlp is optional
_ = out
}
}
return nil
}
func removeAutoRestart() {
// Remove Registry Run key
exec.Command("reg", "delete",