diff --git a/pkg/service/bmx/radiobrowser.go b/pkg/service/bmx/radiobrowser.go index bc72698..d6ace10 100644 --- a/pkg/service/bmx/radiobrowser.go +++ b/pkg/service/bmx/radiobrowser.go @@ -67,8 +67,10 @@ func RadioBrowserSearchPage(query string, offset int) (*models.BmxNavResponse, e subtitle += tags } - // SoundTouch format location for RadioBrowser - location := fmt.Sprintf("%s/soundtouch/stations/byuuid/%s", radioBrowserBaseURL, uuid) + // Relative SoundTouch playback location for RadioBrowser. The speaker + // prepends the BMX-registry base URL (radioBrowserBaseURL + "/soundtouch") + // when it follows a RADIO_BROWSER source, so the href must stay relative. + location := fmt.Sprintf("/stations/byuuid/%s", uuid) item := models.BmxNavItem{ Name: name, diff --git a/pkg/service/bmx/radiobrowser_test.go b/pkg/service/bmx/radiobrowser_test.go index 72a12c7..9f0dced 100644 --- a/pkg/service/bmx/radiobrowser_test.go +++ b/pkg/service/bmx/radiobrowser_test.go @@ -45,6 +45,16 @@ func TestRadioBrowserSearch(t *testing.T) { if item.Name != "Radio Paradise" { t.Errorf("expected name 'Radio Paradise', got %q", item.Name) } + + // The playback href must be the relative /stations/byuuid/ form so a + // RADIO_BROWSER select resolves against the BMX-registry base URL (#479). + if item.Links == nil || item.Links.BmxPlayback == nil { + t.Fatal("expected a bmx_playback link on the station item") + } + + if want := "/stations/byuuid/123-456"; item.Links.BmxPlayback.Href != want { + t.Errorf("expected playback href %q, got %q", want, item.Links.BmxPlayback.Href) + } } // makeStationsJSON returns a JSON array of n station objects. diff --git a/pkg/service/marge/classify_radiobrowser_test.go b/pkg/service/marge/classify_radiobrowser_test.go index 0dca09f..96f466d 100644 --- a/pkg/service/marge/classify_radiobrowser_test.go +++ b/pkg/service/marge/classify_radiobrowser_test.go @@ -67,13 +67,21 @@ func TestClassifyLearnedSource_RadioBrowserByProviderID(t *testing.T) { // TestClassifyLearnedSource_RadioBrowserByLocation verifies that the dispatcher // routes to classifyAsRadioBrowser when the location contains the RadioBrowser -// byuuid path segment (Source "URL" play path). +// byuuid path segment, for both the relative form the native RADIO_BROWSER play +// path now emits and the legacy absolute form (#479). func TestClassifyLearnedSource_RadioBrowserByLocation(t *testing.T) { - src := &models.ConfiguredSource{} + locations := []string{ + "/stations/byuuid/abc-123", // native relative location + "https://all.api.radio-browser.info/soundtouch/stations/byuuid/abc-123", // legacy absolute location + } - classifyLearnedSource(src, "", "https://all.api.radio-browser.info/soundtouch/stations/byuuid/abc-123", "") + for _, location := range locations { + src := &models.ConfiguredSource{} - if src.SourceKey.Type != constants.ProviderRadioBrowser { - t.Errorf("expected RADIO_BROWSER from byuuid location, got %q", src.SourceKey.Type) + classifyLearnedSource(src, "", location, "") + + if src.SourceKey.Type != constants.ProviderRadioBrowser { + t.Errorf("expected RADIO_BROWSER from byuuid location %q, got %q", location, src.SourceKey.Type) + } } } diff --git a/pkg/service/marge/marge.go b/pkg/service/marge/marge.go index f970e70..bf7ca8f 100644 --- a/pkg/service/marge/marge.go +++ b/pkg/service/marge/marge.go @@ -1850,7 +1850,7 @@ func classifyLearnedSource(src *models.ConfiguredSource, sourceID, location, sou classifyAsSpotify(src) case strings.Contains(location, "amazon") || sourceID == constants.ProviderAmazon || sourceProviderID == strconv.Itoa(constants.AmazonProviderID): classifyAsAmazon(src) - case sourceProviderID == strconv.Itoa(constants.RadioBrowserProviderID) || strings.Contains(location, "/soundtouch/stations/byuuid/"): + case sourceProviderID == strconv.Itoa(constants.RadioBrowserProviderID) || strings.Contains(location, "/stations/byuuid/"): classifyAsRadioBrowser(src) } // If we can't classify, leave SourceKey.Type empty so the canonical-by-ID diff --git a/pkg/service/stations/stations.go b/pkg/service/stations/stations.go index 78421b7..a8a71d0 100644 --- a/pkg/service/stations/stations.go +++ b/pkg/service/stations/stations.go @@ -9,6 +9,7 @@ import ( "github.com/gesellix/bose-soundtouch/pkg/models" "github.com/gesellix/bose-soundtouch/pkg/service/bmx" + "github.com/gesellix/bose-soundtouch/pkg/service/constants" ) // Provider identifies the radio station source backend. @@ -132,7 +133,13 @@ func ResolveContentItem(item PlayItem) *models.ContentItem { ci.Location = item.Location ci.ContainerArt = item.ContainerArt case ProviderRadioBrowser: - ci.Source = "URL" + // Native RADIO_BROWSER source: the speaker prepends the BMX-registry + // base URL (https://all.api.radio-browser.info/soundtouch) to the + // relative location and talks to Radio Browser directly. Using + // source="URL" here makes the speaker fetch the location as a raw + // audio stream, but it returns station JSON, not audio -> the speaker + // reports INVALID_SOURCE (issue #479). + ci.Source = constants.ProviderRadioBrowser ci.Type = "stationurl" ci.IsPresetable = true ci.ItemName = item.Name diff --git a/pkg/service/stations/stations_test.go b/pkg/service/stations/stations_test.go index 4c67298..8d6c260 100644 --- a/pkg/service/stations/stations_test.go +++ b/pkg/service/stations/stations_test.go @@ -68,14 +68,14 @@ func TestResolveContentItem_TuneIn_ContainerArt(t *testing.T) { func TestResolveContentItem_RadioBrowser(t *testing.T) { item := PlayItem{ Provider: ProviderRadioBrowser, - Location: "https://all.api.radio-browser.info/soundtouch/stations/byuuid/abc-123", + Location: "/stations/byuuid/abc-123", Name: "Radio Paradise", } ci := ResolveContentItem(item) - if ci.Source != "URL" { - t.Errorf("expected Source URL, got %q", ci.Source) + if ci.Source != "RADIO_BROWSER" { + t.Errorf("expected Source RADIO_BROWSER, got %q", ci.Source) } if ci.Type != "stationurl" { @@ -131,21 +131,21 @@ func TestResolveContentItem_SourceAccountGuard_RealAccountKept(t *testing.T) { } } -// TestResolveContentItem_SourceAccountGuard_RadioBrowserURLSource checks the -// guard for RadioBrowser where Source is "URL". -func TestResolveContentItem_SourceAccountGuard_RadioBrowserURLSource(t *testing.T) { - // SourceAccount == "URL" is the echo value — must be dropped. +// TestResolveContentItem_SourceAccountGuard_RadioBrowserEchoDropped checks the +// guard for RadioBrowser where Source is "RADIO_BROWSER". +func TestResolveContentItem_SourceAccountGuard_RadioBrowserEchoDropped(t *testing.T) { + // SourceAccount == "RADIO_BROWSER" is the echo value — must be dropped. item := PlayItem{ Provider: ProviderRadioBrowser, - Location: "https://all.api.radio-browser.info/soundtouch/stations/byuuid/xyz", + Location: "/stations/byuuid/xyz", Name: "Test", - SourceAccount: "URL", + SourceAccount: "RADIO_BROWSER", } ci := ResolveContentItem(item) if ci.SourceAccount != "" { - t.Errorf("expected SourceAccount dropped for URL source, got %q", ci.SourceAccount) + t.Errorf("expected SourceAccount dropped for RADIO_BROWSER source, got %q", ci.SourceAccount) } }