Documentation for controlling and preserving Bose SoundTouch devices
This document describes the planning for a Golang-based API client for the Bose SoundTouch Web API. The client follows modern Go patterns and supports both native Go library and WASM integration with embedded web UI.
New insights from pattern analysis:
GET /info - Device informationGET /now_playing - Currently playing musicPOST /key - Send key commands (PLAY, PAUSE, etc.)GET/POST /volume - Control volumeGET/POST /bass - Bass settingsGET/POST /sources - Available sourcesPOST /select - Select sourceGET /presets - Read presets (1-6) ✅ COMPLETEPOST /storePreset - Store/update presets ✅ COMPLETE (via SoundTouch Plus Wiki)POST /removePreset - Remove presets ✅ COMPLETE (via SoundTouch Plus Wiki)WebSocket / - Live updates for eventsgithub.com/gesellix/bose-soundtouch/
├── cmd/
│ ├── cli/ # CLI Tool (Main Application)
│ │ └── main.go
│ ├── webapp/ # Web Application with embedded Assets
│ │ ├── main.go
│ │ └── web/ # Embedded HTML/CSS/JS
│ │ ├── index.html
│ │ ├── app.js
│ │ └── style.css
│ └── wasm/ # WASM Entry Point
│ └── main.go
├── pkg/ # Public API (external usage)
│ ├── client/ # HTTP Client with XML Support
│ ├── discovery/ # UPnP Device Discovery
│ ├── models/ # Type-safe XML Data Models
│ ├── websocket/ # Event Streaming Client
│ ├── wasm/ # WASM JavaScript Bridge
│ └── config/ # Configuration Management
├── internal/ # Private Implementation Details
│ ├── xml/ # XML Parsing Utilities
│ ├── http/ # HTTP Utilities & Middleware
│ └── testing/ # Mock Client & Test Utilities
├── web/ # Frontend Development Assets
│ ├── src/ # Source files
│ └── dist/ # Build output → cmd/webapp/web/
├── examples/ # Usage Examples & Demos
├── test/ # Integration Tests & Docker
├── Makefile # Comprehensive Build System
├── .env.example # Configuration Template
├── .air-webapp.toml # Hot Reload Config
├── .air-wasm.toml # WASM Development Config
├── docker-compose.yml # Development Environment
├── PROJECT-PATTERNS.md # Pattern Documentation
├── API-Endpoints-Overview.md # API Reference
├── go.mod
└── README.md
pkg/client)type Client struct {
baseURL string
httpClient *http.Client
timeout time.Duration
userAgent string
}
type ClientConfig struct {
Host string
Port int
Timeout time.Duration
UserAgent string
}
// Core API methods
func NewClient(config ClientConfig) *Client
func (c *Client) GetDeviceInfo() (*models.DeviceInfo, error)
func (c *Client) GetNowPlaying() (*models.NowPlaying, error)
func (c *Client) SetVolume(volume int) error
func (c *Client) GetVolume() (*models.Volume, error)
func (c *Client) SendKey(key models.Key) error
func (c *Client) GetSources() (*models.Sources, error)
func (c *Client) SelectSource(source models.ContentItem) error
func (c *Client) GetPresets() (*models.Presets, error)
func (c *Client) SetPreset(id int, content models.ContentItem) error
// HTTP utilities with XML handling
func (c *Client) get(endpoint string, result interface{}) error
func (c *Client) post(endpoint string, data interface{}, result interface{}) error
pkg/discovery)type DiscoveryService struct {
timeout time.Duration
cache map[string]*Device
cacheTTL time.Duration
mutex sync.RWMutex
}
type Device struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
ModelID string `json:"modelId"`
SerialNo string `json:"serialNo"`
Location string `json:"location"`
LastSeen time.Time `json:"lastSeen"`
}
func NewDiscoveryService(timeout time.Duration) *DiscoveryService
func (d *DiscoveryService) DiscoverDevices() ([]Device, error)
func (d *DiscoveryService) DiscoverDevice(name string) (*Device, error)
func (d *DiscoveryService) GetCachedDevices() []Device
func (d *DiscoveryService) ClearCache()
// SSDP/UPnP implementation
func (d *DiscoveryService) sendMSearch() error
func (d *DiscoveryService) parseResponse(response string) (*Device, error)
pkg/models)// Base XML response with error handling
type XMLResponse struct {
XMLName xml.Name `xml:",innerxml"`
Error *APIError `xml:"error,omitempty"`
}
type APIError struct {
Code string `xml:"code,attr"`
Message string `xml:",innerxml"`
}
// Device Info
type DeviceInfo struct {
XMLResponse
XMLName xml.Name `xml:"info"`
DeviceID string `xml:"deviceID,attr"`
Name string `xml:"name"`
Type string `xml:"type"`
Components []string `xml:"components>component"`
// ... additional fields
}
// Now Playing with complete structure
type NowPlaying struct {
XMLResponse
XMLName xml.Name `xml:"nowPlaying"`
DeviceID string `xml:"deviceID,attr"`
Source string `xml:"source,attr"`
Content ContentItem `xml:"ContentItem"`
Track string `xml:"track"`
Artist string `xml:"artist"`
Album string `xml:"album"`
Art Art `xml:"art"`
PlayStatus PlayStatus `xml:"playStatus"`
Position Position `xml:"position,omitempty"`
}
// Enum types with validation
type PlayStatus string
const (
PlayStatusPlaying PlayStatus = "PLAY_STATE"
PlayStatusPaused PlayStatus = "PAUSE_STATE"
PlayStatusStopped PlayStatus = "STOP_STATE"
)
type Key string
const (
KeyPlay Key = "PLAY"
KeyPause Key = "PAUSE"
KeyStop Key = "STOP"
KeyPrevTrack Key = "PREV_TRACK"
KeyNextTrack Key = "NEXT_TRACK"
KeyVolumeUp Key = "VOLUME_UP"
KeyVolumeDown Key = "VOLUME_DOWN"
KeyMute Key = "MUTE"
KeyPower Key = "POWER"
KeyPreset1 Key = "PRESET_1"
KeyPreset2 Key = "PRESET_2"
KeyPreset3 Key = "PRESET_3"
KeyPreset4 Key = "PRESET_4"
KeyPreset5 Key = "PRESET_5"
KeyPreset6 Key = "PRESET_6"
)
pkg/websocket)type EventClient struct {
client *client.Client
conn *websocket.Conn
handlers map[string]EventHandler
stopChan chan bool
reconnect bool
backoff time.Duration
maxBackoff time.Duration
}
type EventHandler func(event Event)
type Event struct {
Type string `xml:"type,attr"`
DeviceID string `xml:"deviceID,attr"`
Data interface{} `xml:",innerxml"`
Timestamp time.Time `json:"timestamp"`
}
func NewEventClient(client *client.Client) *EventClient
func (e *EventClient) Subscribe(eventType string, handler EventHandler)
func (e *EventClient) Unsubscribe(eventType string)
func (e *EventClient) Start() error
func (e *EventClient) Stop() error
func (e *EventClient) IsConnected() bool
// Event types
const (
EventNowPlayingUpdated = "nowPlayingUpdated"
EventVolumeUpdated = "volumeUpdated"
EventConnectionState = "connectionStateUpdated"
EventPresetUpdated = "presetUpdated"
)
pkg/wasm)//go:build wasm
// +build wasm
import "syscall/js"
// Global WASM API registration
func RegisterWASMFunctions()
// Device Discovery (via proxy)
func wasmDiscoverDevices(this js.Value, args []js.Value) interface{}
// Client Management
func wasmCreateClient(this js.Value, args []js.Value) interface{}
func wasmGetNowPlaying(this js.Value, args []js.Value) interface{}
func wasmSendKey(this js.Value, args []js.Value) interface{}
func wasmSetVolume(this js.Value, args []js.Value) interface{}
func wasmGetSources(this js.Value, args []js.Value) interface{}
// Event Streaming
func wasmStartEventStream(this js.Value, args []js.Value) interface{}
func wasmStopEventStream(this js.Value, args []js.Value) interface{}
pkg/config)type Config struct {
// Server configuration
WebPort int `env:"WEB_PORT" default:"8080"`
APITimeout time.Duration `env:"API_TIMEOUT" default:"10s"`
// Discovery configuration
DiscoveryTimeout time.Duration `env:"DISCOVERY_TIMEOUT" default:"5s"`
CacheDevices bool `env:"CACHE_DEVICES" default:"true"`
CacheTTL time.Duration `env:"CACHE_TTL" default:"5m"`
// CORS configuration (for web proxy)
CORSOrigins []string `env:"CORS_ORIGINS" default:"*"`
// Logging
LogLevel string `env:"LOG_LEVEL" default:"info"`
LogFormat string `env:"LOG_FORMAT" default:"json"`
// Development
DevMode bool `env:"DEV_MODE" default:"false"`
}
func Load() Config
func LoadFromFile(filename string) (Config, error)
func (c Config) Validate() error
/storePreset and /removePreset endpoints (discovered via SoundTouch Plus Wiki)preset store, preset store-current, preset removeBINARY_NAME=soundtouch
VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
GO_VERSION=$(shell go version | cut -d ' ' -f 3)
LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GoVersion=$(GO_VERSION)"
BUILD_FLAGS=-trimpath $(LDFLAGS)
# Development builds
build:
go build $(BUILD_FLAGS) -o $(BINARY_NAME) ./cmd/cli
build-webapp:
go build $(BUILD_FLAGS) -o $(BINARY_NAME)-webapp ./cmd/webapp
# WASM build
build-wasm:
GOOS=js GOARCH=wasm go build $(BUILD_FLAGS) -o web/soundtouch.wasm ./cmd/wasm
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" web/
# Cross-platform builds
build-all: build-linux build-darwin build-windows
# Development with hot reload
dev-cli:
air -c .air-cli.toml
dev-webapp:
air -c .air-webapp.toml
dev-wasm:
air -c .air-wasm.toml
# Testing
test:
go test -v -race ./...
test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Quality checks
check: fmt vet lint test
# Docker development environment
docker-dev:
docker-compose up --build
# Release packaging
release: build-all
mkdir -p dist
tar -czf dist/$(BINARY_NAME)-$(VERSION)-linux-amd64.tar.gz $(BINARY_NAME)-linux-amd64
tar -czf dist/$(BINARY_NAME)-$(VERSION)-darwin-amd64.tar.gz $(BINARY_NAME)-darwin-amd64
zip dist/$(BINARY_NAME)-$(VERSION)-windows-amd64.zip $(BINARY_NAME)-windows-amd64.exe
package main
import (
"fmt"
"log"
"time"
"github.com/gesellix/bose-soundtouch/pkg/client"
"github.com/gesellix/bose-soundtouch/pkg/discovery"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
func main() {
// Discover devices
discoveryService := discovery.NewDiscoveryService(5 * time.Second)
devices, err := discoveryService.DiscoverDevices()
if err != nil {
log.Fatal(err)
}
if len(devices) == 0 {
log.Fatal("No SoundTouch devices found")
}
// Create client for first device
client := client.NewClient(client.ClientConfig{
Host: devices[0].Host,
Port: 8090,
Timeout: 10 * time.Second,
})
// Get device info
info, err := client.GetDeviceInfo()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Connected to: %s\n", info.Name)
// Get current playback
nowPlaying, err := client.GetNowPlaying()
if err != nil {
log.Fatal(err)
}
if nowPlaying.PlayStatus == models.PlayStatusPlaying {
fmt.Printf("Playing: %s - %s (%s)\n",
nowPlaying.Artist, nowPlaying.Track, nowPlaying.Album)
}
// Control playback
if nowPlaying.PlayStatus == models.PlayStatusPlaying {
client.SendKey(models.KeyPause)
fmt.Println("Paused playback")
} else {
client.SendKey(models.KeyPlay)
fmt.Println("Started playback")
}
}
# Discover devices
soundtouch discover
# Device operations
soundtouch --device 192.168.1.100 info
soundtouch --device 192.168.1.100 play
soundtouch --device 192.168.1.100 volume 50
soundtouch --device 192.168.1.100 preset 1
# Interactive mode
soundtouch interactive
# Web interface
soundtouch-webapp --port 8080
// Load WASM module
await loadWASM('/soundtouch.wasm');
// Discover devices (via proxy)
const devices = await boseAPI.discoverDevices();
console.log('Found devices:', devices);
// Create client
const client = boseAPI.createClient(devices[0].host, 8090);
// Get now playing
const nowPlaying = await client.getNowPlaying();
console.log(`Playing: ${nowPlaying.artist} - ${nowPlaying.track}`);
// Control playback
await client.sendKey('PAUSE');
// Volume control
await client.setVolume(75);
// Real-time events
client.startEventStream((event) => {
if (event.type === 'nowPlayingUpdated') {
updateUI(event.data);
}
});
# CLI Tool
./soundtouch-linux-amd64 discover
./soundtouch-linux-amd64 --device IP play
# Web Application (embedded assets)
./soundtouch-webapp-linux-amd64 --port 8080
# Docker
docker run -p 8080:8080 soundtouch-webapp
# Local development with hot reload
make dev-webapp # Web app development
make dev-wasm # WASM development
make dev-cli # CLI development
# Full development environment
docker-compose up # Mock devices + web app