mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Improve Plex media search failure feedback (#67493)
This commit is contained in:
parent
f75d621888
commit
b34da1294c
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user