diff --git a/pkg/service/soundtouchweb/browser_compatibility_test.go b/pkg/service/soundtouchweb/browser_compatibility_test.go index 1fff35a4..3be29471 100644 --- a/pkg/service/soundtouchweb/browser_compatibility_test.go +++ b/pkg/service/soundtouchweb/browser_compatibility_test.go @@ -67,6 +67,7 @@ render(h(Sources, { nowPlayingRevision: 1, sources: { SourceItem: [ { Source: 'RADIO_BROWSER', SourceAccount: '', DisplayName: 'RadioBrowser', Status: 'READY' }, + { Source: 'LOCAL_INTERNET_RADIO', SourceAccount: '', DisplayName: 'Local Radio', Status: 'READY' }, ] }, nowPlaying: { Source: 'SPOTIFY', SourceAccount: 'someone' }, }, @@ -573,7 +574,7 @@ func TestProviderSourceResumesMostRecentStation(t *testing.T) { if err := chromedp.Run(ctx, chromedp.Navigate(server.URL+"/fixture"), chromedp.WaitVisible(`.source-btn`, chromedp.ByQuery), - chromedp.Click(`.source-btn`, chromedp.ByQuery), + chromedp.Click(`.source-btn:nth-child(1)`, chromedp.ByQuery), chromedp.Poll(`document.querySelector('.source-command-status').textContent === 'Source selected'`, nil), chromedp.Evaluate(`window.navigated`, &navigated), ); err != nil { @@ -624,7 +625,7 @@ func TestProviderSourceWithoutRecentsNavigatesInstead(t *testing.T) { if err := chromedp.Run(ctx, chromedp.Navigate(server.URL+"/fixture"), chromedp.WaitVisible(`.source-btn`, chromedp.ByQuery), - chromedp.Click(`.source-btn`, chromedp.ByQuery), + chromedp.Click(`.source-btn:nth-child(1)`, chromedp.ByQuery), chromedp.Poll(`window.navigated.length === 1`, nil), chromedp.Evaluate(`window.navigated`, &navigated), ); err != nil { @@ -635,6 +636,21 @@ func TestProviderSourceWithoutRecentsNavigatesInstead(t *testing.T) { t.Errorf("navigated = %v, want [radiobrowser]", navigated) } + // LOCAL_INTERNET_RADIO has its own browser: Play URL is what emits that + // source, so that is where a click with nothing to resume belongs. + var localRadioNav []string + if err := chromedp.Run(ctx, + chromedp.Evaluate(`window.navigated = []`, nil), + chromedp.Click(`.source-btn:nth-child(2)`, chromedp.ByQuery), + chromedp.Poll(`window.navigated.length === 1`, nil), + chromedp.Evaluate(`window.navigated`, &localRadioNav), + ); err != nil { + t.Fatalf("exercise LOCAL_INTERNET_RADIO without recents: %v", err) + } + if len(localRadioNav) != 1 || localRadioNav[0] != "playurl" { + t.Errorf("navigated = %v, want [playurl]", localRadioNav) + } + mu.Lock() defer mu.Unlock() if writes != 0 { @@ -668,7 +684,7 @@ func TestProviderSourceNavigatesWhenRecentsFail(t *testing.T) { if err := chromedp.Run(ctx, chromedp.Navigate(server.URL+"/fixture"), chromedp.WaitVisible(`.source-btn`, chromedp.ByQuery), - chromedp.Click(`.source-btn`, chromedp.ByQuery), + chromedp.Click(`.source-btn:nth-child(1)`, chromedp.ByQuery), chromedp.Poll(`window.navigated.length === 1`, nil), chromedp.Evaluate(`window.navigated`, &navigated), ); err != nil { diff --git a/pkg/service/soundtouchweb/static/js/components/Sources.js b/pkg/service/soundtouchweb/static/js/components/Sources.js index ddcfd530..95514eca 100644 --- a/pkg/service/soundtouchweb/static/js/components/Sources.js +++ b/pkg/service/soundtouchweb/static/js/components/Sources.js @@ -24,14 +24,20 @@ const SOURCE_READBACK_DELAYS_MS = [2000, 5000, 10000]; // playing. The speaker then reports that stub indefinitely, so the player // shows a source the speaker is not actually playing. // -// Verified against real hardware for RADIO_BROWSER. TUNEIN is listed here -// because ResolveContentItem treats it identically (both need a Location). -// LOCAL_INTERNET_RADIO and ALEXA are also advertised READY but are NOT listed: -// whether a bare select resumes something for them is unverified, and leaving -// them alone preserves today's behaviour. +// Verified against real hardware for RADIO_BROWSER and LOCAL_INTERNET_RADIO: +// both produce the byte-identical stub. TUNEIN is listed because +// ResolveContentItem treats it identically to RADIO_BROWSER (both need a +// Location). ALEXA is also advertised READY but is NOT listed: whether a bare +// select resumes anything for it is unverified, so it keeps today's behaviour. +// +// `page` is the browser to fall back to when there is nothing to resume: the +// page in this app that produces content for that source. LOCAL_INTERNET_RADIO +// maps to Play URL because that is what HandlePlayURL emits, a ContentItem +// with Source "LOCAL_INTERNET_RADIO". const PROVIDER_SOURCES = { - RADIO_BROWSER: { page: 'radiobrowser', label: 'RadioBrowser' }, - TUNEIN: { page: 'tunein', label: 'TuneIn' }, + RADIO_BROWSER: { page: 'radiobrowser' }, + TUNEIN: { page: 'tunein' }, + LOCAL_INTERNET_RADIO: { page: 'playurl' }, }; function isErrorSource(source) { @@ -163,7 +169,7 @@ export function Sources({ } // Nothing to resume, and a bare select would strand the speaker on a - // stub: send the user to the browser to pick a station instead. + // stub: send the user to the browser to pick something instead. if (!item) { onNavigate?.(provider.page);