472 lines
15 KiB
Go
472 lines
15 KiB
Go
package setup
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"os"
|
||
"os/exec"
|
||
"runtime"
|
||
"strings"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
)
|
||
|
||
func init() {
|
||
ensureDependencies()
|
||
}
|
||
|
||
// ensureDependencies 启动时检查 ffmpeg 依赖
|
||
func ensureDependencies() {
|
||
ctx := context.Background()
|
||
g.Log().Info(ctx, "========== 检查依赖环境 ==========")
|
||
|
||
// 打印当前运行环境信息
|
||
g.Log().Infof(ctx, "平台: %s/%s, Docker: %v", runtime.GOOS, runtime.GOARCH, isRunningInContainer())
|
||
|
||
ensureFFmpeg(ctx)
|
||
ensurePython3(ctx)
|
||
ensureSceneDetect(ctx)
|
||
ensureHyperFrames(ctx)
|
||
|
||
g.Log().Info(ctx, "依赖检查完成,所有依赖已就绪")
|
||
g.Log().Info(ctx, "===================================")
|
||
}
|
||
|
||
// isRunningInContainer 检测是否运行在 Docker 容器中
|
||
func isRunningInContainer() bool {
|
||
// 方法1: 检查 /.dockerenv 文件
|
||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||
return true
|
||
}
|
||
// 方法2: 检查 /proc/1/cgroup 是否包含 docker 关键字
|
||
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
|
||
if strings.Contains(string(data), "docker") ||
|
||
strings.Contains(string(data), "kubepods") ||
|
||
strings.Contains(string(data), "containerd") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// inContainer 是否为容器环境(简化调用)
|
||
var inContainer = isRunningInContainer()
|
||
|
||
// ensureFFmpeg 确保 ffmpeg 可用
|
||
func ensureFFmpeg(ctx context.Context) {
|
||
if _, err := exec.LookPath("ffmpeg"); err == nil {
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 已安装")
|
||
return
|
||
}
|
||
|
||
if inContainer {
|
||
g.Log().Fatalf(ctx, "[ffmpeg] ❌ 容器中未找到 ffmpeg,请在 Dockerfile 中预装: RUN apk add --no-cache ffmpeg")
|
||
return
|
||
}
|
||
|
||
g.Log().Infof(ctx, "[ffmpeg] 未找到,尝试自动安装...")
|
||
|
||
switch runtime.GOOS {
|
||
case "darwin":
|
||
installFFmpegOnMac(ctx)
|
||
|
||
case "linux":
|
||
installFFmpegOnLinux(ctx)
|
||
|
||
case "windows":
|
||
installFFmpegOnWindows(ctx)
|
||
|
||
default:
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ 不支持的平台(%s),请手动安装 ffmpeg", runtime.GOOS)
|
||
}
|
||
}
|
||
|
||
// installFFmpegOnMac 通过 Homebrew 安装 ffmpeg
|
||
func installFFmpegOnMac(ctx context.Context) {
|
||
if _, err := exec.LookPath("brew"); err != nil {
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ 未检测到 Homebrew,请手动安装:\n brew install ffmpeg")
|
||
return
|
||
}
|
||
cmd := exec.CommandContext(ctx, "brew", "install", "ffmpeg")
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "[ffmpeg] ❌ 安装失败: %v\n%s", err, string(output))
|
||
return
|
||
}
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
}
|
||
|
||
// installFFmpegOnLinux 在 Linux(含 Docker)上安装 ffmpeg
|
||
func installFFmpegOnLinux(ctx context.Context) {
|
||
// Docker 容器通常以 root 运行,不需要 sudo
|
||
sudoPrefix := ""
|
||
if !inContainer {
|
||
// 非容器环境,检查是否需要 sudo
|
||
if _, err := exec.LookPath("sudo"); err == nil {
|
||
sudoPrefix = "sudo"
|
||
}
|
||
}
|
||
|
||
// 1. 尝试 apt (Debian/Ubuntu)
|
||
if _, err := exec.LookPath("apt-get"); err == nil {
|
||
args := []string{"install", "-y", "ffmpeg"}
|
||
if sudoPrefix != "" {
|
||
args = append([]string{sudoPrefix}, args...)
|
||
}
|
||
cmd := exec.CommandContext(ctx, "apt-get", args...)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "[ffmpeg] ❌ apt-get 安装失败: %v\n%s", err, string(output))
|
||
return
|
||
}
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
// 更新库缓存(Debian/Ubuntu 会用 ldconfig 更新)
|
||
return
|
||
}
|
||
|
||
// 2. 尝试 apk (Alpine Linux,常见于 Docker 精简镜像)
|
||
if _, err := exec.LookPath("apk"); err == nil {
|
||
// Alpine 的 apk 不需要 sudo(默认以 root 运行)
|
||
cmd := exec.CommandContext(ctx, "apk", "add", "ffmpeg")
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "[ffmpeg] ❌ apk 安装失败: %v\n%s", err, string(output))
|
||
return
|
||
}
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
return
|
||
}
|
||
|
||
// 3. 尝试 yum (CentOS/RHEL)
|
||
if _, err := exec.LookPath("yum"); err == nil {
|
||
args := []string{"install", "-y", "ffmpeg"}
|
||
if sudoPrefix != "" {
|
||
args = append([]string{sudoPrefix}, args...)
|
||
}
|
||
cmd := exec.CommandContext(ctx, "yum", args...)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Errorf(ctx, "[ffmpeg] ❌ yum 安装失败: %v\n%s", err, string(output))
|
||
return
|
||
}
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
return
|
||
}
|
||
|
||
if inContainer {
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ 容器中未找到 apt-get/apk/yum,请将 ffmpeg 预装在 Docker 镜像中")
|
||
} else {
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ 请手动安装: sudo apt-get install ffmpeg")
|
||
}
|
||
}
|
||
|
||
// installFFmpegOnWindows 在 Windows 上安装 ffmpeg
|
||
func installFFmpegOnWindows(ctx context.Context) {
|
||
// 1. 尝试 winget (Windows 10/11 内置)
|
||
if _, err := exec.LookPath("winget"); err == nil {
|
||
g.Log().Infof(ctx, "[ffmpeg] 通过 winget 安装...")
|
||
cmd := exec.CommandContext(ctx, "winget", "install", "--id", "FFmpeg.FFmpeg", "-e", "--accept-package-agreements")
|
||
output, err := cmd.CombinedOutput()
|
||
if err == nil {
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
return
|
||
}
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ winget 安装失败: %v\n%s", err, string(output))
|
||
}
|
||
|
||
// 2. 尝试 choco (Chocolatey)
|
||
if _, err := exec.LookPath("choco"); err == nil {
|
||
// choco 安装可能需要管理员权限
|
||
g.Log().Infof(ctx, "[ffmpeg] 通过 choco 安装...")
|
||
cmd := exec.CommandContext(ctx, "choco", "install", "ffmpeg", "-y")
|
||
output, err := cmd.CombinedOutput()
|
||
if err == nil {
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
return
|
||
}
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ choco 安装失败: %v\n%s", err, string(output))
|
||
}
|
||
|
||
// 3. 尝试 scoop
|
||
if _, err := exec.LookPath("scoop"); err == nil {
|
||
g.Log().Infof(ctx, "[ffmpeg] 通过 scoop 安装...")
|
||
cmd := exec.CommandContext(ctx, "scoop", "install", "ffmpeg")
|
||
output, err := cmd.CombinedOutput()
|
||
if err == nil {
|
||
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
|
||
return
|
||
}
|
||
g.Log().Warningf(ctx, "[ffmpeg] ⚠ scoop 安装失败: %v\n%s", err, string(output))
|
||
}
|
||
|
||
g.Log().Warningf(ctx, `[ffmpeg] ⚠ 请手动安装 ffmpeg,推荐方式:
|
||
1. winget install --id FFmpeg.FFmpeg -e
|
||
2. choco install ffmpeg -y
|
||
3. 从 https://ffmpeg.org/download.html 下载并加入 PATH`)
|
||
}
|
||
|
||
// ensureHyperFrames 检查 HyperFrames 是否可用,不可用时自动安装
|
||
func ensureHyperFrames(ctx context.Context) {
|
||
// 1. 优先检查全局 hyperframes 命令
|
||
if path, err := exec.LookPath("hyperframes"); err == nil {
|
||
version := getHyperFramesVersion(path)
|
||
g.Log().Infof(ctx, "[hyperframes] ✔ 已安装, 版本=%s, 路径=%s", version, path)
|
||
return
|
||
}
|
||
|
||
// 2. 检查 npx hyperframes 是否可用
|
||
var outBuf bytes.Buffer
|
||
checkCmd := exec.Command("npx", "hyperframes", "--version")
|
||
checkCmd.Stdout = &outBuf
|
||
checkCmd.Stderr = nil
|
||
if checkCmd.Run() == nil {
|
||
ver := strings.TrimSpace(outBuf.String())
|
||
if ver == "" {
|
||
ver = "未知"
|
||
}
|
||
g.Log().Infof(ctx, "[hyperframes] ✔ 已安装(npx), 版本=%s", ver)
|
||
return
|
||
}
|
||
|
||
// 3. 未安装,检查 npm/node 可用性再决定是否自动安装
|
||
if _, err := exec.LookPath("npm"); err != nil {
|
||
if inContainer {
|
||
g.Log().Infof(ctx, "[hyperframes] npm 不可用,跳过自动安装(Docker 中请预装 Node.js)")
|
||
} else {
|
||
g.Log().Infof(ctx, "[hyperframes] npm 不可用,跳过自动安装(请先安装 Node.js: https://nodejs.org)")
|
||
}
|
||
return
|
||
}
|
||
|
||
if inContainer {
|
||
g.Log().Fatalf(ctx, "[hyperframes] ❌ 容器中未找到 hyperframes,请在 Dockerfile 中预装: RUN npm install -g hyperframes")
|
||
return
|
||
}
|
||
|
||
g.Log().Infof(ctx, "[hyperframes] 未找到,尝试自动安装 (npm install -g hyperframes)...")
|
||
installCmd := exec.Command("npm", "install", "-g", "hyperframes")
|
||
output, err := installCmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[hyperframes] ⚠ 自动安装失败: %v\n%s", err, string(output))
|
||
g.Log().Infof(ctx, "[hyperframes] 请手动安装: npm install -g hyperframes")
|
||
return
|
||
}
|
||
g.Log().Info(ctx, "[hyperframes] ✔ 自动安装成功")
|
||
|
||
// 安装后检查路径
|
||
if path, err := exec.LookPath("hyperframes"); err == nil {
|
||
version := getHyperFramesVersion(path)
|
||
g.Log().Infof(ctx, "[hyperframes] 路径=%s, 版本=%s", path, version)
|
||
}
|
||
}
|
||
|
||
// getHyperFramesVersion 获取 HyperFrames 版本号
|
||
func getHyperFramesVersion(path string) string {
|
||
var outBuf bytes.Buffer
|
||
vCmd := exec.Command(path, "--version")
|
||
vCmd.Stdout = &outBuf
|
||
if vCmd.Run() == nil {
|
||
return strings.TrimSpace(outBuf.String())
|
||
}
|
||
return "未知"
|
||
}
|
||
|
||
// ensurePython3 确保 Python3 可用
|
||
func ensurePython3(ctx context.Context) {
|
||
// 优先检查 python3
|
||
if path, err := exec.LookPath("python3"); err == nil {
|
||
version := getPythonVersion(ctx, path)
|
||
g.Log().Infof(ctx, "[python3] ✔ 已安装, 版本=%s, 路径=%s", version, path)
|
||
return
|
||
}
|
||
|
||
// 回退检查 python(部分系统用 python 指向 python3)
|
||
if path, err := exec.LookPath("python"); err == nil {
|
||
version := getPythonVersion(ctx, path)
|
||
if strings.HasPrefix(version, "3.") {
|
||
g.Log().Infof(ctx, "[python3] ✔ 已安装(python), 版本=%s, 路径=%s", version, path)
|
||
return
|
||
}
|
||
// Python 2,不满足需求
|
||
g.Log().Infof(ctx, "[python3] 检测到 python=%s, 需要 Python 3,尝试自动安装...", version)
|
||
} else {
|
||
if inContainer {
|
||
g.Log().Fatalf(ctx, "[python3] ❌ 容器中未找到 python3,请在 Dockerfile 中预装: RUN apk add --no-cache python3 py3-pip")
|
||
return
|
||
}
|
||
g.Log().Infof(ctx, "[python3] 未找到,尝试自动安装...")
|
||
}
|
||
|
||
installPython3(ctx)
|
||
}
|
||
|
||
// ensureSceneDetect 确保 scenedetect 库可用
|
||
func ensureSceneDetect(ctx context.Context) {
|
||
// 检查 python3 是否可导入 scenedetect
|
||
checkCmd := exec.Command("python3", "-c", "import scenedetect; print(scenedetect.__version__)")
|
||
var outBuf bytes.Buffer
|
||
checkCmd.Stdout = &outBuf
|
||
checkCmd.Stderr = nil
|
||
if checkCmd.Run() == nil {
|
||
ver := strings.TrimSpace(outBuf.String())
|
||
if ver == "" {
|
||
ver = "未知"
|
||
}
|
||
g.Log().Infof(ctx, "[scenedetect] ✔ 已安装, 版本=%s", ver)
|
||
return
|
||
}
|
||
|
||
if inContainer {
|
||
g.Log().Fatalf(ctx, "[scenedetect] ❌ 容器中未找到 scenedetect,请在 Dockerfile 中预装: RUN pip3 install --break-system-packages scenedetect[opencv-headless,ffmpeg]")
|
||
return
|
||
}
|
||
|
||
g.Log().Infof(ctx, "[scenedetect] 未找到,尝试自动安装 (pip install scenedetect[opencv,ffmpeg])...")
|
||
installCmd := exec.Command("pip3", "install", "scenedetect[opencv,ffmpeg]")
|
||
output, err := installCmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[scenedetect] ⚠ pip3 安装失败: %v\n%s", err, string(output))
|
||
g.Log().Infof(ctx, "[scenedetect] 尝试使用 pip 安装...")
|
||
installCmd = exec.Command("pip", "install", "scenedetect[opencv,ffmpeg]")
|
||
output, err = installCmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[scenedetect] ⚠ pip 安装也失败: %v\n%s", err, string(output))
|
||
if inContainer {
|
||
g.Log().Infof(ctx, "[scenedetect] Docker 提示: pip 安装失败可能是因为缺少系统级依赖(如 libopencv-dev)。")
|
||
g.Log().Infof(ctx, "[scenedetect] 建议在 Dockerfile 中预装: apt-get install -y python3-opencv ffmpeg && pip install scenedetect")
|
||
} else {
|
||
g.Log().Infof(ctx, "[scenedetect] 请手动安装: pip install 'scenedetect[opencv,ffmpeg]'")
|
||
}
|
||
return
|
||
}
|
||
}
|
||
g.Log().Info(ctx, "[scenedetect] ✔ 安装成功")
|
||
|
||
// 验证安装
|
||
verifyCmd := exec.Command("python3", "-c", "import scenedetect; print(scenedetect.__version__)")
|
||
outBuf.Reset()
|
||
verifyCmd.Stdout = &outBuf
|
||
if verifyCmd.Run() == nil {
|
||
g.Log().Infof(ctx, "[scenedetect] 验证通过, 版本=%s", strings.TrimSpace(outBuf.String()))
|
||
}
|
||
}
|
||
|
||
// getPythonVersion 获取 Python 版本号
|
||
func getPythonVersion(ctx context.Context, pythonPath string) string {
|
||
var outBuf bytes.Buffer
|
||
vCmd := exec.CommandContext(ctx, pythonPath, "--version")
|
||
vCmd.Stdout = &outBuf
|
||
vCmd.Stderr = &outBuf
|
||
if vCmd.Run() == nil {
|
||
return strings.TrimPrefix(strings.TrimSpace(outBuf.String()), "Python ")
|
||
}
|
||
return "未知"
|
||
}
|
||
|
||
// installPython3 根据平台自动安装 Python3
|
||
func installPython3(ctx context.Context) {
|
||
switch runtime.GOOS {
|
||
case "darwin":
|
||
if _, err := exec.LookPath("brew"); err == nil {
|
||
g.Log().Infof(ctx, "[python3] 通过 brew 安装...")
|
||
cmd := exec.CommandContext(ctx, "brew", "install", "python@3")
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ brew 安装失败: %v\n%s", err, string(output))
|
||
g.Log().Infof(ctx, "[python3] 请手动安装: brew install python@3")
|
||
} else {
|
||
g.Log().Info(ctx, "[python3] ✔ 安装成功")
|
||
}
|
||
} else {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ 未检测到 Homebrew,请手动安装 Python3:\n https://www.python.org/downloads/")
|
||
}
|
||
|
||
case "linux":
|
||
sudoPrefix := ""
|
||
if !inContainer {
|
||
if _, err := exec.LookPath("sudo"); err == nil {
|
||
sudoPrefix = "sudo"
|
||
}
|
||
}
|
||
|
||
// 1. apt (Debian/Ubuntu)
|
||
if _, err := exec.LookPath("apt-get"); err == nil {
|
||
args := []string{"install", "-y", "python3", "python3-pip"}
|
||
if sudoPrefix != "" {
|
||
args = append([]string{sudoPrefix}, args...)
|
||
}
|
||
cmd := exec.CommandContext(ctx, "apt-get", args...)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ apt-get 安装失败: %v\n%s", err, string(output))
|
||
} else {
|
||
g.Log().Info(ctx, "[python3] ✔ 安装成功")
|
||
}
|
||
return
|
||
}
|
||
// 2. apk (Alpine)
|
||
if _, err := exec.LookPath("apk"); err == nil {
|
||
cmd := exec.CommandContext(ctx, "apk", "add", "python3", "py3-pip")
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ apk 安装失败: %v\n%s", err, string(output))
|
||
} else {
|
||
g.Log().Info(ctx, "[python3] ✔ 安装成功")
|
||
}
|
||
return
|
||
}
|
||
// 3. yum (CentOS/RHEL)
|
||
if _, err := exec.LookPath("yum"); err == nil {
|
||
args := []string{"install", "-y", "python3", "python3-pip"}
|
||
if sudoPrefix != "" {
|
||
args = append([]string{sudoPrefix}, args...)
|
||
}
|
||
cmd := exec.CommandContext(ctx, "yum", args...)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ yum 安装失败: %v\n%s", err, string(output))
|
||
} else {
|
||
g.Log().Info(ctx, "[python3] ✔ 安装成功")
|
||
}
|
||
return
|
||
}
|
||
|
||
if inContainer {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ 容器中未找到包管理器,请将 python3 预装在 Docker 镜像中")
|
||
} else {
|
||
g.Log().Warningf(ctx, "[python3] ⚠ 请手动安装: sudo apt-get install python3 python3-pip")
|
||
}
|
||
|
||
case "windows":
|
||
// 1. winget
|
||
if _, err := exec.LookPath("winget"); err == nil {
|
||
g.Log().Infof(ctx, "[python3] 通过 winget 安装...")
|
||
cmd := exec.CommandContext(ctx, "winget", "install", "--id", "Python.Python.3", "-e", "--accept-package-agreements")
|
||
output, err := cmd.CombinedOutput()
|
||
if err == nil {
|
||
g.Log().Info(ctx, "[python3] ✔ 安装成功")
|
||
return
|
||
}
|
||
g.Log().Warningf(ctx, "[python3] ⚠ winget 安装失败: %v\n%s", err, string(output))
|
||
}
|
||
// 2. choco
|
||
if _, err := exec.LookPath("choco"); err == nil {
|
||
g.Log().Infof(ctx, "[python3] 通过 choco 安装...")
|
||
cmd := exec.CommandContext(ctx, "choco", "install", "python", "-y")
|
||
output, err := cmd.CombinedOutput()
|
||
if err == nil {
|
||
g.Log().Info(ctx, "[python3] ✔ 安装成功")
|
||
return
|
||
}
|
||
g.Log().Warningf(ctx, "[python3] ⚠ choco 安装失败: %v\n%s", err, string(output))
|
||
}
|
||
|
||
g.Log().Warningf(ctx, `[python3] ⚠ 请手动安装 Python3:
|
||
1. winget install --id Python.Python.3 -e
|
||
2. 从 https://www.python.org/downloads/ 下载安装(记得勾选"Add to PATH")`)
|
||
|
||
default:
|
||
g.Log().Warningf(ctx, "[python3] ⚠ 不支持的平台(%s),请手动安装 Python3", runtime.GOOS)
|
||
}
|
||
}
|