From 5e6885cfe8bbe50449f14bf54b1b2a7101ab7a43 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Mon, 20 Apr 2026 19:04:46 +0200 Subject: [PATCH] Add missing RADIO_BROWSER default source --- docs/reference/radio-browser.md | 66 ++++++++++++++++++--- pkg/service/datastore/datastore.go | 12 ++++ pkg/service/handlers/handlers_marge_test.go | 4 +- pkg/service/marge/marge_test.go | 7 ++- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/docs/reference/radio-browser.md b/docs/reference/radio-browser.md index fa305eb..e8fca8b 100644 --- a/docs/reference/radio-browser.md +++ b/docs/reference/radio-browser.md @@ -2,6 +2,42 @@ - https://www.radio-browser.info is a community driven radio station database. - It provides an API to access the data and allows users to submit new stations or update existing ones. +- RadioBrowser provides a native SoundTouch-compatible API at `https://all.api.radio-browser.info/soundtouch`. + +### Architecture + +This service registers RadioBrowser in its BMX service registry (provider ID 39) pointing to RadioBrowser's SoundTouch API. The device discovers it from there and communicates directly with RadioBrowser for browsing and playback — the local service does not proxy streams. + +The source is registered with type `RADIO_BROWSER` in the Marge sources list. The RadioBrowser provider ID (39) in the source entry identifies it as RadioBrowser within the BMX layer. + +The device's own `Sources.xml` (`/mnt/nv/BoseApp-Persistence/1/Sources.xml`) must contain a `RADIO_BROWSER` entry for playback to work: + +```xml + + + +``` + +**A device reboot is required after adding this entry.** The firmware only registers `RADIO_BROWSER` as a selectable source type during the boot-time `Sources.xml` load. The Marge runtime sync stores the source in the registry but does not complete the activation — without a reboot, selecting a `RADIO_BROWSER` station results in `INVALID_SOURCE`. + +A reboot achieves two things in sequence: + +1. The speaker fetches all sources from the soundtouch-service via a `/full` request, which updates the device-local `Sources.xml`. +2. The firmware initialises and registers the `RADIO_BROWSER` source type from that updated file. + +The `INVALID_SOURCE_TYPE` message from the Bluetooth daemon visible in device logs (e.g. during `GET /serviceAvailability`) is informational noise and does not affect playback. + +### Triggering a sources refresh without rebooting + +Step 1 above (the `/full` fetch that updates `Sources.xml`) can be triggered independently by posting a `sourcesUpdated` notification directly to the speaker. This is useful for verifying that the soundtouch-service serves the correct sources list before committing to a full reboot: + +```bash +curl -v -X POST http://:8090/notification \ + -H "Content-Type: application/xml" \ + -d '' +``` + +Replace `` with your speaker's IP address and `` with its device ID (visible in `/info`). After this call the speaker re-fetches its sources from the service. Step 2 (source-type registration) still requires a reboot. ### Search for stations @@ -9,10 +45,7 @@ - Click on the station and copy the UUID from the URL. - e.g. `https://www.radio-browser.info/history/d28420a4-eccf-47a2-ace1-088c7e7cb7e0` -### RADIO_BROWSER - -- This project supports source type RADIO_BROWSER to play radio stations. -- Set the `location` attribute to `/stations/byuuid/{UUID}`. +### Playing the station ```xml - RADIO_BROWSER + Radio Station Name ``` -### Playing the station - To start the radio stream replace `` and `` and run curl like this: ```bash curl -d '' :8090/select ``` + +### BMX service registry entry + +The entry in `pkg/service/handlers/static/bmx_services.json` that enables RadioBrowser: + +```json +{ + "baseUrl": "https://all.api.radio-browser.info/soundtouch", + "id": { + "name": "RADIO_BROWSER", + "value": 39 + }, + "streamTypes": ["liveRadio", "onDemand"], + "authenticationModel": { + "anonymousAccount": { + "autoCreate": true, + "enabled": true + } + } +} +``` diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go index 9a48b8f..d0ddda2 100644 --- a/pkg/service/datastore/datastore.go +++ b/pkg/service/datastore/datastore.go @@ -1503,6 +1503,18 @@ func (ds *DataStore) getDefaultSources() []models.ConfiguredSource { CreatedOn: "2017-07-20T16:43:48.000+00:00", UpdatedOn: "2017-07-20T16:43:48.000+00:00", }, + { + ID: "10005", + DisplayName: "", + SourceKeyType: constants.ProviderRadioBrowser, + SourceKeyAccount: "", + SourceProviderID: strconv.Itoa(constants.RadioBrowserProviderID), + Type: "Audio", + SecretType: "token", + Status: "READY", + CreatedOn: "2026-02-16T01:01:01.000+00:00", + UpdatedOn: "2026-02-16T01:01:01.000+00:00", + }, } for i := range sources { diff --git a/pkg/service/handlers/handlers_marge_test.go b/pkg/service/handlers/handlers_marge_test.go index 5c16b4c..826f130 100644 --- a/pkg/service/handlers/handlers_marge_test.go +++ b/pkg/service/handlers/handlers_marge_test.go @@ -65,8 +65,8 @@ func TestMargeCreateAccount(t *testing.T) { } // Verify it has default sources - if len(resp.Sources) != 4 { - t.Errorf("Expected 4 default sources, got %d", len(resp.Sources)) + if len(resp.Sources) != 5 { + t.Errorf("Expected 5 default sources, got %d", len(resp.Sources)) } else { if resp.Sources[0].ID != "10001" { t.Errorf("Expected first source ID 10001, got %s", resp.Sources[0].ID) diff --git a/pkg/service/marge/marge_test.go b/pkg/service/marge/marge_test.go index bde0d27..2c4f76c 100644 --- a/pkg/service/marge/marge_test.go +++ b/pkg/service/marge/marge_test.go @@ -638,7 +638,7 @@ func TestDefaultSources(t *testing.T) { t.Fatalf("Failed to get sources: %v", err) } - expectedCount := 4 + expectedCount := 5 if len(sources) != expectedCount { t.Errorf("Expected %d sources, got %d", expectedCount, len(sources)) } @@ -668,6 +668,11 @@ func TestDefaultSources(t *testing.T) { if s.SecretType != "token" { t.Errorf("Expected INTERNET_RADIO secretType token, got %s", s.SecretType) } + case "RADIO_BROWSER": + foundIR = true + if s.SecretType != "token" { + t.Errorf("Expected RADIO_BROWSER secretType token, got %s", s.SecretType) + } case "AUX": foundAux = true if s.DisplayName != "AUX IN" {