diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index e62fe0325cc..165d53860a4 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -4,10 +4,11 @@ from __future__ import annotations import mimetypes +from aiodns.error import DNSError import pycountry -from radios import FilterBy, Order, RadioBrowser, Station +from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station -from homeassistant.components.media_player import MediaClass, MediaType +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source import ( BrowseMediaSource, MediaSource, @@ -15,6 +16,7 @@ from homeassistant.components.media_source import ( PlayMedia, Unresolvable, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.util.location import vincenty @@ -55,9 +57,20 @@ class RadioMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve selected Radio station to a streaming URL.""" - radios = self.radios - station = await radios.station(uuid=item.identifier) + if self.entry.state != ConfigEntryState.LOADED: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="config_entry_not_ready", + ) + radios = self.radios + try: + station = await radios.station(uuid=item.identifier) + except (DNSError, RadioBrowserError) as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="radio_browser_error", + ) from e if not station: raise Unresolvable("Radio station is no longer available") @@ -74,25 +87,37 @@ class RadioMediaSource(MediaSource): item: MediaSourceItem, ) -> BrowseMediaSource: """Return media.""" + + if self.entry.state != ConfigEntryState.LOADED: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="config_entry_not_ready", + ) radios = self.radios - return BrowseMediaSource( - domain=DOMAIN, - identifier=None, - media_class=MediaClass.CHANNEL, - media_content_type=MediaType.MUSIC, - title=self.entry.title, - can_play=False, - can_expand=True, - children_media_class=MediaClass.DIRECTORY, - children=[ - *await self._async_build_popular(radios, item), - *await self._async_build_by_tag(radios, item), - *await self._async_build_by_language(radios, item), - *await self._async_build_local(radios, item), - *await self._async_build_by_country(radios, item), - ], - ) + try: + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.MUSIC, + title=self.entry.title, + can_play=False, + can_expand=True, + children_media_class=MediaClass.DIRECTORY, + children=[ + *await self._async_build_popular(radios, item), + *await self._async_build_by_tag(radios, item), + *await self._async_build_by_language(radios, item), + *await self._async_build_local(radios, item), + *await self._async_build_by_country(radios, item), + ], + ) + except (DNSError, RadioBrowserError) as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="radio_browser_error", + ) from e @callback @staticmethod diff --git a/homeassistant/components/radio_browser/strings.json b/homeassistant/components/radio_browser/strings.json index 5dd0ad3dcf7..c1e99128ee1 100644 --- a/homeassistant/components/radio_browser/strings.json +++ b/homeassistant/components/radio_browser/strings.json @@ -5,5 +5,13 @@ "description": "Do you want to add Radio Browser to Home Assistant?" } } + }, + "exceptions": { + "config_entry_not_ready": { + "message": "Radio Browser integration is not ready" + }, + "radio_browser_error": { + "message": "Error occurred while communicating with Radio Browser" + } } } diff --git a/tests/components/radio_browser/conftest.py b/tests/components/radio_browser/conftest.py index 24bd93e48a7..651bd6ed53f 100644 --- a/tests/components/radio_browser/conftest.py +++ b/tests/components/radio_browser/conftest.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.radio_browser.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -39,10 +40,15 @@ async def init_integration( ) -> MockConfigEntry: """Set up the Radio Browser integration for testing.""" mock_config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_config_entry.entry_id) + with patch( + "homeassistant.components.radio_browser.RadioBrowser", + autospec=True, + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + assert mock_config_entry.state == ConfigEntryState.LOADED + return mock_config_entry diff --git a/tests/components/radio_browser/test_media_source.py b/tests/components/radio_browser/test_media_source.py index a9d08c1e438..4b1928a4b6c 100644 --- a/tests/components/radio_browser/test_media_source.py +++ b/tests/components/radio_browser/test_media_source.py @@ -1,15 +1,20 @@ """Tests for radio_browser media_source.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch +from aiodns.error import DNSError import pytest -from radios import FilterBy, Order +from radios import FilterBy, Order, RadioBrowserError from homeassistant.components import media_source +from homeassistant.components.media_player import BrowseError from homeassistant.components.radio_browser.media_source import async_get_media_source +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + DOMAIN = "radio_browser" @@ -71,3 +76,113 @@ async def test_browsing_local( assert other_browse is not None assert other_browse.title == "My Radios" assert len(other_browse.children) == 0 + + +@pytest.mark.parametrize( + "exception", + [DNSError, RadioBrowserError], +) +async def test_browsing_exceptions( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + exception: Exception, +) -> None: + """Test browsing exceptions.""" + + with patch( + "homeassistant.components.radio_browser.RadioBrowser", + autospec=True, + ) as mock_browser: + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.LOADED + + mock_browser.return_value.stations.side_effect = exception + with pytest.raises(BrowseError) as exc_info: + await media_source.async_browse_media( + hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular" + ) + assert exc_info.value.translation_key == "radio_browser_error" + + +async def test_browsing_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browsing config entry not ready.""" + + with patch( + "homeassistant.components.radio_browser.RadioBrowser", + autospec=True, + ) as mock_browser: + mock_browser.return_value.stats.side_effect = RadioBrowserError + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + + with pytest.raises(BrowseError) as exc_info: + await media_source.async_browse_media( + hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular" + ) + assert exc_info.value.translation_key == "config_entry_not_ready" + + +@pytest.mark.parametrize( + "exception", + [DNSError, RadioBrowserError], +) +async def test_resolve_media_exceptions( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + exception: Exception, +) -> None: + """Test resolving media exceptions.""" + + with patch( + "homeassistant.components.radio_browser.RadioBrowser", + autospec=True, + ) as mock_browser: + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.LOADED + + mock_browser.return_value.station.side_effect = exception + with pytest.raises(media_source.Unresolvable) as exc_info: + await media_source.async_resolve_media( + hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None + ) + assert exc_info.value.translation_key == "radio_browser_error" + + +async def test_resolve_media_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test resolving media config entry not ready.""" + + with patch( + "homeassistant.components.radio_browser.RadioBrowser", + autospec=True, + ) as mock_browser: + mock_browser.return_value.stats.side_effect = RadioBrowserError + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + + with pytest.raises(media_source.Unresolvable) as exc_info: + await media_source.async_resolve_media( + hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None + ) + assert exc_info.value.translation_key == "config_entry_not_ready"