Improve Plex media search failure feedback (#67493)

This commit is contained in:
jjlawren 2022-03-17 15:57:22 -05:00 committed by GitHub
parent f75d621888
commit b34da1294c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 85 additions and 64 deletions

View File

@ -16,3 +16,7 @@ class ServerNotSpecified(PlexException):
class ShouldUpdateConfigEntry(PlexException): class ShouldUpdateConfigEntry(PlexException):
"""Config entry data is out of date and should be updated.""" """Config entry data is out of date and should be updated."""
class MediaNotFound(PlexException):
"""Requested media was not found."""

View File

@ -19,6 +19,7 @@ from homeassistant.components.media_player.const import (
from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_player.errors import BrowseError
from .const import DOMAIN, PLEX_URI_SCHEME from .const import DOMAIN, PLEX_URI_SCHEME
from .errors import MediaNotFound
from .helpers import pretty_title from .helpers import pretty_title
@ -115,9 +116,9 @@ def browse_media( # noqa: C901
def build_item_response(payload): def build_item_response(payload):
"""Create response payload for the provided media query.""" """Create response payload for the provided media query."""
media = plex_server.lookup_media(**payload) try:
media = plex_server.lookup_media(**payload)
if media is None: except MediaNotFound:
return None return None
try: try:

View File

@ -50,6 +50,7 @@ from .const import (
SERVERS, SERVERS,
TRANSIENT_DEVICE_MODELS, TRANSIENT_DEVICE_MODELS,
) )
from .errors import MediaNotFound
from .media_browser import browse_media from .media_browser import browse_media
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -510,7 +511,7 @@ class PlexMediaPlayer(MediaPlayerEntity):
try: try:
playqueue = self.plex_server.get_playqueue(playqueue_id) playqueue = self.plex_server.get_playqueue(playqueue_id)
except plexapi.exceptions.NotFound as err: except plexapi.exceptions.NotFound as err:
raise HomeAssistantError( raise MediaNotFound(
f"PlayQueue '{playqueue_id}' could not be found" f"PlayQueue '{playqueue_id}' could not be found"
) from err ) from err
else: else:
@ -519,9 +520,6 @@ class PlexMediaPlayer(MediaPlayerEntity):
resume = src.pop("resume", False) resume = src.pop("resume", False)
media = self.plex_server.lookup_media(media_type, **src) 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: if resume and not offset:
offset = media.viewOffset offset = media.viewOffset

View File

@ -1,7 +1,13 @@
"""Helper methods to search for Plex media.""" """Helper methods to search for Plex media."""
from __future__ import annotations
import logging import logging
from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.library import LibrarySection
from .errors import MediaNotFound
LEGACY_PARAM_MAPPING = { LEGACY_PARAM_MAPPING = {
"show_name": "show.title", "show_name": "show.title",
@ -28,13 +34,19 @@ PREFERRED_LIBTYPE_ORDER = (
_LOGGER = logging.getLogger(__name__) _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. """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 = {} search_query = {}
libtype = kwargs.pop("libtype", None) libtype = kwargs.pop("libtype", None)
@ -61,11 +73,12 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs):
try: try:
results = library_section.search(**search_query) results = library_section.search(**search_query)
except (BadRequest, NotFound) as exc: except (BadRequest, NotFound) as exc:
_LOGGER.error("Problem in query %s: %s", search_query, exc) raise MediaNotFound(f"Problem in query {original_query}: {exc}") from exc
return None
if not results: if not results:
return None raise MediaNotFound(
f"No {media_type} results in '{library_section.title}' for {original_query}"
)
if len(results) > 1: if len(results) > 1:
if allow_multiple: 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()] exact_matches = [x for x in results if x.title.lower() == title.lower()]
if len(exact_matches) == 1: if len(exact_matches) == 1:
return exact_matches[0] return exact_matches[0]
_LOGGER.warning( raise MediaNotFound(
"Multiple matches, make content_id more specific or use `allow_multiple`: %s", f"Multiple matches, make content_id more specific or use `allow_multiple`: {results}"
results,
) )
return None
return results[0] return results[0]

View File

@ -42,7 +42,12 @@ from .const import (
X_PLEX_PRODUCT, X_PLEX_PRODUCT,
X_PLEX_VERSION, X_PLEX_VERSION,
) )
from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry from .errors import (
MediaNotFound,
NoServersFound,
ServerNotSpecified,
ShouldUpdateConfigEntry,
)
from .media_search import search_media from .media_search import search_media
from .models import PlexSession from .models import PlexSession
@ -619,37 +624,34 @@ class PlexServer:
key = kwargs["plex_key"] key = kwargs["plex_key"]
try: try:
return self.fetch_item(key) return self.fetch_item(key)
except NotFound: except NotFound as err:
_LOGGER.error("Media for key %s not found", key) raise MediaNotFound(f"Media for key {key} not found") from err
return None
if media_type == MEDIA_TYPE_PLAYLIST: if media_type == MEDIA_TYPE_PLAYLIST:
try: try:
playlist_name = kwargs["playlist_name"] playlist_name = kwargs["playlist_name"]
return self.playlist(playlist_name) return self.playlist(playlist_name)
except KeyError: except KeyError as err:
_LOGGER.error("Must specify 'playlist_name' for this search") raise MediaNotFound(
return None "Must specify 'playlist_name' for this search"
except NotFound: ) from err
_LOGGER.error( except NotFound as err:
"Playlist '%s' not found", raise MediaNotFound(f"Playlist '{playlist_name}' not found") from err
playlist_name,
)
return None
try: try:
library_name = kwargs.pop("library_name") library_name = kwargs.pop("library_name")
library_section = self.library.section(library_name) library_section = self.library.section(library_name)
except KeyError: except KeyError as err:
_LOGGER.error("Must specify 'library_name' for this search") raise MediaNotFound("Must specify 'library_name' for this search") from err
return None except NotFound as err:
except NotFound:
library_sections = [section.title for section in self.library.sections()] library_sections = [section.title for section in self.library.sections()]
_LOGGER.error( raise MediaNotFound(
"Library '%s' not found in %s", library_name, library_sections f"Library '{library_name}' not found in {library_sections}"
) ) from err
return None
_LOGGER.debug(
"Searching for %s in %s using: %s", media_type, library_section, kwargs
)
return search_media(media_type, library_section, **kwargs) return search_media(media_type, library_section, **kwargs)
@property @property

View File

@ -16,6 +16,7 @@ from .const import (
SERVICE_REFRESH_LIBRARY, SERVICE_REFRESH_LIBRARY,
SERVICE_SCAN_CLIENTS, SERVICE_SCAN_CLIENTS,
) )
from .errors import MediaNotFound
REFRESH_LIBRARY_SCHEMA = vol.Schema( REFRESH_LIBRARY_SCHEMA = vol.Schema(
{vol.Optional("server_name"): str, vol.Required("library_name"): str} {vol.Optional("server_name"): str, vol.Required("library_name"): str}
@ -115,15 +116,13 @@ def lookup_plex_media(hass, content_type, content_id):
try: try:
playqueue = plex_server.get_playqueue(playqueue_id) playqueue = plex_server.get_playqueue(playqueue_id)
except NotFound as err: except NotFound as err:
raise HomeAssistantError( raise MediaNotFound(
f"PlayQueue '{playqueue_id}' could not be found" f"PlayQueue '{playqueue_id}' could not be found"
) from err ) from err
return playqueue return playqueue
shuffle = content.pop("shuffle", 0) shuffle = content.pop("shuffle", 0)
media = plex_server.lookup_media(content_type, **content) 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: if shuffle:
return plex_server.create_playqueue(media, shuffle=shuffle) return plex_server.create_playqueue(media, shuffle=shuffle)

View File

@ -16,13 +16,11 @@ from homeassistant.components.media_player.const import (
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
) )
from homeassistant.components.plex.const import DOMAIN from homeassistant.components.plex.const import DOMAIN
from homeassistant.components.plex.errors import MediaNotFound
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
async def test_media_lookups( async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created):
hass, mock_plex_server, requests_mock, playqueue_created, caplog
):
"""Test media lookups to Plex server.""" """Test media lookups to Plex server."""
# Plex Key searches # Plex Key searches
media_player_id = hass.states.async_entity_ids("media_player")[0] media_player_id = hass.states.async_entity_ids("media_player")[0]
@ -39,7 +37,7 @@ async def test_media_lookups(
}, },
True, True,
) )
with pytest.raises(HomeAssistantError) as excinfo: with pytest.raises(MediaNotFound) as excinfo:
with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound):
assert await hass.services.async_call( assert await hass.services.async_call(
MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_DOMAIN,
@ -51,10 +49,10 @@ async def test_media_lookups(
}, },
True, 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 # 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"}' payload = '{"library_name": "Not a Library", "show_name": "TV Show"}'
assert await hass.services.async_call( assert await hass.services.async_call(
MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_DOMAIN,
@ -66,7 +64,7 @@ async def test_media_lookups(
}, },
True, 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: with patch("plexapi.library.LibrarySection.search") as search:
assert await hass.services.async_call( assert await hass.services.async_call(
@ -243,8 +241,21 @@ async def test_media_lookups(
) )
search.assert_called_with(**{"title": "Movie 1", "libtype": None}) search.assert_called_with(**{"title": "Movie 1", "libtype": None})
# TV show searches with pytest.raises(MediaNotFound) as excinfo:
with pytest.raises(HomeAssistantError) 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"}' payload = '{"library_name": "Movies", "title": "Not a Movie"}'
with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest):
assert await hass.services.async_call( assert await hass.services.async_call(
@ -257,8 +268,7 @@ async def test_media_lookups(
}, },
True, True,
) )
assert "Problem in query" in caplog.text assert "Problem in query" in str(excinfo.value)
assert f"Media could not be found: {payload}" in str(excinfo.value)
# Playlist searches # Playlist searches
assert await hass.services.async_call( assert await hass.services.async_call(
@ -272,7 +282,7 @@ async def test_media_lookups(
True, True,
) )
with pytest.raises(HomeAssistantError) as excinfo: with pytest.raises(MediaNotFound) as excinfo:
payload = '{"playlist_name": "Not a Playlist"}' payload = '{"playlist_name": "Not a Playlist"}'
assert await hass.services.async_call( assert await hass.services.async_call(
MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_DOMAIN,
@ -284,10 +294,9 @@ async def test_media_lookups(
}, },
True, True,
) )
assert "Playlist 'Not a Playlist' not found" in caplog.text assert "Playlist 'Not a Playlist' not found" in str(excinfo.value)
assert f"Media could not be found: {payload}" in str(excinfo.value)
with pytest.raises(HomeAssistantError) as excinfo: with pytest.raises(MediaNotFound) as excinfo:
payload = "{}" payload = "{}"
assert await hass.services.async_call( assert await hass.services.async_call(
MEDIA_PLAYER_DOMAIN, MEDIA_PLAYER_DOMAIN,
@ -299,5 +308,4 @@ async def test_media_lookups(
}, },
True, True,
) )
assert "Must specify 'playlist_name' for this search" in caplog.text assert "Must specify 'playlist_name' for this search" in str(excinfo.value)
assert f"Media could not be found: {payload}" in str(excinfo.value)

View File

@ -43,7 +43,6 @@ async def test_media_player_playback(
requests_mock, requests_mock,
playqueue_created, playqueue_created,
player_plexweb_resources, player_plexweb_resources,
caplog,
): ):
"""Test playing media on a Plex media_player.""" """Test playing media on a Plex media_player."""
requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) 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, 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") movie1 = MockPlexMedia("Movie", "movie")
movie2 = MockPlexMedia("Movie II", "movie") movie2 = MockPlexMedia("Movie II", "movie")
@ -120,8 +119,7 @@ async def test_media_player_playback(
}, },
True, True,
) )
assert f"Media could not be found: {payload}" in str(excinfo.value) assert "Multiple matches, make content_id more specific" in str(excinfo.value)
assert "Multiple matches, make content_id more specific" in caplog.text
# Test multiple choices with allow_multiple # Test multiple choices with allow_multiple
movies = [movie1, movie2, movie3] movies = [movie1, movie2, movie3]

View File

@ -168,7 +168,7 @@ async def test_lookup_media_for_other_integrations(
with patch("plexapi.library.LibrarySection.search", return_value=None): with patch("plexapi.library.LibrarySection.search", return_value=None):
with pytest.raises(HomeAssistantError) as excinfo: with pytest.raises(HomeAssistantError) as excinfo:
lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA) 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 # Test with playqueue
requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234)