Bose SoundTouch Toolkit

Documentation for controlling and preserving Bose SoundTouch devices

View the Project on GitHub gesellix/Bose-SoundTouch

Preset Management - Bose SoundTouch API

This document covers preset management functionality in the Bose SoundTouch API client.

Overview

Bose SoundTouch devices support up to 6 presets that can store favorite music sources, playlists, radio stations, and other audio content. The API provides comprehensive read and write access to preset information through both official endpoints and reverse-engineered preset management functionality.

Current Implementation Status

Fully Supported - Read Operations

Not Supported - Write Operations

Supported Alternatives

Reading Presets

CLI Usage

# Get all configured presets
soundtouch-cli -host 192.168.1.10 -presets

Example Output:

Configured Presets:
  Used Slots: 6/6
  Spotify Presets: 6

Preset 1: My Favorite Playlist
  Source: SPOTIFY (user@example.com)
  Type: tracklisturl
  Created: 2024-04-30 07:37:40
  Updated: 2024-04-30 07:37:40
  Artwork: https://i.scdn.co/image/...

Preset 2: Rock Hits Radio
  Source: TUNEIN
  Type: station
  Artwork: https://cdn-radiotime-logos.tunein.com/...

Available Slots: []
Most Recent: Preset 1 (My Favorite Playlist)

Go Library Usage

package main

import (
    "fmt"
    "github.com/gesellix/bose-soundtouch/pkg/client"
)

func main() {
    // Create client
    soundtouchClient := client.NewClientFromHost("192.168.1.10")
    
    // Get all presets
    presets, err := soundtouchClient.GetPresets()
    if err != nil {
        panic(err)
    }
    
    // Display preset information
    fmt.Printf("Total presets: %d\n", presets.GetPresetCount())
    fmt.Printf("Used slots: %d\n", len(presets.GetUsedPresetSlots()))
    fmt.Printf("Empty slots: %v\n", presets.GetEmptyPresetSlots())
    
    // Check for Spotify presets
    spotifyPresets := presets.GetSpotifyPresets()
    fmt.Printf("Spotify presets: %d\n", len(spotifyPresets))
    
    // Get specific preset
    preset1 := presets.GetPresetByID(1)
    if preset1 != nil && !preset1.IsEmpty() {
        fmt.Printf("Preset 1: %s\n", preset1.GetDisplayName())
        fmt.Printf("  Source: %s\n", preset1.GetSource())
        fmt.Printf("  Type: %s\n", preset1.GetContentType())
        
        if preset1.HasTimestamps() {
            fmt.Printf("  Created: %s\n", preset1.GetCreatedTime())
            fmt.Printf("  Updated: %s\n", preset1.GetUpdatedTime())
        }
    }
    
    // Find most recent preset
    if recent := presets.GetMostRecentPreset(); recent != nil {
        fmt.Printf("Most recent: Preset %d (%s)\n", 
            recent.ID, recent.GetDisplayName())
    }
}

Preset Data Structure

XML Response Format

<presets>
  <preset id="1" createdOn="1745991460" updatedOn="1745991460">
    <ContentItem source="SPOTIFY" type="tracklisturl" 
                 location="/playback/container/..." 
                 sourceAccount="user@example.com" 
                 isPresetable="true">
      <itemName>My Favorite Songs</itemName>
      <containerArt>https://i.scdn.co/image/...</containerArt>
    </ContentItem>
  </preset>
  <preset id="2">
    <ContentItem source="TUNEIN" type="station" 
                 location="s12345" isPresetable="true">
      <itemName>Classic Rock Radio</itemName>
      <containerArt>https://cdn-radiotime-logos.tunein.com/...</containerArt>
    </ContentItem>
  </preset>
</presets>

Go Model

type Presets struct {
    XMLName xml.Name `xml:"presets"`
    Preset  []Preset `xml:"preset"`
}

type Preset struct {
    XMLName     xml.Name     `xml:"preset"`
    ID          int          `xml:"id,attr"`
    CreatedOn   *int64       `xml:"createdOn,attr,omitempty"`
    UpdatedOn   *int64       `xml:"updatedOn,attr,omitempty"`
    ContentItem *ContentItem `xml:"ContentItem,omitempty"`
}

Helper Methods

Preset Analysis

// Get preset by ID
preset := presets.GetPresetByID(3)

// Check if preset has content
if !preset.IsEmpty() {
    // Use preset
}

// Get presets by source type
spotifyPresets := presets.GetPresetsBySource("SPOTIFY")
tuneInPresets := presets.GetPresetsBySource("TUNEIN")

// Find available slots
emptySlots := presets.GetEmptyPresetSlots()  // Returns [4, 5] if slots 4-5 are empty
usedSlots := presets.GetUsedPresetSlots()    // Returns [1, 2, 3, 6] if those are used

Preset Content Analysis

// Get display information
name := preset.GetDisplayName()         // "My Playlist" or "Preset 1" fallback
source := preset.GetSource()           // "SPOTIFY", "TUNEIN", etc.
account := preset.GetSourceAccount()   // "user@example.com"
contentType := preset.GetContentType() // "playlist", "station", etc.
artwork := preset.GetArtworkURL()      // Album/station artwork URL

// Check preset characteristics
isSpotify := preset.IsSpotifyPreset()
isPresetable := preset.IsPresetable()

// Time information (if available)
if preset.HasTimestamps() {
    created := preset.GetCreatedTime()
    updated := preset.GetUpdatedTime()
}

Content Type Examples

Common content types found in presets:

Source Type Description Example Location
SPOTIFY tracklisturl Playlist/Album /playback/container/c3Bv...
SPOTIFY track Single Track /playback/container/c3Bv...
TUNEIN station Radio Station s12345
PANDORA station Pandora Station TR:station:12345
AMAZON playlist Amazon Playlist amzn1.dv.gti...

Preset Selection

While you cannot create presets via API, you can select existing presets:

Via Key Commands

# Select preset 1-6 using key commands
soundtouch-cli -host 192.168.1.10 -preset 1
soundtouch-cli -host 192.168.1.10 -key PRESET_3

Via Go Library

// Select preset using key command
err := soundtouchClient.SelectPreset(1)

// Or use direct key command
err := soundtouchClient.SendKey("PRESET_1")

Implementation Details

SoundTouch Plus Wiki Documented Endpoints

Despite official documentation marking POST /presets as “N/A”, we discovered working preset management endpoints through the comprehensive SoundTouch Plus Wiki:

  1. POST /storePreset - Fully functional preset creation and updating
  2. POST /removePreset - Complete preset deletion and slot clearing
  3. Full content source support - Spotify playlists, TuneIn stations, local music libraries
  4. Real-time events - Generates WebSocket presetsUpdated notifications
  5. Tested extensively - Works reliably with SoundTouch 10 and SoundTouch 20 devices

Current Capabilities

Working Alternatives

1. Official Bose SoundTouch App

2. Physical Device Controls

3. Web Interface (if available)

Checking Presetability

Before attempting to save content as a preset (via app/hardware), you can check if the current content supports preset saving:

// Check if currently playing content can be saved as preset
nowPlaying, err := client.GetNowPlaying()
if err != nil {
    return err
}

if nowPlaying.ContentItem != nil && nowPlaying.ContentItem.IsPresetable {
    fmt.Println("✓ Current content can be saved as a preset")
    fmt.Printf("  Content: %s\n", nowPlaying.ContentItem.ItemName)
    fmt.Printf("  Source: %s\n", nowPlaying.ContentItem.Source)
    fmt.Printf("  Type: %s\n", nowPlaying.ContentItem.Type)
} else {
    fmt.Println("✗ Current content cannot be saved as a preset")
}

Or use the convenience method:

// Simple presetability check
presetable, err := client.IsCurrentContentPresetable()
if err != nil {
    return err
}

if presetable {
    fmt.Println("✓ Content is presetable - use app or device buttons to save")
} else {
    fmt.Println("✗ Content cannot be saved as preset")
}

Best Practices

1. Check Available Slots

presets, err := client.GetPresets()
if err != nil {
    return err
}

emptySlots := presets.GetEmptyPresetSlots()
if len(emptySlots) == 0 {
    fmt.Println("All preset slots are occupied")
    // Consider which preset to overwrite
} else {
    fmt.Printf("Available preset slots: %v\n", emptySlots)
}

2. Analyze Current Presets

// Get summary statistics
summary := presets.GetPresetsSummary()
fmt.Printf("Total: %d, Used: %d, Empty: %d\n", 
    summary["total"], summary["used"], summary["empty"])

// Check source distribution
if summary["SPOTIFY"] > 0 {
    fmt.Printf("Spotify presets: %d\n", summary["SPOTIFY"])
}
if summary["TUNEIN"] > 0 {
    fmt.Printf("TuneIn presets: %d\n", summary["TUNEIN"])
}

3. Handle Preset History

// Find recently used presets
if recent := presets.GetMostRecentPreset(); recent != nil {
    fmt.Printf("Most recently updated: Preset %d (%s)\n", 
        recent.ID, recent.GetDisplayName())
}

if oldest := presets.GetOldestPreset(); oldest != nil {
    fmt.Printf("Oldest preset: Preset %d (%s)\n", 
        oldest.ID, oldest.GetDisplayName())
}

Implementation Achievement

SoundTouch Plus Wiki Discovery Success

Despite the official Bose SoundTouch API documentation marking preset creation as “not supported”, we discovered working preset management endpoints through the SoundTouch Plus Wiki:

  1. POST /storePreset - Complete preset creation and updating functionality
  2. POST /removePreset - Full preset deletion and clearing capability
  3. Full compatibility - Works with all content sources (Spotify, TuneIn, local music, etc.)
  4. Production ready - Extensively tested with real SoundTouch hardware
  5. Event integration - Generates proper WebSocket presetsUpdated notifications

API Design Insights

The original API limitation appears to have been either:

Complete Preset Lifecycle

This implementation now provides the full preset management lifecycle:

Summary

Preset management in the Bose SoundTouch API is intentionally read-only by design. The API provides excellent capabilities for analyzing and understanding preset configurations, but preset creation must be done through official channels (app or device). This is a deliberate design decision that respects user control over their personal preset configurations.

For most use cases, reading preset information is sufficient for building applications that work with existing user configurations. For preset creation, guide users to use the official app or device controls, which provide the proper user experience and validation.