92 lines
2.8 KiB
Go
92 lines
2.8 KiB
Go
|
|
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
|
||
|
|
}
|