Files
MeBox/internal/service/ffmpeg_auto_install.go
T
2026-06-27 01:15:06 +08:00

202 lines
5.9 KiB
Go

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 {
if wd, err := os.Getwd(); err == nil {
if _, err := os.Stat(filepath.Join(wd, "scripts", "install-ffmpeg.ps1")); err == nil {
return filepath.Join(wd, "tools", "ffmpeg")
}
}
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 位
}