1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add exception handling to media source in Radio Browser integration (#164653)

This commit is contained in:
Manu
2026-03-17 17:13:11 +01:00
committed by GitHub
parent 0a2fc97696
commit adec1d128c
4 changed files with 179 additions and 25 deletions

View File

@@ -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

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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"