2021-03-16 12:32:59 +00:00
|
|
|
package main
|
|
|
|
|
|
2021-03-16 13:09:27 +00:00
|
|
|
import (
|
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 20:10:54 +00:00
|
|
|
"context"
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
"encoding/json"
|
2021-03-16 13:09:27 +00:00
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
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 20:10:54 +00:00
|
|
|
|
|
|
|
|
"github.com/gmgauthier/gostations/internal/config"
|
|
|
|
|
playerpkg "github.com/gmgauthier/gostations/internal/player"
|
|
|
|
|
"github.com/gmgauthier/gostations/internal/radio"
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
"github.com/gmgauthier/gostations/internal/ui"
|
2021-03-16 13:09:27 +00:00
|
|
|
)
|
2021-03-16 12:32:59 +00:00
|
|
|
|
2021-03-18 22:24:46 +00:00
|
|
|
var version string
|
2021-03-18 21:14:53 +00:00
|
|
|
|
2024-07-09 20:29:11 +00:00
|
|
|
func showVersion() {
|
2021-03-18 21:14:53 +00:00
|
|
|
fmt.Println(version)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 20:29:11 +00:00
|
|
|
func precheck() {
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
p := "mpv"
|
|
|
|
|
if v, err := config.Get("player.command"); err == nil && v != "" {
|
|
|
|
|
p = v
|
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 20:10:54 +00:00
|
|
|
}
|
|
|
|
|
if !playerpkg.IsInstalled(p) {
|
|
|
|
|
fmt.Printf("%s is either not installed, or not on your $PATH. Cannot continue.\n", p)
|
2021-03-17 07:17:09 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
// runFind implements the "find" subcommand for scripting / non-interactive lookup.
|
|
|
|
|
// Usage: gostations find [-c country] [-t tags] ... [-x] [-j]
|
|
|
|
|
func runFind(args []string) {
|
|
|
|
|
fs := flag.NewFlagSet("find", flag.ExitOnError)
|
|
|
|
|
var (
|
|
|
|
|
name string
|
|
|
|
|
country string
|
|
|
|
|
state string
|
|
|
|
|
tags string
|
|
|
|
|
notok bool
|
|
|
|
|
jsonOut bool
|
|
|
|
|
)
|
|
|
|
|
fs.StringVar(&name, "n", "", "Station name (or identifier).")
|
|
|
|
|
fs.StringVar(&country, "c", "", "Home country.")
|
|
|
|
|
fs.StringVar(&state, "s", "", "Home state (if in the United States).")
|
|
|
|
|
fs.StringVar(&tags, "t", "", "Tag (or comma-separated tag list)")
|
|
|
|
|
fs.BoolVar(¬ok, "x", false, "If toggled, will show stations that are down")
|
|
|
|
|
fs.BoolVar(&jsonOut, "j", false, "Output as JSON array (for scripting)")
|
|
|
|
|
fs.BoolVar(&jsonOut, "json", false, "Output as JSON array (for scripting)")
|
|
|
|
|
|
|
|
|
|
fs.Usage = func() {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: gostations find [options]\n\nOptions:\n")
|
|
|
|
|
fs.PrintDefaults()
|
|
|
|
|
}
|
|
|
|
|
_ = fs.Parse(args)
|
|
|
|
|
|
|
|
|
|
if err := config.Init(); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stations, err := radio.Search(context.Background(), name, country, state, tags, notok)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "search: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if jsonOut {
|
|
|
|
|
b, _ := json.MarshalIndent(stations, "", " ")
|
|
|
|
|
fmt.Println(string(b))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// human/script friendly list (similar to old output)
|
|
|
|
|
fmt.Println("...Found Stations...")
|
|
|
|
|
for i, s := range stations {
|
|
|
|
|
listing := fmt.Sprintf("%-40s %-5s %-5s %s", Short(s.Name, 40), s.Codec, s.Bitrate, s.Url)
|
|
|
|
|
fmt.Printf("%d) %s\n", i+1, listing)
|
|
|
|
|
}
|
|
|
|
|
if len(stations) == 0 {
|
|
|
|
|
fmt.Println("(no stations matched)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runPlay implements the "play" subcommand for scripting direct playback.
|
|
|
|
|
// It accepts the same search flags or a direct URL as first positional arg.
|
|
|
|
|
// Plays the first match (or the URL) using the configured player and blocks.
|
|
|
|
|
func runPlay(args []string) {
|
|
|
|
|
fs := flag.NewFlagSet("play", flag.ExitOnError)
|
|
|
|
|
var (
|
|
|
|
|
name string
|
|
|
|
|
country string
|
|
|
|
|
state string
|
|
|
|
|
tags string
|
|
|
|
|
notok bool
|
|
|
|
|
)
|
|
|
|
|
fs.StringVar(&name, "n", "", "Station name (or identifier).")
|
|
|
|
|
fs.StringVar(&country, "c", "", "Home country.")
|
|
|
|
|
fs.StringVar(&state, "s", "", "Home state (if in the United States).")
|
|
|
|
|
fs.StringVar(&tags, "t", "", "Tag (or comma-separated tag list)")
|
|
|
|
|
fs.BoolVar(¬ok, "x", false, "If toggled, will show stations that are down")
|
|
|
|
|
|
|
|
|
|
fs.Usage = func() {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: gostations play [options] [direct-url]\n\nOptions:\n")
|
|
|
|
|
fs.PrintDefaults()
|
|
|
|
|
}
|
|
|
|
|
_ = fs.Parse(args)
|
|
|
|
|
|
|
|
|
|
pos := fs.Args()
|
|
|
|
|
|
|
|
|
|
if err := config.Init(); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
precheck()
|
|
|
|
|
|
|
|
|
|
var playURL string
|
|
|
|
|
if len(pos) > 0 {
|
|
|
|
|
playURL = pos[0]
|
|
|
|
|
} else {
|
|
|
|
|
stations, err := radio.Search(context.Background(), name, country, state, tags, notok)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "search: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
if len(stations) == 0 {
|
|
|
|
|
fmt.Fprintln(os.Stderr, "no stations found to play")
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
playURL = stations[0].Url
|
|
|
|
|
fmt.Printf("Playing first match: %s\n", stations[0].Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use legacy for now (cleaned); will use richer player in TUI/IPC later.
|
|
|
|
|
pname := "mpv"
|
|
|
|
|
if v, err := config.Get("player.command"); err == nil && v != "" {
|
|
|
|
|
pname = v
|
|
|
|
|
}
|
|
|
|
|
opts := ""
|
|
|
|
|
if v, err := config.Get("player.options"); err == nil {
|
|
|
|
|
opts = v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
leg := playerpkg.NewLegacy(pname, opts)
|
|
|
|
|
if err := leg.Play(playURL); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "play error: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 20:29:11 +00:00
|
|
|
func main() {
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
// Early version / help for top level
|
|
|
|
|
if len(os.Args) > 1 {
|
|
|
|
|
switch os.Args[1] {
|
|
|
|
|
case "find":
|
|
|
|
|
runFind(os.Args[2:])
|
|
|
|
|
return
|
|
|
|
|
case "play":
|
|
|
|
|
runPlay(os.Args[2:])
|
|
|
|
|
return
|
|
|
|
|
case "-v", "--version", "version":
|
|
|
|
|
showVersion()
|
|
|
|
|
return
|
|
|
|
|
case "-h", "--help", "help":
|
|
|
|
|
printTopHelp()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-16 13:09:27 +00:00
|
|
|
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
// Common flags (used for TUI seeding or legacy)
|
2021-03-16 13:09:27 +00:00
|
|
|
var (
|
2024-07-09 20:29:11 +00:00
|
|
|
name string
|
2021-03-16 13:09:27 +00:00
|
|
|
country string
|
2024-07-09 20:29:11 +00:00
|
|
|
state string
|
|
|
|
|
tags string
|
2021-03-16 20:17:10 +00:00
|
|
|
notok bool
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
legacy bool
|
2024-07-09 20:29:11 +00:00
|
|
|
version bool
|
2021-03-16 13:09:27 +00:00
|
|
|
)
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
|
|
|
|
|
fs := flag.NewFlagSet("gostations", flag.ExitOnError)
|
|
|
|
|
fs.StringVar(&name, "n", "", "Station name (or identifier).")
|
|
|
|
|
fs.StringVar(&country, "c", "", "Home country.")
|
|
|
|
|
fs.StringVar(&state, "s", "", "Home state (if in the United States).")
|
|
|
|
|
fs.StringVar(&tags, "t", "", "Tag (or comma-separated tag list)")
|
|
|
|
|
fs.BoolVar(¬ok, "x", false, "If toggled, will show stations that are down")
|
|
|
|
|
fs.BoolVar(&legacy, "legacy", false, "Force old wmenu UI (kept until new TUI is perfect)")
|
|
|
|
|
fs.BoolVar(&version, "v", false, "Show version.")
|
|
|
|
|
|
|
|
|
|
fs.Usage = printTopHelp
|
|
|
|
|
|
|
|
|
|
_ = fs.Parse(os.Args[1:])
|
2021-03-18 21:14:53 +00:00
|
|
|
|
|
|
|
|
if version {
|
|
|
|
|
showVersion()
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
return
|
2021-03-18 21:14:53 +00:00
|
|
|
}
|
|
|
|
|
|
2021-03-17 07:17:09 +00:00
|
|
|
precheck()
|
2021-03-16 12:32:59 +00:00
|
|
|
|
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 20:10:54 +00:00
|
|
|
if err := config.Init(); err != nil {
|
|
|
|
|
fmt.Printf("config init: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stations, err := radio.Search(context.Background(), name, country, state, tags, notok)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("warning: station search: %v\n", err)
|
|
|
|
|
stations = nil
|
|
|
|
|
}
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
|
|
|
|
|
if legacy {
|
|
|
|
|
// Old wmenu path (gated)
|
|
|
|
|
menu := RadioMenu(stations)
|
|
|
|
|
if err := menu.Run(); err != nil {
|
|
|
|
|
fmt.Println(err.Error())
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default: new TUI (Bubble Tea)
|
|
|
|
|
if err := ui.Run(stations); err != nil {
|
|
|
|
|
fmt.Printf("TUI error: %v\n", err)
|
2021-03-16 23:06:20 +00:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func printTopHelp() {
|
|
|
|
|
fmt.Printf(`Usage:
|
|
|
|
|
gostations [options] # Launch interactive TUI (default)
|
|
|
|
|
gostations find [options] # Non-interactive station search (scripting)
|
|
|
|
|
gostations play [options] [url] # Direct playback (scripting)
|
|
|
|
|
gostations -h | --help
|
|
|
|
|
gostations -v | --version
|
|
|
|
|
|
|
|
|
|
Global options:
|
|
|
|
|
-n string Station name (or identifier).
|
|
|
|
|
-c string Home country.
|
|
|
|
|
-s string Home state (if in the United States).
|
|
|
|
|
-t string Tag (or comma-separated tag list)
|
|
|
|
|
-x If toggled, will show stations that are down
|
|
|
|
|
-v Show version.
|
|
|
|
|
--legacy Force old wmenu UI (temporary)
|
|
|
|
|
|
|
|
|
|
Subcommand examples:
|
|
|
|
|
gostations find -c "United Kingdom" -t "news" -j
|
|
|
|
|
gostations play -c "Gambia"
|
|
|
|
|
gostations play http://stream.example.com/radio.mp3
|
2021-03-16 14:46:28 +00:00
|
|
|
|
feat(cli): default to bubbles TUI, add find/play subcmds + JSON favorites
- implement internal/ui with bubbles list + ★ fav markers, filter, enter stub
- add data/favorites (JSON roundtrip, Add/Remove, XDG), config tests
- wire subcommands: `find -j`, `play [url|search]`
- gate legacy wmenu behind --legacy; keep old flags for seeding
- fix inverted -short guards, format string, go.mod deps
- add unit tests for radio prune, player IsInstalled, ui keys, etc.
2026-06-05 20:23:11 +00:00
|
|
|
Old top-level behavior without subcommand now defaults to the new TUI.
|
|
|
|
|
Use --legacy to force the classic wmenu flow.
|
|
|
|
|
`)
|
2024-07-09 20:29:11 +00:00
|
|
|
}
|