gostations/internal/player/player.go
Greg Gauthier b378fec3b2
Some checks failed
gobuild / build (push) Failing after 4s
refactor: reorganize into internal packages, fix critical panics and error handling
Move browser, config, player, and radio logic into internal/{config,radio,player,ui,version}.
Add cached API host resolution, context-aware HTTP client, and nil/length guards to eliminate
RandomIP, nslookup, reverseLookup, and GetStations panics. Replace subExecute with clean
legacyPlayer using Run-only execution. Fix inverted -short test guards, unformatted config
error messages, and "Erorr" typo. Remove obsolete vendor/GOPATH logic from build scripts.
Update callers in stations.go and radiomenu.go to new paths; retain shims for transition.
2026-06-05 21:10:54 +01:00

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
}