Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Architecture

## The Problem

Bose SoundTouch speakers depend on 4 cloud servers for network features like TuneIn radio presets, account management, and firmware updates. Bose announced the shutdown of these servers on **May 6, 2026** (extended from the original February 18 date).

Reference: [Bose SoundTouch EOL page](https://www.bose.co.uk/en_gb/landing_pages/soundtouch-eol.html)

Bose has published official API documentation to support community developers: [SoundTouch Web API (PDF)](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf)

## The Four Bose Servers

| Server | Internal Name | Purpose | Status (Feb 2026) |
|--------|--------------|---------|-------------------|
| `streaming.bose.com` | marge | Account data, presets, recents, software updates, streaming tokens | Alive (shutdown not until May 6) |
| `content.api.bose.io` | bmx | TuneIn radio playback URLs, service registry, analytics | API removed early — server responds but returns 404 for all endpoints |
| `worldwide.bose.com` | updates | Firmware update checks | API removed early — returns 404 |
| `events.api.bosecm.com` | stats | Telemetry, device blacklist | Not proxied — telemetry only, safe to ignore |

Note: BMX and updates APIs are already returning 404s even though the official shutdown date hasn't passed. This suggests Bose is deprecating APIs incrementally.

## How SoundCork Replaces Them

```
Speaker → soundcork → local handlers (XML data files)
```

1. Edit the speaker's config file (`SoundTouchSdkPrivateCfg.xml`) to point all server URLs to your soundcork instance
2. SoundCork serves all responses locally from your extracted XML data files
3. No traffic reaches Bose servers

## Operating Modes

### Local Mode (Recommended)

`SOUNDCORK_MODE=local` (default)

All responses served from local data store. Zero traffic to Bose. This is recommended because:

- Complete independence from Bose servers
- No risk of unwanted firmware updates (marge has a `/streaming/software/update/` endpoint)
- No data sent to Bose
- Works identically whether Bose servers are up or down

### Proxy Mode

`SOUNDCORK_MODE=proxy`

Tries upstream Bose servers first, falls back to local handlers on failure. Useful during initial setup to:

- Verify your speaker is correctly talking to soundcork
- Capture real server responses for analysis
- Compare local handler behavior against real servers

**Not recommended for production** — the marge server's software update endpoint could potentially trigger a firmware update.

## Circuit Breaker (Proxy Mode)

When in proxy mode, a circuit breaker tracks upstream health per-server:

- **Closed** (healthy): Requests forwarded to upstream normally
- **Open** (down): Upstream failed recently — skip directly to local fallback. Opens on:
- Connection errors or timeouts (10 second timeout)
- HTTP 404 responses (API removed but server alive)
- HTTP 5xx responses (server errors)
- **Half-open** (probing): After 5-minute cooldown, allows one request through to probe upstream health

The circuit breaker is per-worker (gunicorn runs 2 workers), so at most 2 upstream probes per cooldown window per dead server.

## Data Flows

### Power-on Sequence

Speaker boots and calls (in order):

1. `POST /marge/streaming/support/power_on` — device registration
2. `GET /marge/streaming/sourceproviders` — available source types
3. `GET /marge/streaming/account/{id}/full` — full account with devices, presets, recents
4. `GET /marge/streaming/account/{id}/device/{id}/presets` — preset list
5. `GET /marge/streaming/software/update/account/{id}` — check for firmware updates (soundcork returns "no updates")

All served locally by soundcork's marge handlers.

### Playing a TuneIn Preset

1. Speaker sends preset button press event
2. Speaker requests `GET /bmx/tunein/v1/playback/station/{stationId}`
3. SoundCork returns the stream URL (e.g., `http://icecast.vrtcdn.be/mnm.aac`)
4. Speaker connects directly to the radio stream — no further server involvement

### Spotify

Spotify playback does **not** go through soundcork or Bose servers. See [Spotify Guide](spotify.md) for details.

## Traffic Logging

In proxy mode, all traffic is logged to `SOUNDCORK_LOG_DIR/traffic.jsonl` in JSON Lines format. Each entry contains:

- Timestamp
- Request: method, path, query, headers, body
- Upstream URL (or "local" if handled locally)
- Response: status, headers, body
- Fallback reason (if applicable): `circuit_open`, `upstream_error`, `upstream_http_404`, etc.

Useful for debugging and understanding speaker behavior. Not active in local mode.
169 changes: 169 additions & 0 deletions docs/speaker-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Speaker Setup Guide

How to set up your Bose SoundTouch speaker to work with SoundCork. This guide
covers enabling SSH access, extracting the data SoundCork needs, and redirecting
your speaker's cloud traffic to your SoundCork server.

## Prerequisites

- Bose SoundTouch speaker (tested on SoundTouch 20, firmware 27.0.6)
- Clean FAT32-formatted USB stick
- Ethernet cable (recommended for initial setup)
- Computer on the same network as the speaker

## Step 1: Enable SSH Access

### Firmware 27.x (Current)

The old `remote_services on` TAP command (port 17000) was **removed** in
firmware 27.x. You must use the USB stick method instead:

1. Format a USB stick as FAT32.
2. Create a single empty file called `remote_services` (no file extension).
3. **Critical for macOS users** — remove the junk files that macOS creates
automatically:
```sh
mdutil -i off /Volumes/YOUR_USB_NAME
rm -rf /Volumes/YOUR_USB_NAME/.fseventsd
rm -rf /Volumes/YOUR_USB_NAME/.Spotlight-V100
rm -f /Volumes/YOUR_USB_NAME/._*
```
These hidden files can prevent the speaker from detecting the
`remote_services` file.
4. Power off the speaker completely.
5. Insert the USB stick into the USB port on the back of the speaker.
6. Power on the speaker.
7. Wait approximately 60 seconds.

> **A note on connectivity**: During our testing, we initially failed with WiFi
> and a USB stick containing macOS junk files. We succeeded after cleaning the
> USB AND switching to Ethernet. We changed both variables simultaneously, so we
> cannot confirm which was the actual fix. If WiFi doesn't work for you, try
> connecting the speaker via Ethernet cable as well.

Then SSH in:

```sh
ssh root@<speaker-ip>
```

No password is required.

### Make SSH Persistent Across Reboots

By default, SSH access is lost when the speaker reboots. To make it permanent:

```sh
ssh root@<speaker-ip>
touch /mnt/nv/remote_services
```

This creates a persistent flag file on the speaker's non-volatile storage. You
can now remove the USB stick and SSH will survive reboots.

### Finding Your Speaker's IP Address

There are a few ways to find the speaker's IP:

- Check your router's DHCP client list for a device named "SoundTouch".
- If you have the [Bose CLI](https://github.com/timvw/bose): `bose status`
- The Bose SoundTouch app shows the speaker's IP in device settings.

## Step 2: Extract Speaker Data

SoundCork needs 4 XML files from your speaker. Some are available via the
speaker's local web API (port 8090), others require SSH.

### From the speaker's web API (port 8090)

```sh
curl http://<speaker-ip>:8090/presets > Presets.xml
curl http://<speaker-ip>:8090/recents > Recents.xml
curl http://<speaker-ip>:8090/info > DeviceInfo.xml
```

### From SSH (requires root access)

`Sources.xml` contains authentication tokens that are not exposed via the web
API. You must retrieve it over SSH:

```sh
ssh root@<speaker-ip>
cat /mnt/nv/BoseApp-Persistence/1/Sources.xml
```

Copy the output and save it as `Sources.xml`.

### Get Your Account UUID

From the `DeviceInfo.xml` you just downloaded, find the `margeAccountUUID`
field. Alternatively, via SSH:

```sh
cat /opt/Bose/etc/SoundTouchSdkPrivateCfg.xml
```

Look for the account UUID in the marge URL.

### Store Files in SoundCork's Data Directory

Place the extracted files in the following structure:

```
data/
<accountId>/
Presets.xml
Recents.xml
Sources.xml
devices/
<deviceId>/
DeviceInfo.xml
```

Where:
- `<accountId>` is your `margeAccountUUID`
- `<deviceId>` is the `deviceID` attribute from `DeviceInfo.xml`

See the [`examples/`](../examples/) directory in this repository for the
expected XML format.

## Step 3: Redirect Speaker to SoundCork

### Make the filesystem writable

The speaker's root filesystem is read-only by default. You must switch it to
read-write mode before editing any files:

```sh
ssh root@<speaker-ip>
rw
```

### Edit the server configuration

```sh
vi /opt/Bose/etc/SoundTouchSdkPrivateCfg.xml
```

Change all 4 server URLs to point to your SoundCork instance:

| Server | Before | After |
|---------|-------------------------------------|-------------------------------|
| marge | `https://streaming.bose.com` | `https://your-soundcork-server` |
| bmx | `https://content.api.bose.io` | `https://your-soundcork-server` |
| updates | `https://worldwide.bose.com` | `https://your-soundcork-server` |
| stats | `https://events.api.bosecm.com` | `https://your-soundcork-server` |

Reboot the speaker for changes to take effect. The speaker will now send all
cloud traffic to your SoundCork server.

## Warnings

> **Port 17000 (TAP Console)**: The speaker exposes a diagnostic console on
> port 17000. On firmware 27.x, most commands have been removed. **Do NOT send
> exploratory commands** — the `demo enter` command puts the speaker into
> factory/demo mode which may be difficult to recover from.

> **Read-only filesystem**: The speaker's root filesystem is read-only by
> default. Always run `rw` before editing files. The filesystem reverts to
> read-only on reboot.
100 changes: 100 additions & 0 deletions docs/spotify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Spotify on SoundTouch

## Two Different Spotify Systems

There are two completely separate ways Spotify works on a SoundTouch speaker. This is a common source of confusion.

### 1. Spotify Connect (Works — Recommended)

- The speaker advertises itself as a Spotify Connect device on your local network
- Open the Spotify app on your phone or computer
- Tap the speaker/device icon and select your SoundTouch speaker (e.g., "Bose-Woonkamer")
- Audio streams directly from Spotify's CDN to the speaker
- **No Bose servers involved** — this is purely between the Spotify app, Spotify's servers, and the speaker
- **No soundcork involvement** — Spotify Connect operates independently
- Works before and after the Bose shutdown

### 2. SoundTouch Spotify Integration (Broken Without Workaround)

- This is what the SoundTouch app used for browsing Spotify and setting Spotify presets
- Relies on Bose's own Spotify client ID for OAuth token management via the marge server
- The SoundTouch app can **no longer reconnect or configure** Spotify accounts
- Spotify presets may show as "unplayable" after redirecting to soundcork

## Fixing Spotify Presets

If your Spotify preset fails after setting up soundcork, you'll see these symptoms in the traffic logs:

- `type: "DO_NOT_RESUME"`, `location: "Unplayable location string"`, `playStatus: "STOP_STATE"`
- `source: "INVALID_SOURCE"`, `playStatus: "BUFFERING_STATE"`

### The Fix: Kick-Start via Spotify Connect

1. Open the **Spotify app** on your phone (not the SoundTouch app)
2. Play any song
3. Tap the speaker/device icon at the bottom of the Now Playing screen
4. Select your SoundTouch speaker
5. Wait for the music to start playing (confirms Spotify Connect is active)
6. Now press your Spotify preset button on the speaker — **it should work**

### Why This Works

When you cast via Spotify Connect, the Spotify app authenticates the speaker's **embedded Spotify client** directly over the local network using ZeroConf/mDNS. This gives the speaker a fresh Spotify session.

Key details:

- The Spotify session lives in the speaker's **RAM** — the OAuth token stored in `Sources.xml` doesn't change
- The preset can reuse the active Spotify session established by Spotify Connect
- **You may need to repeat this after a speaker reboot**, since the in-memory session is lost on restart
- Spotify playback traffic never appears in soundcork's traffic logs because it doesn't go through soundcork

### What We Observed (Traffic Analysis)

From our real traffic logs:

**Before Spotify Connect** (speaker trying to play preset on its own):

```
source-state: "SPOTIFY"
contentItem type: "DO_NOT_RESUME"
location: "Unplayable location string"
playStatus: "STOP_STATE"
```

**After casting one song via Spotify Connect** (from the Spotify app):

```
track: "Beachball - Vocal Radio Edit"
artist: "Nalin & Kane"
source: "SPOTIFY"
playStatus: "PLAY_STATE"
```

**Then pressing Spotify preset** (Clouseau):

```
artist: "Clouseau"
album: "Hoezo?"
source: "SPOTIFY"
playStatus: "PLAY_STATE" ← works!
```

## Managing Presets

The official SoundTouch app can no longer configure presets pointing to TuneIn stations. If you need to add or change presets, use the [Bose CLI](https://github.com/timvw/bose), which talks directly to the speaker's local API on port 8090:

```bash
# Install via Homebrew
brew install timvw/tap/bose

# View current presets
bose preset

# Get a specific preset
bose preset 1

# View speaker status
bose status
```

The speaker's local API (port 8090) is completely independent of the cloud servers and will continue to work indefinitely.