mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
`station find` previously surfaced the id only inside the Location href (e.g. /v1/playback/station/s228737). Render the bare id (s228737, p1864248, or radiobrowser UUID) alone in a leading column so it is easy to copy-paste, with the name beside it and the description plus full Location indented below. The Location line stays because that path, not the bare id, is what play/preset commands consume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
682 lines
18 KiB
Go
682 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/stations"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
// 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 "<your search>"`,
|
|
)
|
|
|
|
source := c.String("source")
|
|
sourceAccount := c.String("source-account")
|
|
searchTerm := c.String("query")
|
|
|
|
if searchTerm == "" {
|
|
PrintError("Search query is required")
|
|
return fmt.Errorf("search query cannot be empty")
|
|
}
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Searching %s for: %s", source, searchTerm), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Check service availability for the source
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
|
|
actionDescription := fmt.Sprintf("search %s stations", source)
|
|
if !checker.CheckSourceAvailable(source, actionDescription) {
|
|
return fmt.Errorf("source '%s' is not available for station search", source)
|
|
}
|
|
|
|
response, err := client.SearchStation(source, sourceAccount, searchTerm)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to search stations: %v", err))
|
|
return err
|
|
}
|
|
|
|
printSearchResults(response, searchTerm)
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 "<your search>"`,
|
|
)
|
|
|
|
searchTerm := c.String("query")
|
|
|
|
if searchTerm == "" {
|
|
PrintError("Search query is required")
|
|
return fmt.Errorf("search query cannot be empty")
|
|
}
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Searching TuneIn for: %s", searchTerm), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Check TuneIn availability
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
if !checker.ValidateTuneInAvailable("search TuneIn stations") {
|
|
return fmt.Errorf("TuneIn is not available on this device")
|
|
}
|
|
|
|
response, err := client.SearchTuneInStations(searchTerm)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to search TuneIn: %v", err))
|
|
return err
|
|
}
|
|
|
|
printSearchResults(response, searchTerm)
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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")
|
|
|
|
if sourceAccount == "" {
|
|
PrintError("Pandora source account is required")
|
|
return fmt.Errorf("source account required for Pandora")
|
|
}
|
|
|
|
if searchTerm == "" {
|
|
PrintError("Search query is required")
|
|
return fmt.Errorf("search query cannot be empty")
|
|
}
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Searching Pandora for: %s", searchTerm), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Check Pandora availability
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
if !checker.ValidatePandoraAvailable("search Pandora stations") {
|
|
return fmt.Errorf("pandora is not available on this device")
|
|
}
|
|
|
|
response, err := client.SearchPandoraStations(sourceAccount, searchTerm)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to search Pandora: %v", err))
|
|
return err
|
|
}
|
|
|
|
printSearchResults(response, searchTerm)
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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")
|
|
|
|
if sourceAccount == "" {
|
|
PrintError("Spotify source account is required")
|
|
return fmt.Errorf("source account required for Spotify")
|
|
}
|
|
|
|
if searchTerm == "" {
|
|
PrintError("Search query is required")
|
|
return fmt.Errorf("search query cannot be empty")
|
|
}
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Searching Spotify for: %s", searchTerm), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Check Spotify availability
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
if !checker.ValidateSpotifyAvailable("search Spotify content") {
|
|
return fmt.Errorf("spotify is not available on this device")
|
|
}
|
|
|
|
response, err := client.SearchSpotifyContent(sourceAccount, searchTerm)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to search Spotify: %v", err))
|
|
return err
|
|
}
|
|
|
|
printSearchResults(response, searchTerm)
|
|
|
|
return nil
|
|
}
|
|
|
|
// addStation handles adding a station and playing it immediately
|
|
func addStation(c *cli.Context) error {
|
|
source := c.String("source")
|
|
sourceAccount := c.String("source-account")
|
|
token := c.String("token")
|
|
name := c.String("name")
|
|
|
|
if source == "" {
|
|
PrintError("Source is required")
|
|
return fmt.Errorf("source cannot be empty")
|
|
}
|
|
|
|
if token == "" {
|
|
PrintError("Station token is required")
|
|
return fmt.Errorf("token cannot be empty")
|
|
}
|
|
|
|
if name == "" {
|
|
PrintError("Station name is required")
|
|
return fmt.Errorf("name cannot be empty")
|
|
}
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Adding %s station: %s", source, name), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Check service availability for the source
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
|
|
actionDescription := fmt.Sprintf("add %s station", source)
|
|
if !checker.CheckSourceAvailable(source, actionDescription) {
|
|
return fmt.Errorf("source '%s' is not available for adding stations", source)
|
|
}
|
|
|
|
err = client.AddStation(source, sourceAccount, token, name)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to add station: %v", err))
|
|
return err
|
|
}
|
|
|
|
PrintSuccess(fmt.Sprintf("Added and started playing station: %s", name))
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeStation handles removing a station from collections
|
|
func removeStation(c *cli.Context) error {
|
|
source := c.String("source")
|
|
location := c.String("location")
|
|
itemType := c.String("type")
|
|
sourceAccount := c.String("source-account")
|
|
|
|
if source == "" {
|
|
PrintError("Source is required")
|
|
return fmt.Errorf("source cannot be empty")
|
|
}
|
|
|
|
if location == "" {
|
|
PrintError("Station location is required")
|
|
return fmt.Errorf("location cannot be empty")
|
|
}
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Removing %s station", source), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Create content item for the station to remove
|
|
contentItem := &models.ContentItem{
|
|
Source: source,
|
|
Location: location,
|
|
Type: itemType,
|
|
SourceAccount: sourceAccount,
|
|
}
|
|
|
|
err = client.RemoveStation(contentItem)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to remove station: %v", err))
|
|
return err
|
|
}
|
|
|
|
PrintSuccess("Station removed successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
// printSearchResults formats and displays search results
|
|
func printSearchResults(response *models.SearchStationResponse, searchTerm string) {
|
|
fmt.Printf("Search Results for '%s':\n", searchTerm)
|
|
|
|
if response.IsEmpty() {
|
|
fmt.Printf(" No results found\n")
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" Total results: %d\n", response.GetResultCount())
|
|
|
|
// Group results by type for better display
|
|
songs := response.GetSongs()
|
|
artists := response.GetArtists()
|
|
stations := response.GetStations()
|
|
|
|
printSongs(songs)
|
|
printArtists(artists)
|
|
printStations(stations)
|
|
printSearchHints(response, songs, artists, stations)
|
|
}
|
|
|
|
// printSongs prints song search results
|
|
func printSongs(songs []models.SearchResult) {
|
|
if len(songs) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Printf("\n 🎵 Songs (%d):\n", len(songs))
|
|
|
|
for i := range songs {
|
|
song := &songs[i]
|
|
fmt.Printf(" %d. %s\n", i+1, song.GetDisplayName())
|
|
|
|
if song.Artist != "" {
|
|
fmt.Printf(" Artist: %s\n", song.Artist)
|
|
}
|
|
|
|
if song.Album != "" {
|
|
fmt.Printf(" Album: %s\n", song.Album)
|
|
}
|
|
|
|
if song.SourceAccount != "" {
|
|
fmt.Printf(" Account: %s\n", song.SourceAccount)
|
|
}
|
|
|
|
fmt.Printf(" Token: %s\n", song.Token)
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// printArtists prints artist search results
|
|
func printArtists(artists []models.SearchResult) {
|
|
if len(artists) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" 🎤 Artists (%d):\n", len(artists))
|
|
|
|
for i := range artists {
|
|
artist := &artists[i]
|
|
fmt.Printf(" %d. %s\n", i+1, artist.GetDisplayName())
|
|
|
|
if artist.SourceAccount != "" {
|
|
fmt.Printf(" Account: %s\n", artist.SourceAccount)
|
|
}
|
|
|
|
fmt.Printf(" Token: %s\n", artist.Token)
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// printStations prints station search results
|
|
func printStations(stations []models.SearchResult) {
|
|
if len(stations) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" 📻 Stations (%d):\n", len(stations))
|
|
|
|
for i := range stations {
|
|
station := &stations[i]
|
|
fmt.Printf(" %d. %s\n", i+1, station.GetDisplayName())
|
|
|
|
if station.SourceAccount != "" {
|
|
fmt.Printf(" Account: %s\n", station.SourceAccount)
|
|
}
|
|
|
|
fmt.Printf(" Token: %s\n", station.Token)
|
|
|
|
if station.Description != "" {
|
|
fmt.Printf(" Description: %s\n", station.Description)
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// printSearchHints prints usage hints for search results
|
|
func printSearchHints(response *models.SearchStationResponse, songs, artists, stations []models.SearchResult) {
|
|
fmt.Printf("💡 Usage hints:\n")
|
|
fmt.Printf(" • To add a station and play it: station add --source %s --token <token> --name <name>\n", response.Source)
|
|
|
|
if hasAccountResults(response) {
|
|
fmt.Printf(" • Include --source-account <account> when adding stations that require it\n")
|
|
}
|
|
|
|
if len(songs) > 0 || len(artists) > 0 || len(stations) > 0 {
|
|
fmt.Printf(" • Copy the token from results above to use with 'station add'\n")
|
|
}
|
|
}
|
|
|
|
// hasAccountResults checks if any results have source accounts
|
|
func hasAccountResults(response *models.SearchStationResponse) bool {
|
|
allResults := response.GetAllResults()
|
|
for i := range allResults {
|
|
if allResults[i].SourceAccount != "" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// listStations handles listing saved stations
|
|
func listStations(c *cli.Context) error {
|
|
source := c.String("source")
|
|
sourceAccount := c.String("source-account")
|
|
|
|
clientConfig := GetClientConfig(c)
|
|
PrintDeviceHeader(fmt.Sprintf("Getting %s stations", source), clientConfig.Host, clientConfig.Port)
|
|
|
|
client, err := CreateSoundTouchClient(clientConfig)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to create client: %v", err))
|
|
return err
|
|
}
|
|
|
|
// Check service availability for the source
|
|
checker := NewServiceAvailabilityChecker(client)
|
|
|
|
actionDescription := fmt.Sprintf("list %s stations", source)
|
|
if !checker.CheckSourceAvailable(source, actionDescription) {
|
|
return fmt.Errorf("source '%s' is not available for listing stations", source)
|
|
}
|
|
|
|
var response *models.NavigateResponse
|
|
|
|
switch strings.ToUpper(source) {
|
|
case "TUNEIN":
|
|
response, err = client.GetTuneInStations(sourceAccount)
|
|
case "PANDORA":
|
|
if sourceAccount == "" {
|
|
PrintError("Pandora source account is required")
|
|
return fmt.Errorf("source account required for Pandora")
|
|
}
|
|
|
|
response, err = client.GetPandoraStations(sourceAccount)
|
|
default:
|
|
return fmt.Errorf("listing stations is not supported for source: %s", source)
|
|
}
|
|
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to get stations: %v", err))
|
|
return err
|
|
}
|
|
|
|
printStationList(response, source)
|
|
|
|
return nil
|
|
}
|
|
|
|
// printStationList formats and displays saved station results
|
|
func printStationList(response *models.NavigateResponse, source string) {
|
|
fmt.Printf("Saved %s Stations:\n", source)
|
|
|
|
if response.TotalItems == 0 {
|
|
fmt.Printf(" No stations found\n")
|
|
return
|
|
}
|
|
|
|
stations := response.GetStations()
|
|
fmt.Printf(" Total stations: %d\n", response.TotalItems)
|
|
fmt.Printf(" Showing: %d\n\n", len(stations))
|
|
|
|
for i, station := range stations {
|
|
fmt.Printf(" %d. %s\n", i+1, station.Name)
|
|
|
|
if station.ContentItem != nil {
|
|
if station.ContentItem.Location != "" {
|
|
fmt.Printf(" Location: %s\n", station.ContentItem.Location)
|
|
}
|
|
|
|
if station.ContentItem.SourceAccount != "" {
|
|
fmt.Printf(" Account: %s\n", station.ContentItem.SourceAccount)
|
|
}
|
|
|
|
if station.ContentItem.IsPresetable {
|
|
fmt.Printf(" Can be saved as preset: Yes\n")
|
|
}
|
|
}
|
|
|
|
if station.Type != "" {
|
|
fmt.Printf(" Type: %s\n", station.Type)
|
|
}
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
// Show usage hints
|
|
fmt.Printf("💡 Usage hints:\n")
|
|
fmt.Printf(" • To play a station: Use the location value with 'play content' command\n")
|
|
fmt.Printf(" • To save as preset: Use 'preset set' command with the location\n")
|
|
}
|
|
|
|
// playbackID returns the bare station/episode id from a playback href,
|
|
// e.g. "/v1/playback/station/s228737" -> "s228737" and
|
|
// "/v1/playback/episodes/p1864248?encoded_name=…" -> "p1864248".
|
|
// Returns "" when href is empty.
|
|
func playbackID(href string) string {
|
|
if href == "" {
|
|
return ""
|
|
}
|
|
|
|
if i := strings.IndexByte(href, '?'); i >= 0 {
|
|
href = href[:i]
|
|
}
|
|
|
|
return path.Base(href)
|
|
}
|
|
|
|
// printBmxNavResults renders a *models.BmxNavResponse to stdout.
|
|
// For each section it prints the section name as a header, then each item as a
|
|
// leading id column followed by the name, with subtitle and playback location
|
|
// indented below. The bare id sits alone in its own column so it is easy to
|
|
// copy-paste.
|
|
func printBmxNavResults(resp *models.BmxNavResponse) {
|
|
if len(resp.BmxSections) == 0 {
|
|
fmt.Println(" No results found")
|
|
return
|
|
}
|
|
|
|
for _, section := range resp.BmxSections {
|
|
if section.Name != "" {
|
|
fmt.Printf("\n [%s]\n", section.Name)
|
|
}
|
|
|
|
if len(section.Items) == 0 {
|
|
fmt.Println(" (empty)")
|
|
continue
|
|
}
|
|
|
|
// Width of the leading id column = widest id in this section.
|
|
maxID := 0
|
|
|
|
for _, item := range section.Items {
|
|
if item.Links != nil && item.Links.BmxPlayback != nil {
|
|
maxID = max(maxID, len(playbackID(item.Links.BmxPlayback.Href)))
|
|
}
|
|
}
|
|
|
|
// Continuation lines align under the name: 4 leading spaces
|
|
// + id column + 2-space gap.
|
|
indent := strings.Repeat(" ", 4+maxID+2)
|
|
|
|
for _, item := range section.Items {
|
|
id := ""
|
|
if item.Links != nil && item.Links.BmxPlayback != nil {
|
|
id = playbackID(item.Links.BmxPlayback.Href)
|
|
}
|
|
|
|
fmt.Printf(" %-*s %s\n", maxID, id, item.Name)
|
|
|
|
if item.Subtitle != "" {
|
|
fmt.Printf("%s%s\n", indent, item.Subtitle)
|
|
}
|
|
|
|
if item.Links != nil && item.Links.BmxPlayback != nil {
|
|
fmt.Printf("%sLocation: %s\n", indent, item.Links.BmxPlayback.Href)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// bmxNavCursor extracts the opaque cursor value from a section's BmxNext link.
|
|
// The Href looks like "...?cursor=<value>"; this returns the cursor query param.
|
|
// Returns "" when no next link is present.
|
|
func bmxNavCursor(section *models.BmxNavSection) string {
|
|
if section == nil || section.Links == nil || section.Links.BmxNext == nil {
|
|
return ""
|
|
}
|
|
|
|
href := section.Links.BmxNext.Href
|
|
if href == "" {
|
|
return ""
|
|
}
|
|
|
|
// The cursor is the query parameter named "cursor".
|
|
parsed, err := url.Parse(href)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return parsed.Query().Get("cursor")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
fmt.Printf("Searching %s for: %s\n", label, query)
|
|
|
|
resp, err := stations.Search(provider, query)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Search failed: %v", err))
|
|
return err
|
|
}
|
|
|
|
printBmxNavResults(resp)
|
|
|
|
if !more {
|
|
return nil
|
|
}
|
|
|
|
const maxExtraPages = 3
|
|
for page := 0; page < maxExtraPages; page++ {
|
|
// Find a cursor from any section that has one.
|
|
cursor := ""
|
|
for i := range resp.BmxSections {
|
|
cursor = bmxNavCursor(&resp.BmxSections[i])
|
|
if cursor != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
if cursor == "" {
|
|
break
|
|
}
|
|
|
|
fmt.Printf("\n -- page %d --\n", page+2)
|
|
|
|
resp, err = stations.SearchNext(provider, cursor)
|
|
if err != nil {
|
|
PrintError(fmt.Sprintf("Failed to fetch next page: %v", err))
|
|
return err
|
|
}
|
|
|
|
printBmxNavResults(resp)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findStations is the action for the unified `station find` with
|
|
// --provider / --query / --more.
|
|
func findStations(c *cli.Context) error {
|
|
providerStr := c.String("provider")
|
|
|
|
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)
|
|
}
|
|
|
|
return runFind(provider, label, c.String("query"), c.Bool("more"))
|
|
}
|
|
|
|
// 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"))
|
|
}
|
|
|
|
// 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"))
|
|
}
|