diff --git a/cmd/soundtouch-player/README.md b/cmd/soundtouch-player/README.md index fa4150c7..75120f9f 100644 --- a/cmd/soundtouch-player/README.md +++ b/cmd/soundtouch-player/README.md @@ -348,6 +348,14 @@ Based on WebSocket interaction analysis, potential future features: - Advanced preset programming - Progressive Web App (PWA) features +## Behaviour reference + +[Player: Sources and Selection State](../../docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md) +covers the parts that are not obvious from the code: which advertised sources +can actually be selected and which need a station ContentItem, how a selection +is confirmed against a speaker that answers 200 either way, and the revision +and epoch fields the browser uses to order status updates. + ## License Same as the parent project - see main repository LICENSE file. diff --git a/docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md b/docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md index 2052cf40..07f774cf 100644 --- a/docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md +++ b/docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md @@ -54,13 +54,13 @@ after the fact. The only remedy is not to issue such a select. ## What the player does instead -| Source | Click behaviour | Resumes from Recents | -| --- | --- | --- | -| `RADIO_BROWSER` | resume newest station, else open RadioBrowser | yes | -| `TUNEIN` | resume newest station, else open TuneIn | yes | -| `LOCAL_INTERNET_RADIO` | open Play URL | no | -| `STORED_MUSIC` | open Library | no | -| anything else | `POST /select` as before | n/a | +| Source | Click behaviour | Resumes from Recents | +|------------------------|-----------------------------------------------|----------------------| +| `RADIO_BROWSER` | resume newest station, else open RadioBrowser | yes | +| `TUNEIN` | resume newest station, else open TuneIn | yes | +| `LOCAL_INTERNET_RADIO` | open Play URL | no | +| `STORED_MUSIC` | open Library | no | +| anything else | `POST /select` as before | n/a | Resuming replays the newest Recents entry for that source, using that entry's own ContentItem: the real item the speaker was given, `Location` included. diff --git a/docs/content/docs/reference/SOURCE-SELECTION.md b/docs/content/docs/reference/SOURCE-SELECTION.md index 09b833f7..8a8913b0 100644 --- a/docs/content/docs/reference/SOURCE-SELECTION.md +++ b/docs/content/docs/reference/SOURCE-SELECTION.md @@ -29,17 +29,34 @@ The Bose SoundTouch Go client provides comprehensive source selection functional **Response**: HTTP 200 OK (no body) on success -**Supported Sources:** -- `SPOTIFY` - Spotify streaming service +**Sources selectable this way:** +- `SPOTIFY` - Spotify streaming service (with a real `sourceAccount`) - `BLUETOOTH` - Bluetooth audio input - `AUX` - Auxiliary input (3.5mm jack) -- `TUNEIN` - TuneIn internet radio -- `PANDORA` - Pandora streaming service -- `AMAZON` - Amazon Music -- `IHEARTRADIO` - iHeartRadio streaming -- `STORED_MUSIC` - Local/network stored music - `AIRPLAY` - Apple AirPlay (device dependent) +- `PANDORA`, `AMAZON`, `IHEARTRADIO` - streaming services (with an account) + +### Sources that need a ContentItem instead + +A bare `/select` carrying only a source and account is **not** enough for +every source a speaker advertises. These need a full ContentItem with a +`Location`, built by `stations.ResolveContentItem` with `type="stationurl"`: + +- `TUNEIN` - TuneIn internet radio - `RADIO_BROWSER` - [RadioBrowser](radio-browser.md) internet radio directory +- `LOCAL_INTERNET_RADIO` - stream URLs, including Play URL and TTS output +- `STORED_MUSIC` - one entry per media server; selecting it names no track + +Given a bare select for one of these the speaker does **not** report an +error. It answers `200`, parks on a stub now-playing with no `playStatus`, +empty `type` and `location`, and an `itemName` echoing the source name, and +carries on playing whatever it was playing. It then reports that stub +indefinitely, so callers that check `/now_playing` see the source they asked +for while the audio is something else. + +Use `SelectContentItem` with a real `Location` for these, and see +[Player: Sources and Selection State](PLAYER-SOURCE-BEHAVIOUR.md) for the +stub's full signature and how the web player avoids it. ## Client Library Usage @@ -158,20 +175,20 @@ soundtouch-cli -host 192.0.2.100 -aux ### CLI Flags -| Flag | Description | Example | -|------|-------------|---------| -| `-select-source ` | Select audio source | `-select-source SPOTIFY` | -| `-source-account ` | Account for streaming services | `-source-account "user123"` | -| `-spotify` | Select Spotify source | `-spotify -source-account "user"` | -| `-bluetooth` | Select Bluetooth source | `-bluetooth` | -| `-aux` | Select AUX input | `-aux` | +| Flag | Description | Example | +|-----------------------------|--------------------------------|-----------------------------------| +| `-select-source ` | Select audio source | `-select-source SPOTIFY` | +| `-source-account ` | Account for streaming services | `-source-account "user123"` | +| `-spotify` | Select Spotify source | `-spotify -source-account "user"` | +| `-bluetooth` | Select Bluetooth source | `-bluetooth` | +| `-aux` | Select AUX input | `-aux` | ## Source Account Information ### When Source Accounts are Required - **Spotify**: Required for multi-account setups -- **Pandora**: Required for account-based access +- **Pandora**: Required for account-based access - **TuneIn**: Optional, may improve personalization - **Amazon Music**: Required for account access - **Bluetooth/AUX**: Not required (leave empty) @@ -259,11 +276,11 @@ func selectWithConfig(client *client.Client, config SourceConfig) error { ### Common Error Codes -| Code | Name | Description | Solution | -|------|------|-------------|----------| -| 1005 | UNKNOWN_SOURCE_ERROR | Invalid or unavailable source | Check available sources first | -| 1006 | SOURCE_UNAVAILABLE | Source temporarily unavailable | Try again later | -| 1007 | ACCOUNT_ERROR | Invalid account for source | Check account name format | +| Code | Name | Description | Solution | +|------|----------------------|--------------------------------|-------------------------------| +| 1005 | UNKNOWN_SOURCE_ERROR | Invalid or unavailable source | Check available sources first | +| 1006 | SOURCE_UNAVAILABLE | Source temporarily unavailable | Try again later | +| 1007 | ACCOUNT_ERROR | Invalid account for source | Check account name format | ### Troubleshooting Tips @@ -347,13 +364,16 @@ The implementation follows the official SoundTouch API: ## Related Documentation +- [Player: Sources and Selection State](PLAYER-SOURCE-BEHAVIOUR.md) - which + sources accept a bare select, and how a selection is confirmed + - **[API Endpoints Overview](API-ENDPOINTS.md)** - Complete API reference -- **[Sources](https://github.com/gesellix/Bose-SoundTouch/blob/main/pkg/models/sources.go)** - Source model implementation +- **[Sources](https://github.com/gesellix/Bose-SoundTouch/blob/main/pkg/models/sources.go)** - Source model implementation - **[Now Playing](https://github.com/gesellix/Bose-SoundTouch/blob/main/pkg/models/nowplaying.go)** - ContentItem model - **[Client Usage Examples](https://github.com/gesellix/Bose-SoundTouch/blob/main/cmd/soundtouch-cli/main.go)** - CLI implementation reference --- -**Implementation Date**: 2026-01-09 -**Status**: ✅ Complete and tested +**Implementation Date**: 2026-01-09 +**Status**: ✅ Complete and tested **Real Device Validation**: SoundTouch 10, SoundTouch 20 diff --git a/pkg/client/client.go b/pkg/client/client.go index dfa860c6..9799ad95 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -796,7 +796,14 @@ func (c *Client) DecreaseBalance(amount int) (*models.Balance, error) { return c.GetBalance() } -// SelectSource selects an audio source using the /select endpoint +// SelectSource selects an audio source using the /select endpoint. +// +// Only sources that are inputs in their own right can be selected this way: +// AUX, BLUETOOTH, or a streaming service with a real sourceAccount. Provider +// sources (TUNEIN, RADIO_BROWSER, LOCAL_INTERNET_RADIO) and STORED_MUSIC need +// SelectContentItem with a Location instead; a bare select leaves such a +// speaker reporting a source it is not playing. See +// docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md. func (c *Client) SelectSource(source, sourceAccount string) error { // Validate source parameter if source == "" { @@ -861,7 +868,15 @@ func (c *Client) SelectAux() error { return c.SelectSource("AUX", "") } -// SelectTuneIn is a convenience method to select TuneIn source +// SelectTuneIn is a convenience method to select TuneIn source. +// +// This sends a bare select, with no station. A speaker does not report an +// error for that: it answers 200, parks on a stub now-playing (no playStatus, +// empty type and location, itemName echoing the source name) and carries on +// playing whatever it was playing, then reports that stub indefinitely. To +// actually play something use SelectContentItem with a ContentItem carrying a +// Location, as stations.ResolveContentItem builds. See +// docs/content/docs/reference/PLAYER-SOURCE-BEHAVIOUR.md. func (c *Client) SelectTuneIn(sourceAccount string) error { return c.SelectSource("TUNEIN", sourceAccount) }