Bose SoundTouch Toolkit

Documentation for controlling and preserving Bose SoundTouch devices

View the Project on GitHub gesellix/Bose-SoundTouch

Navigation and Station Management Guide

Overview

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.

Table of Contents

Quick Start

Basic Setup

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...
}

Simple Navigation Example

// 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)
}

Content Navigation

Browse Different Sources

// 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()
}

Advanced Navigation with Pagination

// 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())
}

Search Result Analysis

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()
    }
}

Station Management

Adding Stations (Immediate Playback)

// 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

Removing Stations

// 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")
        }
    }
}

Station Collection Management

// 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!")

Complete Workflows

Discover and Play Workflow

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!")
            }
        }
    }
}

Library Organization Workflow

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!")
}

Multi-Service Content Discovery

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!")
}

Error Handling

Graceful Error Handling

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")
}

Retry Logic

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)
}

Validation and Safety

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)
        }
    }
}

Best Practices

1. Check Source Availability

// 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
    }
}

2. Use Pagination for Large Collections

// 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
        }
    }
}

3. Handle Service-Specific Behavior

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))
    }
}

4. Implement User-Friendly Interfaces

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()
    }
}

5. Performance Considerations

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)
        }
    }
}

API Reference

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

Search Methods

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

Station Management Methods

Method Description Parameters Returns
AddStation() Add station (plays immediately) source, account, token, name error
RemoveStation() Remove station from collection contentItem error

Response Helper Methods

SearchStationResponse Methods

SearchResult Methods

Common Source Types

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

Troubleshooting

Common Issues

β€œSource not available”

β€œNo results found”

β€œAddStation failed”

Navigation timeouts

Getting Help

For additional help:


This guide covers the complete navigation and station management functionality. For preset management, see PRESET-MANAGEMENT.md.