diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index 534c553d45e..ddbc1a2ea40 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -16,3 +16,7 @@ class ServerNotSpecified(PlexException): class ShouldUpdateConfigEntry(PlexException): """Config entry data is out of date and should be updated.""" + + +class MediaNotFound(PlexException): + """Requested media was not found.""" diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 11599086179..c50c173ec8d 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.errors import BrowseError from .const import DOMAIN, PLEX_URI_SCHEME +from .errors import MediaNotFound from .helpers import pretty_title @@ -115,9 +116,9 @@ def browse_media( # noqa: C901 def build_item_response(payload): """Create response payload for the provided media query.""" - media = plex_server.lookup_media(**payload) - - if media is None: + try: + media = plex_server.lookup_media(**payload) + except MediaNotFound: return None try: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 9b8b0df14c7..d652ead7dae 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -50,6 +50,7 @@ from .const import ( SERVERS, TRANSIENT_DEVICE_MODELS, ) +from .errors import MediaNotFound from .media_browser import browse_media _LOGGER = logging.getLogger(__name__) @@ -510,7 +511,7 @@ class PlexMediaPlayer(MediaPlayerEntity): try: playqueue = self.plex_server.get_playqueue(playqueue_id) except plexapi.exceptions.NotFound as err: - raise HomeAssistantError( + raise MediaNotFound( f"PlayQueue '{playqueue_id}' could not be found" ) from err else: @@ -519,9 +520,6 @@ class PlexMediaPlayer(MediaPlayerEntity): resume = src.pop("resume", False) media = self.plex_server.lookup_media(media_type, **src) - if media is None: - raise HomeAssistantError(f"Media could not be found: {media_id}") - if resume and not offset: offset = media.viewOffset diff --git a/homeassistant/components/plex/media_search.py b/homeassistant/components/plex/media_search.py index abe32f7cf4c..351ee1444c4 100644 --- a/homeassistant/components/plex/media_search.py +++ b/homeassistant/components/plex/media_search.py @@ -1,7 +1,13 @@ """Helper methods to search for Plex media.""" +from __future__ import annotations + import logging +from plexapi.base import PlexObject from plexapi.exceptions import BadRequest, NotFound +from plexapi.library import LibrarySection + +from .errors import MediaNotFound LEGACY_PARAM_MAPPING = { "show_name": "show.title", @@ -28,13 +34,19 @@ PREFERRED_LIBTYPE_ORDER = ( _LOGGER = logging.getLogger(__name__) -def search_media(media_type, library_section, allow_multiple=False, **kwargs): +def search_media( + media_type: str, + library_section: LibrarySection, + allow_multiple: bool = False, + **kwargs, +) -> PlexObject | list[PlexObject]: """Search for specified Plex media in the provided library section. - Returns a single media item or None. + Returns a media item or a list of items if `allow_multiple` is set. - If `allow_multiple` is `True`, return a list of matching items. + Raises MediaNotFound if the search was unsuccessful. """ + original_query = kwargs.copy() search_query = {} libtype = kwargs.pop("libtype", None) @@ -61,11 +73,12 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs): try: results = library_section.search(**search_query) except (BadRequest, NotFound) as exc: - _LOGGER.error("Problem in query %s: %s", search_query, exc) - return None + raise MediaNotFound(f"Problem in query {original_query}: {exc}") from exc if not results: - return None + raise MediaNotFound( + f"No {media_type} results in '{library_section.title}' for {original_query}" + ) if len(results) > 1: if allow_multiple: @@ -75,10 +88,8 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs): exact_matches = [x for x in results if x.title.lower() == title.lower()] if len(exact_matches) == 1: return exact_matches[0] - _LOGGER.warning( - "Multiple matches, make content_id more specific or use `allow_multiple`: %s", - results, + raise MediaNotFound( + f"Multiple matches, make content_id more specific or use `allow_multiple`: {results}" ) - return None return results[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index b4dc5755a73..b136bec73e9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -42,7 +42,12 @@ from .const import ( X_PLEX_PRODUCT, X_PLEX_VERSION, ) -from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry +from .errors import ( + MediaNotFound, + NoServersFound, + ServerNotSpecified, + ShouldUpdateConfigEntry, +) from .media_search import search_media from .models import PlexSession @@ -619,37 +624,34 @@ class PlexServer: key = kwargs["plex_key"] try: return self.fetch_item(key) - except NotFound: - _LOGGER.error("Media for key %s not found", key) - return None + except NotFound as err: + raise MediaNotFound(f"Media for key {key} not found") from err if media_type == MEDIA_TYPE_PLAYLIST: try: playlist_name = kwargs["playlist_name"] return self.playlist(playlist_name) - except KeyError: - _LOGGER.error("Must specify 'playlist_name' for this search") - return None - except NotFound: - _LOGGER.error( - "Playlist '%s' not found", - playlist_name, - ) - return None + except KeyError as err: + raise MediaNotFound( + "Must specify 'playlist_name' for this search" + ) from err + except NotFound as err: + raise MediaNotFound(f"Playlist '{playlist_name}' not found") from err try: library_name = kwargs.pop("library_name") library_section = self.library.section(library_name) - except KeyError: - _LOGGER.error("Must specify 'library_name' for this search") - return None - except NotFound: + except KeyError as err: + raise MediaNotFound("Must specify 'library_name' for this search") from err + except NotFound as err: library_sections = [section.title for section in self.library.sections()] - _LOGGER.error( - "Library '%s' not found in %s", library_name, library_sections - ) - return None + raise MediaNotFound( + f"Library '{library_name}' not found in {library_sections}" + ) from err + _LOGGER.debug( + "Searching for %s in %s using: %s", media_type, library_section, kwargs + ) return search_media(media_type, library_section, **kwargs) @property diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 0433ba836cd..280152b972f 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -16,6 +16,7 @@ from .const import ( SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) +from .errors import MediaNotFound REFRESH_LIBRARY_SCHEMA = vol.Schema( {vol.Optional("server_name"): str, vol.Required("library_name"): str} @@ -115,15 +116,13 @@ def lookup_plex_media(hass, content_type, content_id): try: playqueue = plex_server.get_playqueue(playqueue_id) except NotFound as err: - raise HomeAssistantError( + raise MediaNotFound( f"PlayQueue '{playqueue_id}' could not be found" ) from err return playqueue shuffle = content.pop("shuffle", 0) media = plex_server.lookup_media(content_type, **content) - if media is None: - raise HomeAssistantError(f"Plex media not found using payload: '{content_id}'") if shuffle: return plex_server.create_playqueue(media, shuffle=shuffle) diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index f73fdea2806..e5c19d31c4e 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -16,13 +16,11 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) from homeassistant.components.plex.const import DOMAIN +from homeassistant.components.plex.errors import MediaNotFound from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.exceptions import HomeAssistantError -async def test_media_lookups( - hass, mock_plex_server, requests_mock, playqueue_created, caplog -): +async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created): """Test media lookups to Plex server.""" # Plex Key searches media_player_id = hass.states.async_entity_ids("media_player")[0] @@ -39,7 +37,7 @@ async def test_media_lookups( }, True, ) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -51,10 +49,10 @@ async def test_media_lookups( }, True, ) - assert "Media could not be found: 123" in str(excinfo.value) + assert "Media for key 123 not found" in str(excinfo.value) # TV show searches - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: payload = '{"library_name": "Not a Library", "show_name": "TV Show"}' assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -66,7 +64,7 @@ async def test_media_lookups( }, True, ) - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Library 'Not a Library' not found in" in str(excinfo.value) with patch("plexapi.library.LibrarySection.search") as search: assert await hass.services.async_call( @@ -243,8 +241,21 @@ async def test_media_lookups( ) search.assert_called_with(**{"title": "Movie 1", "libtype": None}) - # TV show searches - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: + payload = '{"title": "Movie 1"}' + assert await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert "Must specify 'library_name' for this search" in str(excinfo.value) + + with pytest.raises(MediaNotFound) as excinfo: payload = '{"library_name": "Movies", "title": "Not a Movie"}' with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): assert await hass.services.async_call( @@ -257,8 +268,7 @@ async def test_media_lookups( }, True, ) - assert "Problem in query" in caplog.text - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Problem in query" in str(excinfo.value) # Playlist searches assert await hass.services.async_call( @@ -272,7 +282,7 @@ async def test_media_lookups( True, ) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: payload = '{"playlist_name": "Not a Playlist"}' assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -284,10 +294,9 @@ async def test_media_lookups( }, True, ) - assert "Playlist 'Not a Playlist' not found" in caplog.text - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Playlist 'Not a Playlist' not found" in str(excinfo.value) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: payload = "{}" assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -299,5 +308,4 @@ async def test_media_lookups( }, True, ) - assert "Must specify 'playlist_name' for this search" in caplog.text - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Must specify 'playlist_name' for this search" in str(excinfo.value) diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 2db0323bdcd..e67249ea375 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -43,7 +43,6 @@ async def test_media_player_playback( requests_mock, playqueue_created, player_plexweb_resources, - caplog, ): """Test playing media on a Plex media_player.""" requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) @@ -68,7 +67,7 @@ async def test_media_player_playback( }, True, ) - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert f"No {MEDIA_TYPE_MOVIE} results in 'Movies' for" in str(excinfo.value) movie1 = MockPlexMedia("Movie", "movie") movie2 = MockPlexMedia("Movie II", "movie") @@ -120,8 +119,7 @@ async def test_media_player_playback( }, True, ) - assert f"Media could not be found: {payload}" in str(excinfo.value) - assert "Multiple matches, make content_id more specific" in caplog.text + assert "Multiple matches, make content_id more specific" in str(excinfo.value) # Test multiple choices with allow_multiple movies = [movie1, movie2, movie3] diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 49fdd48f662..ddc4ed58ba8 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -168,7 +168,7 @@ async def test_lookup_media_for_other_integrations( with patch("plexapi.library.LibrarySection.search", return_value=None): with pytest.raises(HomeAssistantError) as excinfo: lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA) - assert "Plex media not found" in str(excinfo.value) + assert f"No {MEDIA_TYPE_MUSIC} results in 'Music' for" in str(excinfo.value) # Test with playqueue requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234)