diff --git a/cmd/soundtouch-cli/cmd_station.go b/cmd/soundtouch-cli/cmd_station.go index 9d56e01..247c487 100644 --- a/cmd/soundtouch-cli/cmd_station.go +++ b/cmd/soundtouch-cli/cmd_station.go @@ -12,6 +12,12 @@ import ( // searchStations handles searching for stations across different sources func searchStations(c *cli.Context) error { + PrintDeprecation( + "station search", + "It asks the speaker to search, which fails when the speaker's cloud is gone.", + `soundtouch-cli station find --provider tunein --query ""`, + ) + source := c.String("source") sourceAccount := c.String("source-account") searchTerm := c.String("query") @@ -51,6 +57,12 @@ func searchStations(c *cli.Context) error { // searchTuneIn handles searching TuneIn specifically func searchTuneIn(c *cli.Context) error { + PrintDeprecation( + "station search-tunein", + "It asks the speaker to search, which fails when the speaker's cloud is gone.", + `soundtouch-cli station find-tunein --query ""`, + ) + searchTerm := c.String("query") if searchTerm == "" { @@ -86,6 +98,12 @@ func searchTuneIn(c *cli.Context) error { // searchPandora handles searching Pandora specifically func searchPandora(c *cli.Context) error { + PrintDeprecation( + "station search-pandora", + "There is no built-in Pandora search yet (it requires the speaker and your account).", + "", + ) + sourceAccount := c.String("source-account") searchTerm := c.String("query") @@ -127,6 +145,12 @@ func searchPandora(c *cli.Context) error { // searchSpotify handles searching Spotify specifically func searchSpotify(c *cli.Context) error { + PrintDeprecation( + "station search-spotify", + "There is no built-in Spotify search yet (it requires the speaker and your account).", + "", + ) + sourceAccount := c.String("source-account") searchTerm := c.String("query") @@ -531,31 +555,18 @@ func bmxNavCursor(section *models.BmxNavSection) string { return parsed.Query().Get("cursor") } -// searchService is the action for `station search` with --provider / --query / --more. -// It uses the service-side stations package (works even when the speaker's cloud is dead). -func searchService(c *cli.Context) error { - providerStr := c.String("provider") - query := c.String("query") - more := c.Bool("more") - +// runFind performs a built-in station search for the given provider and +// prints the results. The search runs inside the CLI itself, querying the +// radio provider's public API directly — it needs neither the speaker's +// cloud nor a running soundtouch-service. When more is true it follows up +// to three additional result pages while a next cursor is available. +func runFind(provider stations.Provider, label, query string, more bool) error { if query == "" { PrintError("Search query is required") return fmt.Errorf("search query cannot be empty") } - var provider stations.Provider - - switch strings.ToLower(providerStr) { - case "tunein": - provider = stations.ProviderTuneIn - case "radiobrowser": - provider = stations.ProviderRadioBrowser - default: - PrintError(fmt.Sprintf("Unknown provider %q: must be 'tunein' or 'radiobrowser'", providerStr)) - return fmt.Errorf("unknown provider: %s", providerStr) - } - - fmt.Printf("Searching %s for: %s\n", providerStr, query) + fmt.Printf("Searching %s for: %s\n", label, query) resp, err := stations.Search(provider, query) if err != nil { @@ -569,7 +580,6 @@ func searchService(c *cli.Context) error { return nil } - // Follow up to 3 additional pages while a next cursor is available. const maxExtraPages = 3 for page := 0; page < maxExtraPages; page++ { // Find a cursor from any section that has one. @@ -599,25 +609,36 @@ func searchService(c *cli.Context) error { return nil } -// searchServiceRadioBrowser is the action for `station search-radiobrowser`. -// It searches via the service-side RadioBrowser backend. -func searchServiceRadioBrowser(c *cli.Context) error { - query := c.String("query") +// findStations is the action for the unified `station find` with +// --provider / --query / --more. +func findStations(c *cli.Context) error { + providerStr := c.String("provider") - if query == "" { - PrintError("Search query is required") - return fmt.Errorf("search query cannot be empty") + var ( + provider stations.Provider + label string + ) + + switch strings.ToLower(providerStr) { + case "tunein": + provider, label = stations.ProviderTuneIn, "TuneIn" + case "radiobrowser": + provider, label = stations.ProviderRadioBrowser, "Radio Browser" + default: + PrintError(fmt.Sprintf("Unknown provider %q: must be 'tunein' or 'radiobrowser'", providerStr)) + return fmt.Errorf("unknown provider: %s", providerStr) } - fmt.Printf("Searching Radio Browser for: %s\n", query) + return runFind(provider, label, c.String("query"), c.Bool("more")) +} - resp, err := stations.Search(stations.ProviderRadioBrowser, query) - if err != nil { - PrintError(fmt.Sprintf("Search failed: %v", err)) - return err - } +// findTuneIn is the action for `station find-tunein` (built-in TuneIn search). +func findTuneIn(c *cli.Context) error { + return runFind(stations.ProviderTuneIn, "TuneIn", c.String("query"), c.Bool("more")) +} - printBmxNavResults(resp) - - return nil +// findRadioBrowser is the action for `station find-radiobrowser` +// (built-in Radio Browser search). +func findRadioBrowser(c *cli.Context) error { + return runFind(stations.ProviderRadioBrowser, "Radio Browser", c.String("query"), c.Bool("more")) } diff --git a/cmd/soundtouch-cli/common.go b/cmd/soundtouch-cli/common.go index 850d4a0..6bb2f9b 100644 --- a/cmd/soundtouch-cli/common.go +++ b/cmd/soundtouch-cli/common.go @@ -340,6 +340,22 @@ func PrintWarning(message string) { fmt.Printf("⚠️ %s\n", message) } +// PrintDeprecation prints a deprecation notice to stderr (so it does not +// pollute piped stdout output). reason explains why the command is going +// away; newUsage is an optional replacement example — pass "" when there +// is no replacement yet. +func PrintDeprecation(command, reason, newUsage string) { + fmt.Fprintf(os.Stderr, "⚠️ '%s' is deprecated and will be removed in a future release.\n", command) + + if reason != "" { + fmt.Fprintf(os.Stderr, " %s\n", reason) + } + + if newUsage != "" { + fmt.Fprintf(os.Stderr, " Use instead:\n %s\n", newUsage) + } +} + // showVersionInfo displays detailed version information including build details func showVersionInfo(_ *cli.Context) error { fmt.Printf("%s version %s\n", os.Args[0], version) diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index 2fdf0a4..e310418 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -589,9 +589,72 @@ func main() { Aliases: []string{"st"}, Usage: "Search and manage stations", Subcommands: []*cli.Command{ + // Built-in search ("find" family): runs inside the CLI, + // querying the radio provider's public API directly. No + // speaker cloud and no soundtouch-service required. + { + Name: "find", + Usage: "Find stations directly (built-in tunein or radiobrowser search; no speaker needed)", + Action: findStations, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "provider", + Usage: "Station provider: tunein or radiobrowser", + Value: "tunein", + }, + &cli.StringFlag{ + Name: "query", + Aliases: []string{"q"}, + Usage: "Search query", + Required: true, + }, + &cli.BoolFlag{ + Name: "more", + Usage: "Follow up to 3 additional result pages when available", + }, + }, + }, + { + Name: "find-tunein", + Usage: "Find TuneIn stations directly (built-in search; no speaker needed)", + Action: findTuneIn, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "query", + Aliases: []string{"q"}, + Usage: "Search query", + Required: true, + }, + &cli.BoolFlag{ + Name: "more", + Usage: "Follow up to 3 additional result pages when available", + }, + }, + }, + { + Name: "find-radiobrowser", + Usage: "Find Radio Browser stations directly (built-in search; no speaker needed)", + Action: findRadioBrowser, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "query", + Aliases: []string{"q"}, + Usage: "Search query", + Required: true, + }, + &cli.BoolFlag{ + Name: "more", + Usage: "Follow up to 3 additional result pages when available", + }, + }, + }, + // Deprecated speaker-based search commands. They ask the + // speaker to search, which fails once its cloud is gone. + // Prefer the "find" family above. Kept for now; each emits + // a deprecation notice on stderr. { Name: "search", - Usage: "Search for stations and content", + Usage: "[DEPRECATED] Search via the speaker; use 'station find' instead", Action: searchStations, Flags: []cli.Flag{ &cli.StringFlag{ @@ -612,31 +675,9 @@ func main() { }, Before: RequireHost, }, - { - Name: "find", - Usage: "Find stations via the AfterTouch service (tunein or radiobrowser)", - Action: searchService, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "provider", - Usage: "Station provider: tunein or radiobrowser", - Value: "tunein", - }, - &cli.StringFlag{ - Name: "query", - Aliases: []string{"q"}, - Usage: "Search query", - Required: true, - }, - &cli.BoolFlag{ - Name: "more", - Usage: "Follow up to 3 additional result pages when available", - }, - }, - }, { Name: "search-tunein", - Usage: "Search TuneIn stations", + Usage: "[DEPRECATED] Search TuneIn via the speaker; use 'station find-tunein' instead", Action: searchTuneIn, Flags: []cli.Flag{ &cli.StringFlag{ @@ -650,7 +691,7 @@ func main() { }, { Name: "search-pandora", - Usage: "Search Pandora stations", + Usage: "[DEPRECATED] Search Pandora via the speaker (no built-in equivalent yet)", Action: searchPandora, Flags: []cli.Flag{ &cli.StringFlag{ @@ -669,7 +710,7 @@ func main() { }, { Name: "search-spotify", - Usage: "Search Spotify content", + Usage: "[DEPRECATED] Search Spotify via the speaker (no built-in equivalent yet)", Action: searchSpotify, Flags: []cli.Flag{ &cli.StringFlag{ @@ -686,19 +727,6 @@ func main() { }, Before: RequireHost, }, - { - Name: "search-radiobrowser", - Usage: "Search Radio Browser stations via the AfterTouch service", - Action: searchServiceRadioBrowser, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "query", - Aliases: []string{"q"}, - Usage: "Search query", - Required: true, - }, - }, - }, { Name: "add", Usage: "Add station and play immediately", diff --git a/docs/content/docs/guides/CLI-REFERENCE.md b/docs/content/docs/guides/CLI-REFERENCE.md index f396423..c0be0d5 100644 --- a/docs/content/docs/guides/CLI-REFERENCE.md +++ b/docs/content/docs/guides/CLI-REFERENCE.md @@ -904,25 +904,63 @@ Search for and manage radio stations and streaming content. Search and manage stations. +##### Built-in search: the `find` family (recommended) + +The `find` commands run the search **inside the CLI itself**, querying the +radio provider's public API directly. They need **neither the speaker's +cloud nor a running `soundtouch-service`**, and they don't require a +reachable speaker (`--host`) to search — so they keep working even after +the speaker's original cloud is gone. This is the recommended way to +search. + +- `station find --provider tunein|radiobrowser --query ` — unified + built-in search. `--provider` defaults to `tunein`. +- `station find-tunein --query ` — TuneIn sibling + (= `find --provider tunein`). +- `station find-radiobrowser --query ` — Radio Browser sibling + (= `find --provider radiobrowser`). +- `… --more` — follow up to three additional result pages when available + (both TuneIn and Radio Browser paginate). + +Results include each station's playback `Location`, which you can feed to +`source tunein` (TuneIn) or a preset/play flow. + ```bash -# Search across any source +# Unified built-in search (no speaker required) +soundtouch-cli station find --provider tunein --query "jazz" + +# TuneIn sibling +soundtouch-cli station find-tunein --query "jazz" + +# Radio Browser, walking extra result pages +soundtouch-cli station find-radiobrowser --query "jazz" --more +``` + +##### Deprecated: speaker-based search + +These commands ask the **speaker** to search, which only works while the +speaker's cloud source is reachable. They are **deprecated** — each prints +a deprecation notice — and will be removed in a future release. Prefer the +`find` family above. There is no built-in equivalent for Pandora or +Spotify *yet*; those still require the speaker and your account. + +```bash +# [DEPRECATED] Search across any source via the speaker → use `station find` soundtouch-cli --host station search --source --query -# Search TuneIn specifically +# [DEPRECATED] Search TuneIn via the speaker → use `station find-tunein` soundtouch-cli --host station search-tunein --query -# Search Pandora specifically (requires account) +# [DEPRECATED] Search Pandora via the speaker (no built-in equivalent yet) soundtouch-cli --host station search-pandora --source-account --query -# Search Spotify specifically (requires account) +# [DEPRECATED] Search Spotify via the speaker (no built-in equivalent yet) soundtouch-cli --host station search-spotify --source-account --query +``` -# Find stations via the AfterTouch service (TuneIn or Radio Browser; no live cloud needed) -soundtouch-cli station find --provider --query [--more] - -# Search Radio Browser via the AfterTouch service -soundtouch-cli station search-radiobrowser --query +##### Manage stations +```bash # Add station and play immediately soundtouch-cli --host station add --source --token --name @@ -933,51 +971,6 @@ soundtouch-cli --host station remove --source --location station list --source [--source-account ] ``` -**Search Examples:** -```bash -# Search TuneIn for jazz stations -soundtouch-cli --host 192.0.2.10 station search-tunein --query "jazz" - -# Search Pandora for Taylor Swift -soundtouch-cli --host 192.0.2.10 station search-pandora --source-account myuser123 --query "Taylor Swift" - -# Search Spotify for workout playlists -soundtouch-cli --host 192.0.2.10 station search-spotify --source-account spotify_user --query "workout playlist" - -# General search across any source -soundtouch-cli --host 192.0.2.10 station search --source TUNEIN --query "classic rock" -``` - -##### Service-side search: `station find` and `station search-radiobrowser` - -The commands above (`search`, `search-tunein`, …) ask the **speaker** to -search, which only works while the speaker's cloud source is reachable. -`station find` and `station search-radiobrowser` instead run the search -**in AfterTouch itself**, so they work even when the speaker's original -cloud is gone — and they don't require a reachable speaker (`--host`) to -search: - -- `station find --provider tunein|radiobrowser --query ` — unified - service-side search. `--provider` defaults to `tunein`. -- `station find … --more` — follow up to three additional result pages - when more are available (Radio Browser and TuneIn both paginate). -- `station search-radiobrowser --query ` — Radio Browser sibling, - equivalent to `station find --provider radiobrowser`. - -Results include each station's playback `Location`, which you can feed to -`source tunein` (TuneIn) or a preset/play flow. - -```bash -# Service-side TuneIn search (no speaker required to search) -soundtouch-cli station find --provider tunein --query "jazz" - -# Service-side Radio Browser search, walking extra result pages -soundtouch-cli station find --provider radiobrowser --query "jazz" --more - -# Radio Browser sibling (same as: station find --provider radiobrowser) -soundtouch-cli station search-radiobrowser --query "VRT" -``` - **Station Management Examples:** ```bash # Add a station found from search results (use token from search output) @@ -1001,8 +994,8 @@ soundtouch-cli --host 192.0.2.10 station remove \ **Workflow Example - Discover and Play New Content:** ```bash -# 1. Search for content -soundtouch-cli --host 192.0.2.10 station search-tunein --query "smooth jazz" +# 1. Search for content (built-in, no speaker needed) +soundtouch-cli station find-tunein --query "smooth jazz" # 2. Add interesting station from results (copy token from output) soundtouch-cli --host 192.0.2.10 station add \