Allow advanced Plex play_media search options (#56226)

This commit is contained in:
jjlawren 2021-10-24 16:22:16 -05:00 committed by GitHub
parent 6c01ed8d97
commit 0600a21e02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 492 additions and 472 deletions

View file

@ -16,7 +16,3 @@ 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):
"""Media lookup failed for a given search query."""

View file

@ -468,10 +468,10 @@ class PlexMediaPlayer(MediaPlayerEntity):
def play_media(self, media_type, media_id, **kwargs): def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media.""" """Play a piece of media."""
if not (self.device and "playback" in self._device_protocol_capabilities): if not (self.device and "playback" in self._device_protocol_capabilities):
_LOGGER.debug( raise HomeAssistantError(
"Client is not currently accepting playback controls: %s", self.name f"Client is not currently accepting playback controls: {self.name}"
) )
return
if not self.plex_server.has_token: if not self.plex_server.has_token:
_LOGGER.warning( _LOGGER.warning(
"Plex integration configured without a token, playback may fail" "Plex integration configured without a token, playback may fail"
@ -495,16 +495,17 @@ class PlexMediaPlayer(MediaPlayerEntity):
media = self.plex_server.lookup_media(media_type, **src) media = self.plex_server.lookup_media(media_type, **src)
if media is None: if media is None:
_LOGGER.error("Media could not be found: %s", media_id) raise HomeAssistantError(f"Media could not be found: {media_id}")
return
_LOGGER.debug("Attempting to play %s on %s", media, self.name) _LOGGER.debug("Attempting to play %s on %s", media, self.name)
playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle)
try: try:
self.device.playMedia(playqueue) self.device.playMedia(playqueue)
except requests.exceptions.ConnectTimeout: except requests.exceptions.ConnectTimeout as exc:
_LOGGER.error("Timed out playing on %s", self.name) raise HomeAssistantError(
f"Request failed when playing on {self.name}"
) from exc
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):

View file

@ -3,114 +3,82 @@ import logging
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from .errors import MediaNotFound LEGACY_PARAM_MAPPING = {
"show_name": "show.title",
"season_number": "season.index",
"episode_name": "episode.title",
"episode_number": "episode.index",
"artist_name": "artist.title",
"album_name": "album.title",
"track_name": "track.title",
"track_number": "track.index",
"video_name": "movie.title",
}
PREFERRED_LIBTYPE_ORDER = (
"episode",
"season",
"show",
"track",
"album",
"artist",
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def lookup_movie(library_section, **kwargs): def search_media(media_type, library_section, allow_multiple=False, **kwargs):
"""Find a specific movie and return a Plex media object.""" """Search for specified Plex media in the provided library section.
Returns a single media item or None.
If `allow_multiple` is `True`, return a list of matching items.
"""
search_query = {}
libtype = kwargs.pop("libtype", None)
# Preserve legacy service parameters
for legacy_key, key in LEGACY_PARAM_MAPPING.items():
if value := kwargs.pop(legacy_key, None):
_LOGGER.debug(
"Legacy parameter '%s' used, consider using '%s'", legacy_key, key
)
search_query[key] = value
search_query.update(**kwargs)
if not libtype:
# Default to a sane libtype if not explicitly provided
for preferred_libtype in PREFERRED_LIBTYPE_ORDER:
if any(key.startswith(preferred_libtype) for key in search_query):
libtype = preferred_libtype
break
search_query.update(libtype=libtype)
_LOGGER.debug("Processed search query: %s", search_query)
try: try:
title = kwargs["title"] results = library_section.search(**search_query)
except KeyError: except (BadRequest, NotFound) as exc:
_LOGGER.error("Must specify 'title' for this search") _LOGGER.error("Problem in query %s: %s", search_query, exc)
return None return None
try: if not results:
movies = library_section.search(**kwargs, libtype="movie", maxresults=3)
except BadRequest as err:
_LOGGER.error("Invalid search payload provided: %s", err)
return None return None
if not movies: if len(results) > 1:
raise MediaNotFound(f"Movie {title}") from None if allow_multiple:
return results
if len(movies) > 1: if title := search_query.get("title") or search_query.get("movie.title"):
exact_matches = [x for x in movies 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]
match_list = [f"{x.title} ({x.year})" for x in movies] _LOGGER.warning(
_LOGGER.warning("Multiple matches found during search: %s", match_list) "Multiple matches, make content_id more specific or use `allow_multiple`: %s",
results,
)
return None return None
return movies[0] return results[0]
def lookup_tv(library_section, **kwargs):
"""Find TV media and return a Plex media object."""
season_number = kwargs.get("season_number")
episode_number = kwargs.get("episode_number")
try:
show_name = kwargs["show_name"]
show = library_section.get(show_name)
except KeyError:
_LOGGER.error("Must specify 'show_name' for this search")
return None
except NotFound as err:
raise MediaNotFound(f"Show {show_name}") from err
if not season_number:
return show
try:
season = show.season(int(season_number))
except NotFound as err:
raise MediaNotFound(f"Season {season_number} of {show_name}") from err
if not episode_number:
return season
try:
return season.episode(episode=int(episode_number))
except NotFound as err:
episode = f"S{str(season_number).zfill(2)}E{str(episode_number).zfill(2)}"
raise MediaNotFound(f"Episode {episode} of {show_name}") from err
def lookup_music(library_section, **kwargs):
"""Search for music and return a Plex media object."""
album_name = kwargs.get("album_name")
track_name = kwargs.get("track_name")
track_number = kwargs.get("track_number")
try:
artist_name = kwargs["artist_name"]
artist = library_section.get(artist_name)
except KeyError:
_LOGGER.error("Must specify 'artist_name' for this search")
return None
except NotFound as err:
raise MediaNotFound(f"Artist {artist_name}") from err
if album_name:
try:
album = artist.album(album_name)
except NotFound as err:
raise MediaNotFound(f"Album {album_name} by {artist_name}") from err
if track_name:
try:
return album.track(track_name)
except NotFound as err:
raise MediaNotFound(
f"Track {track_name} on {album_name} by {artist_name}"
) from err
if track_number:
for track in album.tracks():
if int(track.index) == int(track_number):
return track
raise MediaNotFound(
f"Track {track_number} on {album_name} by {artist_name}"
) from None
return album
if track_name:
try:
return artist.get(track_name)
except NotFound as err:
raise MediaNotFound(f"Track {track_name} by {artist_name}") from err
return artist

View file

@ -13,13 +13,7 @@ from requests import Session
import requests.exceptions import requests.exceptions
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import MEDIA_TYPE_PLAYLIST
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_VIDEO,
)
from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
@ -47,13 +41,8 @@ from .const import (
X_PLEX_PRODUCT, X_PLEX_PRODUCT,
X_PLEX_VERSION, X_PLEX_VERSION,
) )
from .errors import ( from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry
MediaNotFound, from .media_search import search_media
NoServersFound,
ServerNotSpecified,
ShouldUpdateConfigEntry,
)
from .media_search import lookup_movie, lookup_music, lookup_tv
from .models import PlexSession from .models import PlexSession
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -652,26 +641,7 @@ class PlexServer:
_LOGGER.error("Library '%s' not found", library_name) _LOGGER.error("Library '%s' not found", library_name)
return None return None
try: return search_media(media_type, library_section, **kwargs)
if media_type == MEDIA_TYPE_EPISODE:
return lookup_tv(library_section, **kwargs)
if media_type == MEDIA_TYPE_MOVIE:
return lookup_movie(library_section, **kwargs)
if media_type == MEDIA_TYPE_MUSIC:
return lookup_music(library_section, **kwargs)
if media_type == MEDIA_TYPE_VIDEO:
# Legacy method for compatibility
try:
video_name = kwargs["video_name"]
return library_section.get(video_name)
except KeyError:
_LOGGER.error("Must specify 'video_name' for this search")
return None
except NotFound as err:
raise MediaNotFound(f"Video {video_name}") from err
except MediaNotFound as failed_item:
_LOGGER.error("%s not found in %s", failed_item, library_name)
return None
@property @property
def sensor_attributes(self): def sensor_attributes(self):

View file

@ -0,0 +1,303 @@
"""Tests for Plex server."""
from unittest.mock import patch
from plexapi.exceptions import BadRequest, NotFound
import pytest
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_VIDEO,
SERVICE_PLAY_MEDIA,
)
from homeassistant.components.plex.const import DOMAIN
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
):
"""Test media lookups to Plex server."""
# Plex Key searches
media_player_id = hass.states.async_entity_ids("media_player")[0]
requests_mock.post("/playqueues", text=playqueue_created)
requests_mock.get("/player/playback/playMedia", status_code=200)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: DOMAIN,
ATTR_MEDIA_CONTENT_ID: 1,
},
True,
)
with pytest.raises(HomeAssistantError) as excinfo:
with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound):
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: DOMAIN,
ATTR_MEDIA_CONTENT_ID: 123,
},
True,
)
assert "Media could not be found: 123" in str(excinfo.value)
# TV show searches
with pytest.raises(HomeAssistantError) as excinfo:
payload = '{"library_name": "Not a Library", "show_name": "TV Show"}'
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE,
ATTR_MEDIA_CONTENT_ID: payload,
},
True,
)
assert f"Media could not be found: {payload}" in str(excinfo.value)
with patch("plexapi.library.LibrarySection.search") as search:
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show"}',
},
True,
)
search.assert_called_with(**{"show.title": "TV Show", "libtype": "show"})
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "episode_name": "An Episode"}',
},
True,
)
search.assert_called_with(
**{"episode.title": "An Episode", "libtype": "episode"}
)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1}',
},
True,
)
search.assert_called_with(
**{"show.title": "TV Show", "season.index": 1, "libtype": "season"}
)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 3}',
},
True,
)
search.assert_called_with(
**{
"show.title": "TV Show",
"season.index": 1,
"episode.index": 3,
"libtype": "episode",
}
)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist"}',
},
True,
)
search.assert_called_with(**{"artist.title": "Artist", "libtype": "artist"})
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "album_name": "Album"}',
},
True,
)
search.assert_called_with(**{"album.title": "Album", "libtype": "album"})
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "track_name": "Track 3"}',
},
True,
)
search.assert_called_with(
**{"artist.title": "Artist", "track.title": "Track 3", "libtype": "track"}
)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}',
},
True,
)
search.assert_called_with(
**{"artist.title": "Artist", "album.title": "Album", "libtype": "album"}
)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_number": 3}',
},
True,
)
search.assert_called_with(
**{
"artist.title": "Artist",
"album.title": "Album",
"track.index": 3,
"libtype": "track",
}
)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Track 3"}',
},
True,
)
search.assert_called_with(
**{
"artist.title": "Artist",
"album.title": "Album",
"track.title": "Track 3",
"libtype": "track",
}
)
# Movie searches
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "video_name": "Movie 1"}',
},
True,
)
search.assert_called_with(**{"movie.title": "Movie 1", "libtype": None})
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1"}',
},
True,
)
search.assert_called_with(**{"title": "Movie 1", "libtype": None})
# TV show searches
with pytest.raises(HomeAssistantError) 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(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO,
ATTR_MEDIA_CONTENT_ID: payload,
},
True,
)
assert "Problem in query" in caplog.text
assert f"Media could not be found: {payload}" in str(excinfo.value)
# Playlist searches
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST,
ATTR_MEDIA_CONTENT_ID: '{"playlist_name": "Playlist 1"}',
},
True,
)
with pytest.raises(HomeAssistantError) as excinfo:
payload = '{"playlist_name": "Not a Playlist"}'
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST,
ATTR_MEDIA_CONTENT_ID: payload,
},
True,
)
assert "Playlist 'Not a Playlist' not found" in caplog.text
assert f"Media could not be found: {payload}" in str(excinfo.value)
with pytest.raises(HomeAssistantError) as excinfo:
payload = "{}"
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST,
ATTR_MEDIA_CONTENT_ID: payload,
},
True,
)
assert "Must specify 'playlist_name' for this search" in caplog.text
assert f"Media could not be found: {payload}" in str(excinfo.value)

View file

@ -2,20 +2,48 @@
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import patch from unittest.mock import patch
import pytest
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_TYPE,
DOMAIN as MP_DOMAIN, DOMAIN as MP_DOMAIN,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
) )
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
class MockPlexMedia:
"""Minimal mock of plexapi media object."""
key = "key"
def __init__(self, title, mediatype):
"""Initialize the instance."""
self.listType = mediatype
self.title = title
self.type = mediatype
def section(self):
"""Return the LibrarySection."""
return MockPlexLibrarySection()
class MockPlexLibrarySection:
"""Minimal mock of plexapi LibrarySection."""
uuid = "00000000-0000-0000-0000-000000000000"
async def test_media_player_playback( async def test_media_player_playback(
hass, setup_plex_server, requests_mock, playqueue_created, player_plexweb_resources hass,
setup_plex_server,
requests_mock,
playqueue_created,
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)
@ -26,112 +54,88 @@ async def test_media_player_playback(
requests_mock.post("/playqueues", text=playqueue_created) requests_mock.post("/playqueues", text=playqueue_created)
requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK) requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK)
# Test movie success # Test media lookup failure
assert await hass.services.async_call( payload = '{"library_name": "Movies", "title": "Movie 1" }'
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }',
},
True,
)
# Test movie incomplete dict
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies"}',
},
True,
)
# Test movie failure with options
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }',
},
True,
)
# Test movie failure with nothing found
with patch("plexapi.library.LibrarySection.search", return_value=None): with patch("plexapi.library.LibrarySection.search", return_value=None):
with pytest.raises(HomeAssistantError) as excinfo:
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: payload,
},
True,
)
assert f"Media could not be found: {payload}" in str(excinfo.value)
movie1 = MockPlexMedia("Movie", "movie")
movie2 = MockPlexMedia("Movie II", "movie")
movie3 = MockPlexMedia("Movie III", "movie")
# Test movie success
movies = [movie1]
with patch("plexapi.library.LibrarySection.search", return_value=movies):
assert await hass.services.async_call( assert await hass.services.async_call(
MP_DOMAIN, MP_DOMAIN,
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
{ {
ATTR_ENTITY_ID: media_player, ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }',
}, },
True, True,
) )
# Test movie success with dict # Test multiple choices with exact match
assert await hass.services.async_call( movies = [movie1, movie2]
MP_DOMAIN, with patch("plexapi.library.LibrarySection.search", return_value=movies), patch(
SERVICE_PLAY_MEDIA, "homeassistant.components.plex.server.PlexServer.create_playqueue"
{ ) as mock_create_playqueue:
ATTR_ENTITY_ID: media_player, assert await hass.services.async_call(
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, MP_DOMAIN,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', SERVICE_PLAY_MEDIA,
}, {
True, ATTR_ENTITY_ID: media_player,
) ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie" }',
},
True,
)
assert mock_create_playqueue.call_args.args == (movie1,)
# Test TV show episoe lookup failure # Test multiple choices without exact match
assert await hass.services.async_call( movies = [movie2, movie3]
MP_DOMAIN, with pytest.raises(HomeAssistantError) as excinfo:
SERVICE_PLAY_MEDIA, payload = '{"library_name": "Movies", "title": "Movie" }'
{ with patch("plexapi.library.LibrarySection.search", return_value=movies):
ATTR_ENTITY_ID: media_player, assert await hass.services.async_call(
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, MP_DOMAIN,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 99}', SERVICE_PLAY_MEDIA,
}, {
True, ATTR_ENTITY_ID: media_player,
) ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: payload,
},
True,
)
assert f"Media could not be found: {payload}" in str(excinfo.value)
assert "Multiple matches, make content_id more specific" in caplog.text
# Test track name lookup failure # Test multiple choices with allow_multiple
assert await hass.services.async_call( movies = [movie1, movie2, movie3]
MP_DOMAIN, with patch("plexapi.library.LibrarySection.search", return_value=movies), patch(
SERVICE_PLAY_MEDIA, "homeassistant.components.plex.server.PlexServer.create_playqueue"
{ ) as mock_create_playqueue:
ATTR_ENTITY_ID: media_player, assert await hass.services.async_call(
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, MP_DOMAIN,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Not a track"}', SERVICE_PLAY_MEDIA,
}, {
True, ATTR_ENTITY_ID: media_player,
) ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE,
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie", "allow_multiple": true }',
# Test media lookup failure by key },
requests_mock.get("/library/metadata/999", status_code=HTTPStatus.NOT_FOUND) True,
assert await hass.services.async_call( )
MP_DOMAIN, assert mock_create_playqueue.call_args.args == (movies,)
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: "999",
},
True,
)
# Test invalid Plex server requested
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player,
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}',
},
True,
)

View file

@ -1,22 +1,10 @@
"""Tests for Plex server.""" """Tests for Plex server."""
import copy import copy
from http import HTTPStatus
from unittest.mock import patch from unittest.mock import patch
from plexapi.exceptions import BadRequest, NotFound
from requests.exceptions import ConnectionError, RequestException from requests.exceptions import ConnectionError, RequestException
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_VIDEO,
SERVICE_PLAY_MEDIA,
)
from homeassistant.components.plex.const import ( from homeassistant.components.plex.const import (
CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_NEW_SHARED_USERS,
CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_IGNORE_PLEX_WEB_CLIENTS,
@ -25,7 +13,6 @@ from homeassistant.components.plex.const import (
DOMAIN, DOMAIN,
SERVERS, SERVERS,
) )
from homeassistant.const import ATTR_ENTITY_ID
from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .helpers import trigger_plex_update, wait_for_debouncer from .helpers import trigger_plex_update, wait_for_debouncer
@ -179,215 +166,3 @@ async def test_ignore_plex_web_client(hass, entry, setup_plex_server):
media_players = hass.states.async_entity_ids("media_player") media_players = hass.states.async_entity_ids("media_player")
assert len(media_players) == int(sensor.state) - 1 assert len(media_players) == int(sensor.state) - 1
async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created):
"""Test media lookups to Plex server."""
server_id = mock_plex_server.machine_identifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
# Plex Key searches
media_player_id = hass.states.async_entity_ids("media_player")[0]
requests_mock.post("/playqueues", text=playqueue_created)
requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: DOMAIN,
ATTR_MEDIA_CONTENT_ID: 1,
},
True,
)
with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound):
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: DOMAIN,
ATTR_MEDIA_CONTENT_ID: 123,
},
True,
)
# TV show searches
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show"
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show"
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="TV Shows", episode_name="An Episode"
)
is None
)
assert loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show"
)
assert loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=1,
)
assert loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=1,
episode_number=3,
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
episode_number=1,
)
is None
)
# Music searches
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC, library_name="Music", album_name="Album"
)
is None
)
assert loaded_server.lookup_media(
MEDIA_TYPE_MUSIC, library_name="Music", artist_name="Artist"
)
assert loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
track_name="Track 3",
)
assert loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Album",
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Not an Artist",
album_name="Album",
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Not an Album",
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name=" Album",
track_name="Not a Track",
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
track_name="Not a Track",
)
is None
)
assert loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Album",
track_number=3,
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Album",
track_number=30,
)
is None
)
assert loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Album",
track_name="Track 3",
)
# Playlist searches
assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Playlist 1")
assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST) is None
assert (
loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist")
is None
)
# Legacy Movie searches
assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None
assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None
assert loaded_server.lookup_media(
MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie 1"
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie"
)
is None
)
# Movie searches
assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, title="Movie") is None
assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, library_name="Movies") is None
assert loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie 1"
)
with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie"
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie"
)
) is None

View file

@ -180,10 +180,13 @@ async def test_sonos_play_media(
# Test with speakers available but media not found # Test with speakers available but media not found
content_id_bad_media = '{"library_name": "Music", "artist_name": "Not an Artist"}' content_id_bad_media = '{"library_name": "Music", "artist_name": "Not an Artist"}'
with pytest.raises(HomeAssistantError) as excinfo: with patch("plexapi.library.LibrarySection.search", return_value=None):
play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_bad_media, sonos_speaker_name) with pytest.raises(HomeAssistantError) as excinfo:
assert "Plex media not found" in str(excinfo.value) play_on_sonos(
assert playback_mock.call_count == 3 hass, MEDIA_TYPE_MUSIC, content_id_bad_media, sonos_speaker_name
)
assert "Plex media not found" in str(excinfo.value)
assert playback_mock.call_count == 3
# Test with speakers available and playqueue # Test with speakers available and 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)