package player import ( "fmt" "os" "os/exec" "runtime" "strings" ) // Player abstracts execution of a media player for a stream URL. // Implementations may use exec (legacy) or IPC (preferred for mpv). type Player interface { // Play starts playback of the given URL. For long-running players this blocks // until the player exits (or returns immediately for background/IPC impls). Play(url string, extraArgs ...string) error Stop() error // best effort } // IsInstalled reports whether the named executable is on PATH. // Fixed to avoid shell injection (no "/bin/sh -c 'command -v ' + name"). func IsInstalled(name string) bool { if name == "" { return false } // Direct lookpath is best; fall back to exec the name with a harmless arg. if _, err := exec.LookPath(name); err == nil { return true } // Some players may be scripts without direct lookpath in certain envs; try exec --version like. cmd := exec.Command(name, "--version") _ = cmd.Run() // ignore output/err; if it started at all, likely present // More reliable: use LookPath on common variants? For now LookPath is primary. return false } // NewLegacy returns a simple exec-based player (stdio inheritance for interactive controls). // This is the cleaned version of the old subExecute pattern. func NewLegacy(program string, baseArgs ...string) Player { return &legacyPlayer{program: program, baseArgs: baseArgs} } type legacyPlayer struct { program string baseArgs []string cmd *exec.Cmd } func (p *legacyPlayer) Play(url string, extra ...string) error { args := append([]string{}, p.baseArgs...) args = append(args, extra...) args = append(args, url) p.cmd = exec.Command(p.program, args...) p.cmd.Stdin = os.Stdin p.cmd.Stdout = os.Stdout p.cmd.Stderr = os.Stderr if err := p.cmd.Run(); err != nil { // For interactive players the err is often just "exit status N" from 'q'. // Surface it but don't treat as fatal for the app. fmt.Printf("(player exited: %v)\n", err) } return nil // we intentionally do not try CombinedOutput after Run } func (p *legacyPlayer) Stop() error { if p.cmd != nil && p.cmd.Process != nil { _ = p.cmd.Process.Kill() } return nil } // isShellMeta reports if name looks like it contains shell metachars (defense in depth). func isShellMeta(name string) bool { return strings.ContainsAny(name, ";|&`$(){}[]<>\n\r\t ") } // isInstalledLegacy is the old body (kept only for reference / windows where.exe). // New code should use the top-level IsInstalled. func isInstalledLegacy(name string) bool { if runtime.GOOS == "windows" { cmd := exec.Command("where.exe", name) return cmd.Run() == nil } if isShellMeta(name) { return false // refuse obviously bad names } cmd := exec.Command("/bin/sh", "-c", "command -v "+name) // still used on some old paths; prefer LookPath above return cmd.Run() == nil }