TUISTREAM/internal/jellyfin/install.go
28allday 96c908f324 Auto-install GPU hardware-transcoding packages on Jellyfin install
Detect the host's GPU vendor(s) via PCI sysfs (no lspci dependency) and
append vendor-matched encoding-package steps to the install plan:
Intel -> intel-media-driver + vpl-gpu-rt + intel-compute-runtime,
AMD -> mesa + libva-mesa-driver. NVIDIA NVENC ships with the driver's
own userspace, so nothing is added when a driver is loaded, and a
warning step (never an automatic kernel-driver install) when it isn't.

Without these packages Jellyfin installs fine and direct-plays fine,
but any client that needs a transcode fails instantly with the opaque
"FFmpeg exited with code 251" / "fatal playback error" pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:58:22 +01:00

118 lines
3.8 KiB
Go

package jellyfin
import (
"os/exec"
"strings"
"tuistream/internal/step"
"tuistream/internal/system"
)
// InstallPlan builds the ordered list of commands needed to install Jellyfin
// from the official Arch extra repo. We don't support the AUR jellyfin-bin
// path anymore — see reference_aur_jellyfin_bin_stale memory for the why.
//
// This is a *plan*, not an execution. The TUI runs it step by step via
// tea.ExecProcess so pacman can prompt interactively if needed.
func InstallPlan() []step.Step {
steps := []step.Step{
{
Title: "Refresh package databases",
Cmd: exec.Command("pacman", "-Sy", "--noconfirm"),
},
{
Title: "Install jellyfin-server + jellyfin-web + jellyfin-ffmpeg",
Cmd: exec.Command("pacman", "-S", "--needed", "--noconfirm",
"jellyfin-server", "jellyfin-web", "jellyfin-ffmpeg"),
},
}
steps = append(steps, gpuEncodingSteps()...)
return append(steps, step.Step{
Title: "Enable and start jellyfin.service",
Cmd: exec.Command("systemctl", "enable", "--now", "jellyfin.service"),
})
}
// gpuEncodingSteps maps the host's GPU vendor(s) to the userspace packages
// jellyfin-ffmpeg needs before hardware transcoding works on that vendor.
// Without them Jellyfin installs and direct-plays fine, but the moment a
// client needs a transcode every attempt dies at hw-init with the opaque
// "FFmpeg exited with code 251" / fatal-playback-error combo.
//
// NVIDIA is deliberately conservative: NVENC needs only the driver's own
// userspace (nvidia-utils), so with a loaded driver there is nothing to
// add, and without one we won't auto-install a kernel driver from here —
// picking nvidia vs nvidia-open vs -dkms per kernel flavour is a job for
// the distro/user, and getting it wrong can break the box's boot.
func gpuEncodingSteps() []step.Step {
var steps []step.Step
for _, v := range system.DetectGPUs() {
switch v {
case system.VendorIntel:
steps = append(steps, step.Step{
Title: "Install Intel QSV/VA-API encoding packages",
Cmd: exec.Command("pacman", "-S", "--needed", "--noconfirm",
"intel-media-driver", "vpl-gpu-rt", "intel-compute-runtime"),
})
case system.VendorAMD:
steps = append(steps, step.Step{
Title: "Install AMD VA-API encoding packages",
Cmd: exec.Command("pacman", "-S", "--needed", "--noconfirm",
"mesa", "libva-mesa-driver"),
})
case system.VendorNVIDIA:
if system.NvidiaDriverLoaded() {
continue
}
steps = append(steps, step.Step{
Title: "NVIDIA GPU found but no driver loaded — skipping NVENC setup",
Cmd: exec.Command("bash", "-lc",
`echo "Install the NVIDIA driver for your kernel (nvidia / nvidia-open / nvidia-lts) and reboot to enable NVENC transcoding."`),
})
}
}
return steps
}
// UninstallPlan reverses an install. Pass `purgeData` to also delete
// /var/lib/jellyfin (library DB + settings).
func UninstallPlan(packages PackageSet, purgeData bool) []step.Step {
var steps []step.Step
steps = append(steps, step.Step{
Title: "Stop and disable jellyfin.service",
Cmd: exec.Command("systemctl", "disable", "--now", "jellyfin.service"),
})
if names := packages.Installed(); len(names) > 0 {
args := []string{"-Rns", "--noconfirm"}
args = append(args, names...)
steps = append(steps, step.Step{
Title: "Remove " + sprintList(names),
Cmd: exec.Command("pacman", args...),
})
}
if purgeData {
steps = append(steps, step.Step{
Title: "Delete /var/lib/jellyfin (library DB + settings)",
Cmd: exec.Command("rm", "-rf", "/var/lib/jellyfin"),
})
steps = append(steps, step.Step{
Title: "Delete /etc/jellyfin and caches",
Cmd: bashAsRoot(`rm -rf /etc/jellyfin /var/cache/jellyfin /var/log/jellyfin`),
})
}
return steps
}
// ---- helpers ----
func bashAsRoot(script string) *exec.Cmd {
return exec.Command("bash", "-lc", script)
}
func sprintList(s []string) string {
return strings.Join(s, ", ")
}