Documentation for controlling and preserving Bose SoundTouch devices
The Bose SoundTouch Go client provides comprehensive navigation and station management functionality that allows you to:
This guide provides complete examples and best practices for using these features.
package main
import (
"fmt"
"log"
"github.com/gesellix/bose-soundtouch/pkg/client"
)
func main() {
// Create client
config := &client.Config{
Host: "192.168.1.100",
Port: 8090,
}
soundtouch := client.NewClient(config)
// Your navigation code here...
}
// Browse TuneIn content
response, err := soundtouch.Navigate("TUNEIN", "", 1, 25)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d items\n", response.TotalItems)
for _, item := range response.Items {
fmt.Printf("- %s (%s)\n", item.GetDisplayName(), item.Type)
}
// Browse TuneIn radio stations
tuneInStations, err := soundtouch.GetTuneInStations("")
if err != nil {
log.Printf("TuneIn not available: %v", err)
} else {
fmt.Printf("TuneIn has %d items\n", tuneInStations.TotalItems)
}
// Browse Pandora stations (requires account)
pandoraStations, err := soundtouch.GetPandoraStations("your_pandora_account")
if err != nil {
log.Printf("Pandora not available: %v", err)
} else {
stations := pandoraStations.GetStations()
fmt.Printf("Found %d Pandora stations\n", len(stations))
}
// Browse stored music library
musicLibrary, err := soundtouch.GetStoredMusicLibrary("device_account/0")
if err != nil {
log.Printf("Stored music not available: %v", err)
} else {
directories := musicLibrary.GetDirectories()
tracks := musicLibrary.GetTracks()
fmt.Printf("Music library: %d dirs, %d tracks\n", len(directories), len(tracks))
}
// First, get the root level
musicLibrary, err := soundtouch.GetStoredMusicLibrary("device_account/0")
if err != nil {
log.Fatal(err)
}
// Find a directory to browse into
directories := musicLibrary.GetDirectories()
if len(directories) == 0 {
fmt.Println("No directories found")
return
}
// Navigate into the first directory
directory := directories[0]
fmt.Printf("Browsing into: %s\n", directory.GetDisplayName())
contents, err := soundtouch.NavigateContainer(
"STORED_MUSIC",
"device_account/0",
1, 100, // Get up to 100 items starting from position 1
directory.ContentItem,
)
if err != nil {
log.Fatal(err)
}
// Show what's inside
tracks := contents.GetTracks()
subdirs := contents.GetDirectories()
fmt.Printf("Found %d tracks and %d subdirectories\n", len(tracks), len(subdirs))
// List first few tracks
for i, track := range tracks[:min(5, len(tracks))] {
fmt.Printf("%d. %s", i+1, track.GetDisplayName())
if track.ArtistName != "" {
fmt.Printf(" - %s", track.ArtistName)
}
if track.AlbumName != "" {
fmt.Printf(" [%s]", track.AlbumName)
}
fmt.Println()
}
// Browse large collections with pagination
const pageSize = 50
startItem := 1
for {
response, err := soundtouch.Navigate("STORED_MUSIC", "device/0", startItem, pageSize)
if err != nil {
log.Fatal(err)
}
if len(response.Items) == 0 {
break // No more items
}
fmt.Printf("Page starting at %d: %d items\n", startItem, len(response.Items))
// Process this page
for _, item := range response.Items {
fmt.Printf(" %s (%s)\n", item.GetDisplayName(), item.Type)
}
// Move to next page
startItem += pageSize
// Stop if we've seen all items
if startItem > response.TotalItems {
break
}
}
// Search TuneIn for jazz stations
results, err := soundtouch.SearchTuneInStations("jazz")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d total results for 'jazz'\n", results.GetResultCount())
// Show different types of results
songs := results.GetSongs()
artists := results.GetArtists()
stations := results.GetStations()
fmt.Printf("Songs: %d, Artists: %d, Stations: %d\n",
len(songs), len(artists), len(stations))
// Search Pandora (requires account)
pandoraResults, err := soundtouch.SearchPandoraStations("your_account", "Taylor Swift")
if err != nil {
log.Fatal(err)
}
// Show artists found
artists := pandoraResults.GetArtists()
for _, artist := range artists {
fmt.Printf("Artist: %s (Token: %s)\n", artist.Name, artist.Token)
if artist.Logo != "" {
fmt.Printf(" Artwork: %s\n", artist.GetArtworkURL())
}
}
// Search Spotify content
spotifyResults, err := soundtouch.SearchSpotifyContent("your_spotify_account", "Queen")
if err != nil {
log.Fatal(err)
}
songs := spotifyResults.GetSongs()
for _, song := range songs[:min(5, len(songs))] {
fmt.Printf("Song: %s\n", song.GetFullTitle())
}
results, err := soundtouch.SearchPandoraStations("account", "classic rock")
if err != nil {
log.Fatal(err)
}
// Analyze all results
for _, result := range results.GetAllResults() {
fmt.Printf("Name: %s, Token: %s\n", result.GetDisplayName(), result.Token)
// Determine result type
switch {
case result.IsSong():
fmt.Printf(" Type: Song by %s\n", result.Artist)
case result.IsArtist():
fmt.Printf(" Type: Artist\n")
case result.IsStation():
fmt.Printf(" Type: Station")
if result.Description != "" {
fmt.Printf(" - %s", result.Description)
}
fmt.Println()
}
}
// Search for content first
results, err := soundtouch.SearchPandoraStations("your_account", "Led Zeppelin")
if err != nil {
log.Fatal(err)
}
// Find an artist to create a station from
artists := results.GetArtists()
if len(artists) == 0 {
fmt.Println("No artists found")
return
}
artist := artists[0]
stationName := artist.Name + " Radio"
// Add station - this immediately starts playing it!
err = soundtouch.AddStation("PANDORA", "your_account", artist.Token, stationName)
if err != nil {
log.Fatal(err)
}
fmt.Printf("β Added and now playing: %s\n", stationName)
// The station is now:
// 1. Added to your Pandora collection
// 2. Currently playing on the device
// First, get existing stations
stations, err := soundtouch.GetPandoraStations("your_account")
if err != nil {
log.Fatal(err)
}
// Show current stations
fmt.Printf("Current stations (%d):\n", len(stations.Items))
for i, station := range stations.Items {
fmt.Printf("%d. %s\n", i+1, station.GetDisplayName())
}
// Remove a specific station (example: remove the first one)
if len(stations.Items) > 0 {
stationToRemove := stations.Items[0]
if stationToRemove.ContentItem != nil {
fmt.Printf("Removing: %s\n", stationToRemove.GetDisplayName())
err := soundtouch.RemoveStation(stationToRemove.ContentItem)
if err != nil {
log.Printf("Failed to remove station: %v", err)
} else {
fmt.Println("β Station removed successfully")
}
}
}
// Get current collection
currentStations, err := soundtouch.GetPandoraStations("your_account")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Current collection has %d stations\n", len(currentStations.Items))
// Search for new content
searchResults, err := soundtouch.SearchPandoraStations("your_account", "indie rock")
if err != nil {
log.Fatal(err)
}
// Add top 3 artist stations
artists := searchResults.GetArtists()
for i, artist := range artists[:min(3, len(artists))] {
stationName := fmt.Sprintf("%s Radio", artist.Name)
fmt.Printf("Adding station %d: %s\n", i+1, stationName)
err := soundtouch.AddStation("PANDORA", "your_account", artist.Token, stationName)
if err != nil {
log.Printf("Failed to add %s: %v", stationName, err)
continue
}
fmt.Printf("β Added: %s\n", stationName)
// Note: Each AddStation immediately starts playing that station
// You might want to pause between additions in a real app
}
fmt.Println("Station collection updated!")
func discoverAndPlayWorkflow(soundtouch *client.Client) {
fmt.Println("=== Discover and Play Workflow ===")
// Step 1: Search for content
searchTerm := "electronic music"
fmt.Printf("π Searching for '%s'...\n", searchTerm)
results, err := soundtouch.SearchTuneInStations(searchTerm)
if err != nil {
log.Fatal(err)
}
if results.IsEmpty() {
fmt.Println("β No results found")
return
}
// Step 2: Show options
stations := results.GetStations()
fmt.Printf("π» Found %d stations:\n", len(stations))
for i, station := range stations[:min(5, len(stations))] {
fmt.Printf("%d. %s", i+1, station.GetDisplayName())
if station.Description != "" {
fmt.Printf(" - %s", station.Description)
}
fmt.Println()
}
// Step 3: Select and play (example: select first one)
if len(stations) > 0 {
selectedStation := stations[0]
fmt.Printf("π΅ Playing: %s\n", selectedStation.GetDisplayName())
// For services that support it, add the station to play it
if selectedStation.Token != "" {
err := soundtouch.AddStation("TUNEIN", "", selectedStation.Token, selectedStation.Name)
if err != nil {
log.Printf("Could not add station: %v", err)
} else {
fmt.Println("β Station added and playing!")
}
}
}
}
func organizeLibraryWorkflow(soundtouch *client.Client, deviceAccount string) {
fmt.Println("=== Library Organization Workflow ===")
// Step 1: Explore library structure
fmt.Println("π Exploring music library...")
library, err := soundtouch.GetStoredMusicLibrary(deviceAccount)
if err != nil {
log.Fatal(err)
}
directories := library.GetDirectories()
tracks := library.GetTracks()
fmt.Printf("π Library overview: %d directories, %d tracks\n",
len(directories), len(tracks))
// Step 2: Navigate into each directory
for _, dir := range directories[:min(3, len(directories))] {
fmt.Printf("\nπ Exploring: %s\n", dir.GetDisplayName())
contents, err := soundtouch.NavigateContainer(
"STORED_MUSIC", deviceAccount, 1, 20, dir.ContentItem)
if err != nil {
log.Printf("β Failed to explore %s: %v", dir.GetDisplayName(), err)
continue
}
subTracks := contents.GetTracks()
subDirs := contents.GetDirectories()
fmt.Printf(" Contains: %d tracks, %d subdirectories\n",
len(subTracks), len(subDirs))
// Show some tracks
for i, track := range subTracks[:min(3, len(subTracks))] {
fmt.Printf(" %d. %s", i+1, track.GetDisplayName())
if track.ArtistName != "" {
fmt.Printf(" - %s", track.ArtistName)
}
fmt.Println()
}
}
fmt.Println("\nβ Library exploration complete!")
}
func multiServiceDiscovery(soundtouch *client.Client, accounts map[string]string) {
searchTerm := "jazz"
fmt.Printf("π Searching '%s' across all services...\n", searchTerm)
// Search TuneIn (no account needed)
fmt.Println("\nπ» TuneIn Results:")
tuneInResults, err := soundtouch.SearchTuneInStations(searchTerm)
if err != nil {
fmt.Printf("β TuneIn search failed: %v\n", err)
} else {
stations := tuneInResults.GetStations()
fmt.Printf("β Found %d TuneIn stations\n", len(stations))
for i, station := range stations[:min(3, len(stations))] {
fmt.Printf(" %d. %s\n", i+1, station.GetDisplayName())
}
}
// Search Pandora (if account available)
if pandoraAccount, ok := accounts["PANDORA"]; ok {
fmt.Println("\nπ΅ Pandora Results:")
pandoraResults, err := soundtouch.SearchPandoraStations(pandoraAccount, searchTerm)
if err != nil {
fmt.Printf("β Pandora search failed: %v\n", err)
} else {
artists := pandoraResults.GetArtists()
stations := pandoraResults.GetStations()
fmt.Printf("β Found %d artists, %d stations\n", len(artists), len(stations))
for i, artist := range artists[:min(2, len(artists))] {
fmt.Printf(" Artist: %s\n", artist.GetDisplayName())
}
}
}
// Search Spotify (if account available)
if spotifyAccount, ok := accounts["SPOTIFY"]; ok {
fmt.Println("\nπΌ Spotify Results:")
spotifyResults, err := soundtouch.SearchSpotifyContent(spotifyAccount, searchTerm)
if err != nil {
fmt.Printf("β Spotify search failed: %v\n", err)
} else {
songs := spotifyResults.GetSongs()
fmt.Printf("β Found %d songs\n", len(songs))
for i, song := range songs[:min(2, len(songs))] {
fmt.Printf(" Song: %s\n", song.GetFullTitle())
}
}
}
fmt.Println("\nβ Multi-service discovery complete!")
}
func robustNavigation(soundtouch *client.Client) error {
// Try multiple sources gracefully
sources := []string{"TUNEIN", "SPOTIFY", "STORED_MUSIC"}
for _, source := range sources {
fmt.Printf("Trying %s...\n", source)
response, err := soundtouch.Navigate(source, "", 1, 10)
if err != nil {
fmt.Printf("β %s failed: %v\n", source, err)
continue
}
if response.IsEmpty() {
fmt.Printf("β οΈ %s has no content\n", source)
continue
}
fmt.Printf("β %s available with %d items\n", source, response.TotalItems)
return nil
}
return fmt.Errorf("no sources available")
}
func searchWithRetry(soundtouch *client.Client, maxRetries int) (*models.SearchStationResponse, error) {
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
fmt.Printf("Search attempt %d/%d...\n", attempt, maxRetries)
results, err := soundtouch.SearchTuneInStations("classical")
if err == nil {
return results, nil
}
lastErr = err
fmt.Printf("β Attempt %d failed: %v\n", attempt, err)
if attempt < maxRetries {
time.Sleep(time.Duration(attempt) * time.Second)
}
}
return nil, fmt.Errorf("search failed after %d attempts: %w", maxRetries, lastErr)
}
func safeStationManagement(soundtouch *client.Client, pandoraAccount string) {
// Always validate inputs
if pandoraAccount == "" {
log.Fatal("Pandora account required")
}
// Search safely
results, err := soundtouch.SearchPandoraStations(pandoraAccount, "blues")
if err != nil {
log.Fatal(err)
}
if results.IsEmpty() {
fmt.Println("No results found")
return
}
// Check what we have before adding stations
artists := results.GetArtists()
if len(artists) == 0 {
fmt.Println("No artists found to create stations from")
return
}
// Get current stations to avoid duplicates
currentStations, err := soundtouch.GetPandoraStations(pandoraAccount)
if err != nil {
log.Printf("Warning: Could not get current stations: %v", err)
}
// Create a map of existing station names
existingStations := make(map[string]bool)
for _, station := range currentStations.Items {
existingStations[station.GetDisplayName()] = true
}
// Add stations only if they don't exist
for _, artist := range artists[:min(2, len(artists))] {
stationName := artist.Name + " Radio"
if existingStations[stationName] {
fmt.Printf("β οΈ Station already exists: %s\n", stationName)
continue
}
fmt.Printf("Adding new station: %s\n", stationName)
err := soundtouch.AddStation("PANDORA", pandoraAccount, artist.Token, stationName)
if err != nil {
log.Printf("β Failed to add %s: %v", stationName, err)
} else {
fmt.Printf("β Added: %s\n", stationName)
}
}
}
// Always check what sources are available first
sources, err := soundtouch.GetSources()
if err != nil {
return err
}
// Check if TuneIn is ready
for _, source := range sources.SourceItem {
if source.Source == "TUNEIN" && source.Status.IsReady() {
// TuneIn is available
break
}
}
// For large libraries, use pagination
const batchSize = 50
func processLargeLibrary(soundtouch *client.Client, sourceAccount string) {
startItem := 1
for {
batch, err := soundtouch.Navigate("STORED_MUSIC", sourceAccount, startItem, batchSize)
if err != nil {
log.Printf("Error at position %d: %v", startItem, err)
break
}
if len(batch.Items) == 0 {
break // No more items
}
// Process this batch
processBatch(batch.Items)
startItem += batchSize
// Prevent infinite loops
if startItem > batch.TotalItems {
break
}
}
}
func handleServiceDifferences(soundtouch *client.Client) {
// TuneIn: Usually no account needed
tuneInStations, err := soundtouch.SearchTuneInStations("news")
if err == nil {
fmt.Printf("TuneIn: %d stations\n", len(tuneInStations.GetStations()))
}
// Pandora: Requires user account
pandoraResults, err := soundtouch.SearchPandoraStations("user_account", "rock")
if err == nil {
// Pandora returns artists you can create stations from
artists := pandoraResults.GetArtists()
fmt.Printf("Pandora: %d artists\n", len(artists))
}
// Spotify: Requires user account, returns tracks/playlists
spotifyResults, err := soundtouch.SearchSpotifyContent("spotify_user", "pop")
if err == nil {
songs := spotifyResults.GetSongs()
fmt.Printf("Spotify: %d songs\n", len(songs))
}
}
func userFriendlySearch(soundtouch *client.Client, searchTerm string) {
fmt.Printf("π Searching for '%s'...\n", searchTerm)
results, err := soundtouch.SearchTuneInStations(searchTerm)
if err != nil {
fmt.Printf("β Search failed: %v\n", err)
return
}
if results.IsEmpty() {
fmt.Printf("π No results found for '%s'\n", searchTerm)
fmt.Println("π‘ Try different search terms like:")
fmt.Println(" - Genre names: jazz, rock, classical")
fmt.Println(" - Artist names: Beatles, Mozart")
fmt.Println(" - Station types: news, talk, music")
return
}
stations := results.GetStations()
fmt.Printf("π΅ Found %d stations:\n", len(stations))
for i, station := range stations {
fmt.Printf("%d. π» %s", i+1, station.GetDisplayName())
if station.Description != "" {
fmt.Printf("\n %s", station.Description)
}
if station.GetArtworkURL() != "" {
fmt.Printf("\n π¨ %s", station.GetArtworkURL())
}
fmt.Println()
}
}
func efficientBrowsing(soundtouch *client.Client) {
// Use reasonable page sizes
const optimalPageSize = 25 // Good balance of network efficiency and memory usage
// Cache frequently accessed data
var cachedSources *models.Sources
getSources := func() (*models.Sources, error) {
if cachedSources == nil {
var err error
cachedSources, err = soundtouch.GetSources()
return cachedSources, err
}
return cachedSources, nil
}
// Use the cached sources
sources, err := getSources()
if err != nil {
return
}
// Process efficiently
for _, source := range sources.SourceItem {
if source.Status.IsReady() {
// Only browse ready sources
procesReadySource(soundtouch, source.Source, source.SourceAccount)
}
}
}
| Method | Description | Parameters | Returns |
|---|---|---|---|
Navigate() |
Browse content source | source, account, start, count | NavigateResponse |
NavigateWithMenu() |
Browse with menu/sort | source, account, menu, sort, start, count | NavigateResponse |
NavigateContainer() |
Browse into directory | source, account, start, count, container | NavigateResponse |
GetTuneInStations() |
Convenience for TuneIn | account | NavigateResponse |
GetPandoraStations() |
Convenience for Pandora | account | NavigateResponse |
GetStoredMusicLibrary() |
Convenience for stored music | account | NavigateResponse |
| Method | Description | Parameters | Returns |
|---|---|---|---|
SearchStation() |
Generic station search | source, account, term | SearchStationResponse |
SearchTuneInStations() |
Search TuneIn | term | SearchStationResponse |
SearchPandoraStations() |
Search Pandora | account, term | SearchStationResponse |
SearchSpotifyContent() |
Search Spotify | account, term | SearchStationResponse |
| Method | Description | Parameters | Returns |
|---|---|---|---|
AddStation() |
Add station (plays immediately) | source, account, token, name | error |
RemoveStation() |
Remove station from collection | contentItem | error |
GetPlayableItems() - Filter playable itemsGetDirectories() - Filter directoriesGetTracks() - Filter music tracksGetStations() - Filter radio stationsIsEmpty() - Check if response has no itemsGetSongs() - Filter song resultsGetArtists() - Filter artist resultsGetStations() - Filter station resultsGetAllResults() - Get all results combinedGetResultCount() - Count total resultsHasResults() - Check if any results foundIsEmpty() - Check if no resultsIsSong() - Check if result is a songIsArtist() - Check if result is an artistIsStation() - Check if result is a stationGetDisplayName() - Get formatted nameGetFullTitle() - Get name with artist (for songs)GetArtworkURL() - Get artwork/logo URL| Source | Description | Account Required | Search Support |
|---|---|---|---|
TUNEIN |
Internet radio stations | No | Yes |
PANDORA |
Pandora music service | Yes | Yes |
SPOTIFY |
Spotify music service | Yes | Yes |
STORED_MUSIC |
Local/network music | Device account | No |
BLUETOOTH |
Bluetooth audio input | No | No |
AUX |
Auxiliary input | No | No |
βSource not availableβ
GetSources() to see whatβs actually availableβNo results foundβ
βAddStation failedβ
Navigation timeouts
For additional help:
This guide covers the complete navigation and station management functionality. For preset management, see PRESET-MANAGEMENT.md.