package service import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "time" "go.uber.org/zap" "github.com/ShukeBta/MediaStationGo/internal/config" ) // AutoInstallFFmpeg is only called by the admin tool-install endpoint. The // server must not auto-download or keep ffmpeg/ffprobe running during startup. func AutoInstallFFmpeg(log *zap.Logger, cfg *config.Config) (ffprobePath, ffmpegPath string) { // 1. 优先使用配置 / PATH / 本机常见软件目录中的现有工具。 if path, err := resolveLocalExecutable(cfg.App.FFprobePath, "ffprobe"); err == nil { ffprobePath = path cfg.App.FFprobePath = path log.Info("found local ffprobe", zap.String("path", path)) } if path, err := resolveLocalExecutable(cfg.App.FFmpegPath, "ffmpeg"); err == nil { ffmpegPath = path cfg.App.FFmpegPath = path log.Info("found local ffmpeg", zap.String("path", path)) } if ffprobePath != "" || ffmpegPath != "" { return ffprobePath, ffmpegPath } // 2. 检查默认安装位置。 defaultDir := getDefaultInstallDir() ffprobeDefault := filepath.Join(defaultDir, "bin", "ffprobe.exe") ffmpegDefault := filepath.Join(defaultDir, "bin", "ffmpeg.exe") if _, err := os.Stat(ffprobeDefault); err == nil { log.Info("在默认位置找到 ffprobe", zap.String("path", ffprobeDefault)) return ffprobeDefault, ffmpegDefault } // 3. 尝试自动安装。 log.Warn("未找到 ffmpeg/ffprobe,尝试自动安装...") installed, err := tryAutoInstall(log, defaultDir) if err != nil { log.Error("自动安装失败,请手动安装 ffmpeg", zap.Error(err)) return "", "" } if installed { if _, err := os.Stat(ffprobeDefault); err == nil { log.Info("自动安装成功", zap.String("path", ffprobeDefault)) // 更新配置 updateConfigPaths(cfg, ffprobeDefault, ffmpegDefault) return ffprobeDefault, ffmpegDefault } } return "", "" } // getDefaultInstallDir 返回默认安装目录 func getDefaultInstallDir() string { exePath, err := os.Executable() if err != nil { return "./tools/ffmpeg" } exeDir := filepath.Dir(exePath) return filepath.Join(exeDir, "tools", "ffmpeg") } // tryAutoInstall 尝试自动下载并安装 ffmpeg func tryAutoInstall(log *zap.Logger, installDir string) (bool, error) { if runtime.GOOS == "windows" { return downloadFFmpegWindows(log, installDir) } return false, fmt.Errorf("不支持的操作系统: %s", runtime.GOOS) } // downloadFFmpegWindows 下载 Windows 版本的 ffmpeg func downloadFFmpegWindows(log *zap.Logger, installDir string) (bool, error) { log.Info("开始下载 ffmpeg...") // 创建安装目录 if err := os.MkdirAll(installDir, 0o750); err != nil { return false, fmt.Errorf("创建安装目录失败: %w", err) } tempDir, err := os.MkdirTemp("", "mediastationgo-ffmpeg-*") if err != nil { return false, fmt.Errorf("创建临时目录失败: %w", err) } defer os.RemoveAll(tempDir) // 下载 URL (使用 gyani.org 的静态构建) arch := "win64" if !is64Bit() { arch = "win32" } // 先尝试从 gyan.dev 下载(更可靠) downloadURL := fmt.Sprintf("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip") log.Info("下载 ffmpeg", zap.String("url", downloadURL)) // 使用 Go 下载 zipPath := filepath.Join(tempDir, "ffmpeg.zip") if err := downloadFile(log, downloadURL, zipPath); err != nil { // 尝试备用 URL backupURL := fmt.Sprintf("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-%s-gpl.zip", arch) log.Info("尝试备用下载地址", zap.String("url", backupURL)) if err2 := downloadFile(log, backupURL, zipPath); err2 != nil { return false, fmt.Errorf("下载失败: %v, %v", err, err2) } } // 解压 log.Info("解压 ffmpeg...") extractDir := filepath.Join(tempDir, "extract") if err := unzip(log, zipPath, extractDir); err != nil { return false, fmt.Errorf("解压失败: %w", err) } packageRoot, err := findFFmpegPackageRoot(extractDir) if err != nil { return false, err } if err := copyDirContents(packageRoot, installDir); err != nil { return false, fmt.Errorf("复制 ffmpeg 文件失败: %w", err) } ffmpegBin := filepath.Join(installDir, "bin", "ffmpeg.exe") ffprobeBin := filepath.Join(installDir, "bin", "ffprobe.exe") if _, err := os.Stat(ffmpegBin); err != nil { return false, fmt.Errorf("安装后未找到 ffmpeg: %w", err) } if _, err := os.Stat(ffprobeBin); err != nil { return false, fmt.Errorf("安装后未找到 ffprobe: %w", err) } log.Info("ffmpeg 安装完成", zap.String("dir", installDir)) return true, nil } // downloadFile 下载文件 func downloadFile(log *zap.Logger, url, filepath string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("下载失败,HTTP 状态码: %d", resp.StatusCode) } out, err := os.Create(filepath) // #nosec G304 -- filepath is generated by the installer under its temporary download directory. if err != nil { return err } defer out.Close() _, err = io.Copy(out, resp.Body) return err } // updateConfigPaths 更新配置文件中的路径 func updateConfigPaths(cfg *config.Config, ffprobePath, ffmpegPath string) { cfg.App.FFprobePath = ffprobePath cfg.App.FFmpegPath = ffmpegPath // 保存到配置文件 // 这里需要调用 config 包的保存函数 log := zap.L().Named("config") log.Info("已更新 ffmpeg 路径配置", zap.String("ffprobe", ffprobePath), zap.String("ffmpeg", ffmpegPath)) } // is64Bit 检查是否为 64 位系统 func is64Bit() bool { return true // 简化处理,假设为 64 位 }