Support multiple Plex servers in media browser (#68321)
This commit is contained in:
parent
16655c4ccc
commit
653305b998
12 changed files with 436 additions and 221 deletions
|
@ -896,6 +896,7 @@ omit =
|
|||
homeassistant/components/plaato/const.py
|
||||
homeassistant/components/plaato/entity.py
|
||||
homeassistant/components/plaato/sensor.py
|
||||
homeassistant/components/plex/cast.py
|
||||
homeassistant/components/plex/media_player.py
|
||||
homeassistant/components/plex/view.py
|
||||
homeassistant/components/plugwise/select.py
|
||||
|
|
|
@ -69,7 +69,7 @@ async def async_browse_media(hass, media_content_type, media_content_id, platfor
|
|||
return await hass.async_add_executor_job(
|
||||
partial(
|
||||
browse_media,
|
||||
plex_server,
|
||||
hass,
|
||||
is_internal,
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
|
|
|
@ -10,8 +10,7 @@ from homeassistant.components.media_player.const import MEDIA_CLASS_APP
|
|||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import async_browse_media as async_browse_plex_media, is_plex_media_id
|
||||
from .const import PLEX_URI_SCHEME
|
||||
from .services import lookup_plex_media
|
||||
from .services import process_plex_payload
|
||||
|
||||
|
||||
async def async_get_media_browser_root_object(
|
||||
|
@ -51,13 +50,10 @@ def _play_media(
|
|||
hass: HomeAssistant, chromecast: Chromecast, media_type: str, media_id: str
|
||||
) -> None:
|
||||
"""Play media."""
|
||||
media_id = media_id[len(PLEX_URI_SCHEME) :]
|
||||
media = lookup_plex_media(hass, media_type, media_id)
|
||||
if media is None:
|
||||
return
|
||||
result = process_plex_payload(hass, media_type, media_id)
|
||||
controller = PlexController()
|
||||
chromecast.register_handler(controller)
|
||||
controller.play_media(media)
|
||||
controller.play_media(result.media, offset=result.offset)
|
||||
|
||||
|
||||
async def async_play_media(
|
||||
|
@ -68,7 +64,7 @@ async def async_play_media(
|
|||
media_id: str,
|
||||
) -> bool:
|
||||
"""Play media."""
|
||||
if media_id and media_id.startswith(PLEX_URI_SCHEME):
|
||||
if is_plex_media_id(media_id):
|
||||
await hass.async_add_executor_job(
|
||||
_play_media, hass, chromecast, media_type, media_id
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Support to interface with the Plex API."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia
|
||||
from homeassistant.components.media_player.const import (
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.components.media_player.const import (
|
|||
)
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
|
||||
from .const import DOMAIN, PLEX_URI_SCHEME
|
||||
from .const import DOMAIN, SERVERS
|
||||
from .errors import MediaNotFound
|
||||
from .helpers import pretty_title
|
||||
|
||||
|
@ -27,16 +27,7 @@ class UnknownMediaType(BrowseError):
|
|||
"""Unknown media type."""
|
||||
|
||||
|
||||
HUB_PREFIX = "hub:"
|
||||
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
|
||||
PLAYLISTS_BROWSE_PAYLOAD = {
|
||||
"title": "Playlists",
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": PLEX_URI_SCHEME + "all",
|
||||
"media_content_type": "playlists",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
}
|
||||
ITEM_TYPE_MEDIA_CLASS = {
|
||||
"album": MEDIA_CLASS_ALBUM,
|
||||
"artist": MEDIA_CLASS_ARTIST,
|
||||
|
@ -52,25 +43,39 @@ ITEM_TYPE_MEDIA_CLASS = {
|
|||
"video": MEDIA_CLASS_VIDEO,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def browse_media( # noqa: C901
|
||||
plex_server, is_internal, media_content_type, media_content_id, *, platform=None
|
||||
hass, is_internal, media_content_type, media_content_id, *, platform=None
|
||||
):
|
||||
"""Implement the websocket media browsing helper."""
|
||||
server_id = None
|
||||
plex_server = None
|
||||
special_folder = None
|
||||
|
||||
if media_content_id:
|
||||
url = URL(media_content_id)
|
||||
server_id = url.host
|
||||
plex_server = hass.data[DOMAIN][SERVERS][server_id]
|
||||
if media_content_type == "hub":
|
||||
_, hub_location, hub_identifier = url.parts
|
||||
elif media_content_type in ["library", "server"] and len(url.parts) > 2:
|
||||
_, media_content_id, special_folder = url.parts
|
||||
else:
|
||||
media_content_id = url.name
|
||||
|
||||
if media_content_type in ("plex_root", None):
|
||||
return root_payload(hass, is_internal, platform=platform)
|
||||
|
||||
def item_payload(item, short_name=False):
|
||||
"""Create response payload for a single media item."""
|
||||
try:
|
||||
media_class = ITEM_TYPE_MEDIA_CLASS[item.type]
|
||||
except KeyError as err:
|
||||
_LOGGER.debug("Unknown type received: %s", item.type)
|
||||
raise UnknownMediaType from err
|
||||
raise UnknownMediaType("Unknown type received: {item.type}") from err
|
||||
payload = {
|
||||
"title": pretty_title(item, short_name),
|
||||
"media_class": media_class,
|
||||
"media_content_id": PLEX_URI_SCHEME + str(item.ratingKey),
|
||||
"media_content_id": generate_plex_uri(server_id, item.ratingKey),
|
||||
"media_content_type": item.type,
|
||||
"can_play": True,
|
||||
"can_expand": item.type in EXPANDABLES,
|
||||
|
@ -81,16 +86,41 @@ def browse_media( # noqa: C901
|
|||
thumbnail = item.thumbUrl
|
||||
else:
|
||||
thumbnail = get_proxy_image_url(
|
||||
plex_server.machine_identifier,
|
||||
server_id,
|
||||
item.ratingKey,
|
||||
)
|
||||
payload["thumbnail"] = thumbnail
|
||||
|
||||
return BrowseMedia(**payload)
|
||||
|
||||
def library_payload(library_id):
|
||||
def server_payload():
|
||||
"""Create response payload to describe libraries of the Plex server."""
|
||||
server_info = BrowseMedia(
|
||||
title=plex_server.friendly_name,
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id=generate_plex_uri(server_id, "server"),
|
||||
media_content_type="server",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[],
|
||||
children_media_class=MEDIA_CLASS_DIRECTORY,
|
||||
thumbnail="https://brands.home-assistant.io/_/plex/logo.png",
|
||||
)
|
||||
if platform != "sonos":
|
||||
server_info.children.append(
|
||||
special_library_payload(server_info, "Recommended")
|
||||
)
|
||||
for library in plex_server.library.sections():
|
||||
if library.type == "photo":
|
||||
continue
|
||||
if library.type != "artist" and platform == "sonos":
|
||||
continue
|
||||
server_info.children.append(library_section_payload(library))
|
||||
server_info.children.append(playlists_payload())
|
||||
return server_info
|
||||
|
||||
def library_contents(library):
|
||||
"""Create response payload to describe contents of a specific library."""
|
||||
library = plex_server.library.sectionByID(library_id)
|
||||
library_info = library_section_payload(library)
|
||||
library_info.children = [special_library_payload(library_info, "Recommended")]
|
||||
for item in library.all():
|
||||
|
@ -100,9 +130,17 @@ def browse_media( # noqa: C901
|
|||
continue
|
||||
return library_info
|
||||
|
||||
def playlists_payload(platform):
|
||||
def playlists_payload():
|
||||
"""Create response payload for all available playlists."""
|
||||
playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []}
|
||||
playlists_info = {
|
||||
"title": "Playlists",
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": generate_plex_uri(server_id, "all"),
|
||||
"media_content_type": "playlists",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
"children": [],
|
||||
}
|
||||
for playlist in plex_server.playlists():
|
||||
if playlist.playlistType != "audio" and platform == "sonos":
|
||||
continue
|
||||
|
@ -137,35 +175,29 @@ def browse_media( # noqa: C901
|
|||
continue
|
||||
return media_info
|
||||
|
||||
if media_content_id:
|
||||
assert media_content_id.startswith(PLEX_URI_SCHEME)
|
||||
media_content_id = media_content_id[len(PLEX_URI_SCHEME) :]
|
||||
|
||||
if media_content_id and media_content_id.startswith(HUB_PREFIX):
|
||||
media_content_id = media_content_id[len(HUB_PREFIX) :]
|
||||
location, hub_identifier = media_content_id.split(":")
|
||||
if location == "server":
|
||||
if media_content_type == "hub":
|
||||
if hub_location == "server":
|
||||
hub = next(
|
||||
x
|
||||
for x in plex_server.library.hubs()
|
||||
if x.hubIdentifier == hub_identifier
|
||||
)
|
||||
media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}"
|
||||
media_content_id = f"server/{hub.hubIdentifier}"
|
||||
else:
|
||||
library_section = plex_server.library.sectionByID(int(location))
|
||||
library_section = plex_server.library.sectionByID(int(hub_location))
|
||||
hub = next(
|
||||
x for x in library_section.hubs() if x.hubIdentifier == hub_identifier
|
||||
)
|
||||
media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}"
|
||||
media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}"
|
||||
try:
|
||||
children_media_class = ITEM_TYPE_MEDIA_CLASS[hub.type]
|
||||
except KeyError as err:
|
||||
raise BrowseError(f"Unknown type received: {hub.type}") from err
|
||||
raise UnknownMediaType(f"Unknown type received: {hub.type}") from err
|
||||
payload = {
|
||||
"title": hub.title,
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": PLEX_URI_SCHEME + media_content_id,
|
||||
"media_content_type": hub.type,
|
||||
"media_content_id": generate_plex_uri(server_id, media_content_id),
|
||||
"media_content_type": "hub",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
"children": [],
|
||||
|
@ -180,11 +212,6 @@ def browse_media( # noqa: C901
|
|||
payload["children"].append(item_payload(item))
|
||||
return BrowseMedia(**payload)
|
||||
|
||||
if media_content_id and ":" in media_content_id:
|
||||
media_content_id, special_folder = media_content_id.split(":")
|
||||
else:
|
||||
special_folder = None
|
||||
|
||||
if special_folder:
|
||||
if media_content_type == "server":
|
||||
library_or_section = plex_server.library
|
||||
|
@ -196,7 +223,7 @@ def browse_media( # noqa: C901
|
|||
try:
|
||||
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
|
||||
except KeyError as err:
|
||||
raise BrowseError(
|
||||
raise UnknownMediaType(
|
||||
f"Unknown type received: {library_or_section.TYPE}"
|
||||
) from err
|
||||
else:
|
||||
|
@ -207,8 +234,9 @@ def browse_media( # noqa: C901
|
|||
payload = {
|
||||
"title": title,
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": PLEX_URI_SCHEME
|
||||
+ f"{media_content_id}:{special_folder}",
|
||||
"media_content_id": generate_plex_uri(
|
||||
server_id, f"{media_content_id}/{special_folder}"
|
||||
),
|
||||
"media_content_type": media_content_type,
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
|
@ -225,11 +253,13 @@ def browse_media( # noqa: C901
|
|||
return BrowseMedia(**payload)
|
||||
|
||||
try:
|
||||
if media_content_type in ("server", None):
|
||||
return server_payload(plex_server, platform)
|
||||
if media_content_type == "server":
|
||||
return server_payload()
|
||||
|
||||
if media_content_type == "library":
|
||||
return library_payload(int(media_content_id))
|
||||
library_id = int(media_content_id)
|
||||
library = plex_server.library.sectionByID(library_id)
|
||||
return library_contents(library)
|
||||
|
||||
except UnknownMediaType as err:
|
||||
raise BrowseError(
|
||||
|
@ -237,7 +267,7 @@ def browse_media( # noqa: C901
|
|||
) from err
|
||||
|
||||
if media_content_type == "playlists":
|
||||
return playlists_payload(platform)
|
||||
return playlists_payload()
|
||||
|
||||
payload = {
|
||||
"media_type": DOMAIN,
|
||||
|
@ -249,17 +279,61 @@ def browse_media( # noqa: C901
|
|||
return response
|
||||
|
||||
|
||||
def generate_plex_uri(server_id, media_id):
|
||||
"""Create a media_content_id URL for playable Plex media."""
|
||||
if isinstance(media_id, int):
|
||||
media_id = str(media_id)
|
||||
if isinstance(media_id, str) and not media_id.startswith("/"):
|
||||
media_id = f"/{media_id}"
|
||||
return str(
|
||||
URL.build(
|
||||
scheme=DOMAIN,
|
||||
host=server_id,
|
||||
path=media_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def root_payload(hass, is_internal, platform=None):
|
||||
"""Return root payload for Plex."""
|
||||
children = []
|
||||
|
||||
for server_id in hass.data[DOMAIN][SERVERS]:
|
||||
children.append(
|
||||
browse_media(
|
||||
hass,
|
||||
is_internal,
|
||||
"server",
|
||||
generate_plex_uri(server_id, ""),
|
||||
platform=platform,
|
||||
)
|
||||
)
|
||||
|
||||
if len(children) == 1:
|
||||
return children[0]
|
||||
|
||||
return BrowseMedia(
|
||||
title="Plex",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="plex_root",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
def library_section_payload(section):
|
||||
"""Create response payload for a single library section."""
|
||||
try:
|
||||
children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE]
|
||||
except KeyError as err:
|
||||
_LOGGER.debug("Unknown type received: %s", section.TYPE)
|
||||
raise UnknownMediaType from err
|
||||
raise UnknownMediaType(f"Unknown type received: {section.TYPE}") from err
|
||||
server_id = section._server.machineIdentifier # pylint: disable=protected-access
|
||||
return BrowseMedia(
|
||||
title=section.title,
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id=PLEX_URI_SCHEME + str(section.key),
|
||||
media_content_id=generate_plex_uri(server_id, section.key),
|
||||
media_content_type="library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
|
@ -270,10 +344,11 @@ def library_section_payload(section):
|
|||
def special_library_payload(parent_payload, special_type):
|
||||
"""Create response payload for special library folders."""
|
||||
title = f"{special_type} ({parent_payload.title})"
|
||||
special_library_id = f"{parent_payload.media_content_id}/{special_type}"
|
||||
return BrowseMedia(
|
||||
title=title,
|
||||
media_class=parent_payload.media_class,
|
||||
media_content_id=f"{parent_payload.media_content_id}:{special_type}",
|
||||
media_content_id=special_library_id,
|
||||
media_content_type=parent_payload.media_content_type,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
|
@ -281,41 +356,18 @@ def special_library_payload(parent_payload, special_type):
|
|||
)
|
||||
|
||||
|
||||
def server_payload(plex_server, platform):
|
||||
"""Create response payload to describe libraries of the Plex server."""
|
||||
server_info = BrowseMedia(
|
||||
title=plex_server.friendly_name,
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id=PLEX_URI_SCHEME + plex_server.machine_identifier,
|
||||
media_content_type="server",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[],
|
||||
children_media_class=MEDIA_CLASS_DIRECTORY,
|
||||
)
|
||||
if platform != "sonos":
|
||||
server_info.children.append(special_library_payload(server_info, "Recommended"))
|
||||
for library in plex_server.library.sections():
|
||||
if library.type == "photo":
|
||||
continue
|
||||
if library.type != "artist" and platform == "sonos":
|
||||
continue
|
||||
server_info.children.append(library_section_payload(library))
|
||||
server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD))
|
||||
return server_info
|
||||
|
||||
|
||||
def hub_payload(hub):
|
||||
"""Create response payload for a hub."""
|
||||
if hasattr(hub, "librarySectionID"):
|
||||
media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}"
|
||||
media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}"
|
||||
else:
|
||||
media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}"
|
||||
media_content_id = f"server/{hub.hubIdentifier}"
|
||||
server_id = hub._server.machineIdentifier # pylint: disable=protected-access
|
||||
payload = {
|
||||
"title": hub.title,
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": PLEX_URI_SCHEME + media_content_id,
|
||||
"media_content_type": hub.type,
|
||||
"media_content_id": generate_plex_uri(server_id, media_content_id),
|
||||
"media_content_type": "hub",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
}
|
||||
|
@ -324,10 +376,11 @@ def hub_payload(hub):
|
|||
|
||||
def station_payload(station):
|
||||
"""Create response payload for a music station."""
|
||||
server_id = station._server.machineIdentifier # pylint: disable=protected-access
|
||||
return BrowseMedia(
|
||||
title=station.title,
|
||||
media_class=ITEM_TYPE_MEDIA_CLASS[station.type],
|
||||
media_content_id=PLEX_URI_SCHEME + station.key,
|
||||
media_content_id=generate_plex_uri(server_id, station.key),
|
||||
media_content_type="station",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
|
||||
import plexapi.exceptions
|
||||
|
@ -46,12 +45,11 @@ from .const import (
|
|||
PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL,
|
||||
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
|
||||
PLEX_UPDATE_SENSOR_SIGNAL,
|
||||
PLEX_URI_SCHEME,
|
||||
SERVERS,
|
||||
TRANSIENT_DEVICE_MODELS,
|
||||
)
|
||||
from .errors import MediaNotFound
|
||||
from .media_browser import browse_media
|
||||
from .services import process_plex_payload
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -483,51 +481,13 @@ class PlexMediaPlayer(MediaPlayerEntity):
|
|||
f"Client is not currently accepting playback controls: {self.name}"
|
||||
)
|
||||
|
||||
if not self.plex_server.has_token:
|
||||
_LOGGER.warning(
|
||||
"Plex integration configured without a token, playback may fail"
|
||||
)
|
||||
|
||||
if media_id.startswith(PLEX_URI_SCHEME):
|
||||
media_id = media_id[len(PLEX_URI_SCHEME) :]
|
||||
|
||||
if media_type == "station":
|
||||
playqueue = self.plex_server.create_station_playqueue(media_id)
|
||||
try:
|
||||
self.device.playMedia(playqueue)
|
||||
except requests.exceptions.ConnectTimeout as exc:
|
||||
raise HomeAssistantError(
|
||||
f"Request failed when playing on {self.name}"
|
||||
) from exc
|
||||
return
|
||||
|
||||
src = json.loads(media_id)
|
||||
if isinstance(src, int):
|
||||
src = {"plex_key": src}
|
||||
|
||||
offset = 0
|
||||
|
||||
if playqueue_id := src.pop("playqueue_id", None):
|
||||
try:
|
||||
playqueue = self.plex_server.get_playqueue(playqueue_id)
|
||||
except plexapi.exceptions.NotFound as err:
|
||||
raise MediaNotFound(
|
||||
f"PlayQueue '{playqueue_id}' could not be found"
|
||||
) from err
|
||||
else:
|
||||
shuffle = src.pop("shuffle", 0)
|
||||
offset = src.pop("offset", 0) * 1000
|
||||
resume = src.pop("resume", False)
|
||||
media = self.plex_server.lookup_media(media_type, **src)
|
||||
|
||||
if resume and not offset:
|
||||
offset = media.viewOffset
|
||||
|
||||
_LOGGER.debug("Attempting to play %s on %s", media, self.name)
|
||||
playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle)
|
||||
result = process_plex_payload(
|
||||
self.hass, media_type, media_id, default_plex_server=self.plex_server
|
||||
)
|
||||
_LOGGER.debug("Attempting to play %s on %s", result.media, self.name)
|
||||
|
||||
try:
|
||||
self.device.playMedia(playqueue, offset=offset)
|
||||
self.device.playMedia(result.media, offset=result.offset)
|
||||
except requests.exceptions.ConnectTimeout as exc:
|
||||
raise HomeAssistantError(
|
||||
f"Request failed when playing on {self.name}"
|
||||
|
@ -578,7 +538,7 @@ class PlexMediaPlayer(MediaPlayerEntity):
|
|||
is_internal = is_internal_request(self.hass)
|
||||
return await self.hass.async_add_executor_job(
|
||||
browse_media,
|
||||
self.plex_server,
|
||||
self.hass,
|
||||
is_internal,
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Models to represent various Plex objects used in the integration."""
|
||||
from distutils.util import strtobool
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
|
@ -141,3 +142,35 @@ class PlexSession:
|
|||
thumb_url = media.url(media.art)
|
||||
|
||||
return thumb_url
|
||||
|
||||
|
||||
class PlexMediaSearchResult:
|
||||
"""Represents results from a Plex media media_content_id search.
|
||||
|
||||
Results are used by media_player.play_media implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, media, params=None) -> None:
|
||||
"""Initialize the result."""
|
||||
self.media = media
|
||||
self._params = params or {}
|
||||
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
"""Provide the appropriate offset based on payload contents."""
|
||||
if offset := self._params.get("offset", 0):
|
||||
return offset * 1000
|
||||
resume = self._params.get("resume", False)
|
||||
if isinstance(resume, str):
|
||||
resume = bool(strtobool(resume))
|
||||
if resume:
|
||||
return self.media.viewOffset
|
||||
return 0
|
||||
|
||||
@property
|
||||
def shuffle(self) -> bool:
|
||||
"""Return value of shuffle parameter."""
|
||||
shuffle = self._params.get("shuffle", False)
|
||||
if isinstance(shuffle, str):
|
||||
shuffle = bool(strtobool(shuffle))
|
||||
return shuffle
|
||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
|||
|
||||
from plexapi.exceptions import NotFound
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
@ -12,11 +13,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|||
from .const import (
|
||||
DOMAIN,
|
||||
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
||||
PLEX_URI_SCHEME,
|
||||
SERVERS,
|
||||
SERVICE_REFRESH_LIBRARY,
|
||||
SERVICE_SCAN_CLIENTS,
|
||||
)
|
||||
from .errors import MediaNotFound
|
||||
from .models import PlexMediaSearchResult
|
||||
|
||||
REFRESH_LIBRARY_SCHEMA = vol.Schema(
|
||||
{vol.Optional("server_name"): str, vol.Required("library_name"): str}
|
||||
|
@ -73,7 +76,7 @@ def refresh_library(hass: HomeAssistant, service_call: ServiceCall) -> None:
|
|||
library.update()
|
||||
|
||||
|
||||
def get_plex_server(hass, plex_server_name=None):
|
||||
def get_plex_server(hass, plex_server_name=None, plex_server_id=None):
|
||||
"""Retrieve a configured Plex server by name."""
|
||||
if DOMAIN not in hass.data:
|
||||
raise HomeAssistantError("Plex integration not configured")
|
||||
|
@ -81,6 +84,9 @@ def get_plex_server(hass, plex_server_name=None):
|
|||
if not plex_servers:
|
||||
raise HomeAssistantError("No Plex servers available")
|
||||
|
||||
if plex_server_id:
|
||||
return hass.data[DOMAIN][SERVERS][plex_server_id]
|
||||
|
||||
if plex_server_name:
|
||||
plex_server = next(
|
||||
(x for x in plex_servers if x.friendly_name == plex_server_name), None
|
||||
|
@ -101,30 +107,69 @@ def get_plex_server(hass, plex_server_name=None):
|
|||
)
|
||||
|
||||
|
||||
def lookup_plex_media(hass, content_type, content_id):
|
||||
"""Look up Plex media for other integrations using media_player.play_media service payloads."""
|
||||
content = json.loads(content_id)
|
||||
def process_plex_payload(
|
||||
hass, content_type, content_id, default_plex_server=None, supports_playqueues=True
|
||||
) -> PlexMediaSearchResult:
|
||||
"""Look up Plex media using media_player.play_media service payloads."""
|
||||
plex_server = default_plex_server
|
||||
|
||||
if content_id.startswith(PLEX_URI_SCHEME + "{"):
|
||||
# Handle the special payload of 'plex://{<json>}'
|
||||
content_id = content_id[len(PLEX_URI_SCHEME) :]
|
||||
content = json.loads(content_id)
|
||||
elif content_id.startswith(PLEX_URI_SCHEME):
|
||||
# Handle standard media_browser payloads
|
||||
plex_url = URL(content_id)
|
||||
if plex_url.name:
|
||||
if len(plex_url.parts) == 2:
|
||||
# The path contains a single item, will always be a ratingKey
|
||||
content = int(plex_url.name)
|
||||
else:
|
||||
# For "special" items like radio stations
|
||||
content = plex_url.path
|
||||
server_id = plex_url.host
|
||||
plex_server = get_plex_server(hass, plex_server_id=server_id)
|
||||
else:
|
||||
# Handle legacy payloads without server_id in URL host position
|
||||
content = int(plex_url.host) # type: ignore[arg-type]
|
||||
else:
|
||||
content = json.loads(content_id)
|
||||
|
||||
if isinstance(content, dict):
|
||||
if plex_server_name := content.pop("plex_server", None):
|
||||
plex_server = get_plex_server(hass, plex_server_name)
|
||||
|
||||
if not plex_server:
|
||||
plex_server = get_plex_server(hass)
|
||||
|
||||
if content_type == "station":
|
||||
if not supports_playqueues:
|
||||
raise HomeAssistantError("Plex stations are not supported on this device")
|
||||
playqueue = plex_server.create_station_playqueue(content)
|
||||
return PlexMediaSearchResult(playqueue)
|
||||
|
||||
if isinstance(content, int):
|
||||
content = {"plex_key": content}
|
||||
content_type = DOMAIN
|
||||
|
||||
plex_server_name = content.pop("plex_server", None)
|
||||
plex_server = get_plex_server(hass, plex_server_name)
|
||||
|
||||
if playqueue_id := content.pop("playqueue_id", None):
|
||||
if not supports_playqueues:
|
||||
raise HomeAssistantError("Plex playqueues are not supported on this device")
|
||||
try:
|
||||
playqueue = plex_server.get_playqueue(playqueue_id)
|
||||
except NotFound as err:
|
||||
raise MediaNotFound(
|
||||
f"PlayQueue '{playqueue_id}' could not be found"
|
||||
) from err
|
||||
return playqueue
|
||||
return PlexMediaSearchResult(playqueue, content)
|
||||
|
||||
shuffle = content.pop("shuffle", 0)
|
||||
media = plex_server.lookup_media(content_type, **content)
|
||||
search_query = content.copy()
|
||||
shuffle = search_query.pop("shuffle", 0)
|
||||
|
||||
if shuffle:
|
||||
return plex_server.create_playqueue(media, shuffle=shuffle)
|
||||
media = plex_server.lookup_media(content_type, **search_query)
|
||||
|
||||
return media
|
||||
if supports_playqueues and (isinstance(media, list) or shuffle):
|
||||
playqueue = plex_server.create_playqueue(media, shuffle=shuffle)
|
||||
return PlexMediaSearchResult(playqueue, content)
|
||||
|
||||
return PlexMediaSearchResult(media, content)
|
||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||
|
||||
from asyncio import run_coroutine_threadsafe
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
@ -50,7 +49,7 @@ from homeassistant.components.media_player.const import (
|
|||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
from homeassistant.components.plex.const import PLEX_URI_SCHEME
|
||||
from homeassistant.components.plex.services import lookup_plex_media
|
||||
from homeassistant.components.plex.services import process_plex_payload
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
|
@ -567,20 +566,16 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||
soco = self.coordinator.soco
|
||||
if media_id and media_id.startswith(PLEX_URI_SCHEME):
|
||||
plex_plugin = self.speaker.plex_plugin
|
||||
media_id = media_id[len(PLEX_URI_SCHEME) :]
|
||||
payload = json.loads(media_id)
|
||||
if isinstance(payload, dict):
|
||||
shuffle = payload.pop("shuffle", False)
|
||||
else:
|
||||
shuffle = False
|
||||
media = lookup_plex_media(self.hass, media_type, json.dumps(payload))
|
||||
if shuffle:
|
||||
result = process_plex_payload(
|
||||
self.hass, media_type, media_id, supports_playqueues=False
|
||||
)
|
||||
if result.shuffle:
|
||||
self.set_shuffle(True)
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
plex_plugin.add_to_queue(media)
|
||||
plex_plugin.add_to_queue(result.media)
|
||||
else:
|
||||
soco.clear_queue()
|
||||
plex_plugin.add_to_queue(media)
|
||||
plex_plugin.add_to_queue(result.media)
|
||||
soco.play_from_queue(0)
|
||||
return
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Tests for Plex media browser."""
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
|
@ -104,6 +106,7 @@ class MockPlexStation:
|
|||
title = "Radio Station"
|
||||
radio = True
|
||||
type = "playlist"
|
||||
_server = Mock(machineIdentifier="unique_id_123")
|
||||
|
||||
|
||||
async def test_browse_media(
|
||||
|
@ -138,7 +141,7 @@ async def test_browse_media(
|
|||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "server"
|
||||
assert (
|
||||
result[ATTR_MEDIA_CONTENT_ID]
|
||||
== PLEX_URI_SCHEME + DEFAULT_DATA[CONF_SERVER_IDENTIFIER]
|
||||
== PLEX_URI_SCHEME + DEFAULT_DATA[CONF_SERVER_IDENTIFIER] + "/server"
|
||||
)
|
||||
# Library Sections + Recommended + Playlists
|
||||
assert len(result["children"]) == len(mock_plex_server.library.sections()) + 2
|
||||
|
@ -162,7 +165,7 @@ async def test_browse_media(
|
|||
"entity_id": media_players[0],
|
||||
ATTR_MEDIA_CONTENT_TYPE: "server",
|
||||
ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}",
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/server/{special_keys[0]}",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -174,13 +177,14 @@ async def test_browse_media(
|
|||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "server"
|
||||
assert (
|
||||
result[ATTR_MEDIA_CONTENT_ID]
|
||||
== PLEX_URI_SCHEME + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}"
|
||||
== PLEX_URI_SCHEME
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/server/{special_keys[0]}"
|
||||
)
|
||||
assert len(result["children"]) == 4 # Hardcoded in fixture
|
||||
assert result["children"][0]["media_content_type"] == "mixed"
|
||||
assert result["children"][1]["media_content_type"] == "album"
|
||||
assert result["children"][2]["media_content_type"] == "clip"
|
||||
assert result["children"][3]["media_content_type"] == "playlist"
|
||||
assert result["children"][0]["media_content_type"] == "hub"
|
||||
assert result["children"][1]["media_content_type"] == "hub"
|
||||
assert result["children"][2]["media_content_type"] == "hub"
|
||||
assert result["children"][3]["media_content_type"] == "hub"
|
||||
|
||||
# Browse into a special folder (server): Continue Watching
|
||||
msg_id += 1
|
||||
|
@ -199,7 +203,7 @@ async def test_browse_media(
|
|||
assert msg["type"] == TYPE_RESULT
|
||||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "mixed"
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "hub"
|
||||
|
||||
requests_mock.get(
|
||||
f"{mock_plex_server.url_in_use}/hubs/sections/3?includeStations=1",
|
||||
|
@ -216,7 +220,7 @@ async def test_browse_media(
|
|||
"entity_id": media_players[0],
|
||||
ATTR_MEDIA_CONTENT_TYPE: "library",
|
||||
ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME
|
||||
+ f"{library_section_id}:{special_keys[0]}",
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/{library_section_id}/{special_keys[0]}",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -228,7 +232,8 @@ async def test_browse_media(
|
|||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "library"
|
||||
assert (
|
||||
result[ATTR_MEDIA_CONTENT_ID]
|
||||
== PLEX_URI_SCHEME + f"{library_section_id}:{special_keys[0]}"
|
||||
== PLEX_URI_SCHEME
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/{library_section_id}/{special_keys[0]}"
|
||||
)
|
||||
assert len(result["children"]) == 1
|
||||
|
||||
|
@ -249,7 +254,7 @@ async def test_browse_media(
|
|||
assert msg["type"] == TYPE_RESULT
|
||||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "station"
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "hub"
|
||||
assert len(result["children"]) == 3
|
||||
assert result["children"][0]["title"] == "Library Radio"
|
||||
|
||||
|
@ -271,7 +276,7 @@ async def test_browse_media(
|
|||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "library"
|
||||
result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :])
|
||||
result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name)
|
||||
# All items in section + Hubs
|
||||
assert (
|
||||
len(result["children"])
|
||||
|
@ -305,7 +310,7 @@ async def test_browse_media(
|
|||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "show"
|
||||
result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :])
|
||||
result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name)
|
||||
assert result["title"] == mock_plex_server.fetch_item(result_id).title
|
||||
assert result["children"][0]["title"] == f"{mock_season.title} ({mock_season.year})"
|
||||
|
||||
|
@ -335,7 +340,7 @@ async def test_browse_media(
|
|||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "season"
|
||||
result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :])
|
||||
result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name)
|
||||
assert (
|
||||
result["title"]
|
||||
== f"{mock_season.parentTitle} - {mock_season.title} ({mock_season.year})"
|
||||
|
@ -360,7 +365,7 @@ async def test_browse_media(
|
|||
|
||||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :])
|
||||
result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name)
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "library"
|
||||
assert result["title"] == "Music"
|
||||
|
||||
|
@ -390,7 +395,7 @@ async def test_browse_media(
|
|||
assert mock_fetch.called
|
||||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :])
|
||||
result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name)
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "artist"
|
||||
assert result["title"] == mock_artist.title
|
||||
assert result["children"][0]["title"] == "Radio Station"
|
||||
|
@ -411,7 +416,7 @@ async def test_browse_media(
|
|||
|
||||
assert msg["success"]
|
||||
result = msg["result"]
|
||||
result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :])
|
||||
result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name)
|
||||
assert result[ATTR_MEDIA_CONTENT_TYPE] == "album"
|
||||
assert (
|
||||
result["title"]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Tests for Plex player playback methods/services."""
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -11,14 +11,19 @@ from homeassistant.components.media_player.const import (
|
|||
MEDIA_TYPE_MOVIE,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
)
|
||||
from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, PLEX_URI_SCHEME
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DEFAULT_DATA, PLEX_DIRECT_URL
|
||||
|
||||
|
||||
class MockPlexMedia:
|
||||
"""Minimal mock of plexapi media object."""
|
||||
|
||||
key = "key"
|
||||
viewOffset = 333
|
||||
_server = Mock(_baseurl=PLEX_DIRECT_URL)
|
||||
|
||||
def __init__(self, title, mediatype):
|
||||
"""Initialize the instance."""
|
||||
|
@ -51,7 +56,9 @@ async def test_media_player_playback(
|
|||
|
||||
media_player = "media_player.plex_plex_web_chrome"
|
||||
requests_mock.post("/playqueues", text=playqueue_created)
|
||||
requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK)
|
||||
playmedia_mock = requests_mock.get(
|
||||
"/player/playback/playMedia", status_code=HTTPStatus.OK
|
||||
)
|
||||
|
||||
# Test media lookup failure
|
||||
payload = '{"library_name": "Movies", "title": "Movie 1" }'
|
||||
|
@ -67,6 +74,7 @@ async def test_media_player_playback(
|
|||
},
|
||||
True,
|
||||
)
|
||||
assert not playmedia_mock.called
|
||||
assert f"No {MEDIA_TYPE_MOVIE} results in 'Movies' for" in str(excinfo.value)
|
||||
|
||||
movie1 = MockPlexMedia("Movie", "movie")
|
||||
|
@ -86,12 +94,57 @@ async def test_media_player_playback(
|
|||
},
|
||||
True,
|
||||
)
|
||||
assert playmedia_mock.called
|
||||
|
||||
# Test movie success with resume
|
||||
playmedia_mock.reset()
|
||||
with patch("plexapi.library.LibrarySection.search", return_value=movies):
|
||||
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": "Movie 1", "resume": true}',
|
||||
},
|
||||
True,
|
||||
)
|
||||
assert playmedia_mock.called
|
||||
assert playmedia_mock.last_request.qs["offset"][0] == str(movie1.viewOffset)
|
||||
|
||||
# Test movie success with media browser URL
|
||||
playmedia_mock.reset()
|
||||
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: PLEX_URI_SCHEME
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/1",
|
||||
},
|
||||
True,
|
||||
)
|
||||
assert playmedia_mock.called
|
||||
|
||||
# Test movie success with legacy media browser URL
|
||||
playmedia_mock.reset()
|
||||
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: PLEX_URI_SCHEME + "1",
|
||||
},
|
||||
True,
|
||||
)
|
||||
assert playmedia_mock.called
|
||||
|
||||
# Test multiple choices with exact match
|
||||
playmedia_mock.reset()
|
||||
movies = [movie1, movie2]
|
||||
with patch("plexapi.library.LibrarySection.search", return_value=movies), patch(
|
||||
"homeassistant.components.plex.server.PlexServer.create_playqueue"
|
||||
) as mock_create_playqueue:
|
||||
with patch("plexapi.library.LibrarySection.search", return_value=movies):
|
||||
assert await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
|
@ -102,9 +155,10 @@ async def test_media_player_playback(
|
|||
},
|
||||
True,
|
||||
)
|
||||
assert mock_create_playqueue.call_args.args == (movie1,)
|
||||
assert playmedia_mock.called
|
||||
|
||||
# Test multiple choices without exact match
|
||||
playmedia_mock.reset()
|
||||
movies = [movie2, movie3]
|
||||
with pytest.raises(HomeAssistantError) as excinfo:
|
||||
payload = '{"library_name": "Movies", "title": "Movie" }'
|
||||
|
@ -119,6 +173,7 @@ async def test_media_player_playback(
|
|||
},
|
||||
True,
|
||||
)
|
||||
assert not playmedia_mock.called
|
||||
assert "Multiple matches, make content_id more specific" in str(excinfo.value)
|
||||
|
||||
# Test multiple choices with allow_multiple
|
||||
|
@ -137,3 +192,20 @@ async def test_media_player_playback(
|
|||
True,
|
||||
)
|
||||
assert mock_create_playqueue.call_args.args == (movies,)
|
||||
assert playmedia_mock.called
|
||||
|
||||
# Test radio station
|
||||
playmedia_mock.reset()
|
||||
radio_id = "/library/sections/3/stations/1"
|
||||
assert await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: media_player,
|
||||
ATTR_MEDIA_CONTENT_TYPE: "station",
|
||||
ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME
|
||||
+ f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/{radio_id}",
|
||||
},
|
||||
True,
|
||||
)
|
||||
assert playmedia_mock.called
|
||||
|
|
|
@ -13,10 +13,11 @@ from homeassistant.components.plex.const import (
|
|||
CONF_SERVER_IDENTIFIER,
|
||||
DOMAIN,
|
||||
PLEX_SERVER_CONFIG,
|
||||
PLEX_URI_SCHEME,
|
||||
SERVICE_REFRESH_LIBRARY,
|
||||
SERVICE_SCAN_CLIENTS,
|
||||
)
|
||||
from homeassistant.components.plex.services import lookup_plex_media
|
||||
from homeassistant.components.plex.services import process_plex_payload
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
@ -121,19 +122,25 @@ async def test_lookup_media_for_other_integrations(
|
|||
playqueue_created,
|
||||
):
|
||||
"""Test media lookup for media_player.play_media calls from cast/sonos."""
|
||||
CONTENT_ID = '{"library_name": "Music", "artist_name": "Artist"}'
|
||||
CONTENT_ID_KEY = "100"
|
||||
CONTENT_ID_BAD_MEDIA = '{"library_name": "Music", "artist_name": "Not an Artist"}'
|
||||
CONTENT_ID_PLAYQUEUE = '{"playqueue_id": 1234}'
|
||||
CONTENT_ID_BAD_PLAYQUEUE = '{"playqueue_id": 1235}'
|
||||
CONTENT_ID_SERVER = '{"plex_server": "Plex Server 1", "library_name": "Music", "artist_name": "Artist"}'
|
||||
CONTENT_ID = PLEX_URI_SCHEME + '{"library_name": "Music", "artist_name": "Artist"}'
|
||||
CONTENT_ID_KEY = PLEX_URI_SCHEME + "100"
|
||||
CONTENT_ID_BAD_MEDIA = (
|
||||
PLEX_URI_SCHEME + '{"library_name": "Music", "artist_name": "Not an Artist"}'
|
||||
)
|
||||
CONTENT_ID_PLAYQUEUE = PLEX_URI_SCHEME + '{"playqueue_id": 1234}'
|
||||
CONTENT_ID_BAD_PLAYQUEUE = PLEX_URI_SCHEME + '{"playqueue_id": 1235}'
|
||||
CONTENT_ID_SERVER = (
|
||||
PLEX_URI_SCHEME
|
||||
+ '{"plex_server": "Plex Server 1", "library_name": "Music", "artist_name": "Artist"}'
|
||||
)
|
||||
CONTENT_ID_SHUFFLE = (
|
||||
'{"library_name": "Music", "artist_name": "Artist", "shuffle": 1}'
|
||||
PLEX_URI_SCHEME
|
||||
+ '{"library_name": "Music", "artist_name": "Artist", "shuffle": 1}'
|
||||
)
|
||||
|
||||
# Test with no Plex integration available
|
||||
with pytest.raises(HomeAssistantError) as excinfo:
|
||||
lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID)
|
||||
process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID)
|
||||
assert "Plex integration not configured" in str(excinfo.value)
|
||||
|
||||
with patch(
|
||||
|
@ -145,45 +152,61 @@ async def test_lookup_media_for_other_integrations(
|
|||
|
||||
# Test with no Plex servers available
|
||||
with pytest.raises(HomeAssistantError) as excinfo:
|
||||
lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID)
|
||||
process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID)
|
||||
assert "No Plex servers available" in str(excinfo.value)
|
||||
|
||||
# Complete setup of a Plex server
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await setup_plex_server()
|
||||
|
||||
# Test lookup success
|
||||
result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID)
|
||||
assert isinstance(result, plexapi.audio.Artist)
|
||||
# Test lookup success without playqueue
|
||||
result = process_plex_payload(
|
||||
hass, MEDIA_TYPE_MUSIC, CONTENT_ID, supports_playqueues=False
|
||||
)
|
||||
assert isinstance(result.media, plexapi.audio.Artist)
|
||||
assert not result.shuffle
|
||||
|
||||
# Test media key payload
|
||||
result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_KEY)
|
||||
assert isinstance(result, plexapi.audio.Track)
|
||||
# Test media key payload without playqueue
|
||||
result = process_plex_payload(
|
||||
hass, MEDIA_TYPE_MUSIC, CONTENT_ID_KEY, supports_playqueues=False
|
||||
)
|
||||
assert isinstance(result.media, plexapi.audio.Track)
|
||||
assert not result.shuffle
|
||||
|
||||
# Test with specified server
|
||||
result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SERVER)
|
||||
assert isinstance(result, plexapi.audio.Artist)
|
||||
# Test with specified server without playqueue
|
||||
result = process_plex_payload(
|
||||
hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SERVER, supports_playqueues=False
|
||||
)
|
||||
assert isinstance(result.media, plexapi.audio.Artist)
|
||||
assert not result.shuffle
|
||||
|
||||
# Test shuffle without playqueue
|
||||
result = process_plex_payload(
|
||||
hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE, supports_playqueues=False
|
||||
)
|
||||
assert isinstance(result.media, plexapi.audio.Artist)
|
||||
assert result.shuffle
|
||||
|
||||
# Test with media not found
|
||||
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)
|
||||
process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA)
|
||||
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)
|
||||
result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_PLAYQUEUE)
|
||||
assert isinstance(result, plexapi.playqueue.PlayQueue)
|
||||
result = process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_PLAYQUEUE)
|
||||
assert isinstance(result.media, plexapi.playqueue.PlayQueue)
|
||||
|
||||
# Test with invalid playqueue
|
||||
requests_mock.get(
|
||||
"https://1.2.3.4:32400/playQueues/1235", status_code=HTTPStatus.NOT_FOUND
|
||||
)
|
||||
with pytest.raises(HomeAssistantError) as excinfo:
|
||||
lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_PLAYQUEUE)
|
||||
process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_PLAYQUEUE)
|
||||
assert "PlayQueue '1235' could not be found" in str(excinfo.value)
|
||||
|
||||
# Test playqueue is created with shuffle
|
||||
requests_mock.post("/playqueues", text=playqueue_created)
|
||||
result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE)
|
||||
assert isinstance(result, plexapi.playqueue.PlayQueue)
|
||||
result = process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE)
|
||||
assert isinstance(result.media, plexapi.playqueue.PlayQueue)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for the Sonos Media Player platform."""
|
||||
from unittest.mock import patch
|
||||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -10,23 +11,25 @@ from homeassistant.components.media_player.const import (
|
|||
MEDIA_TYPE_MUSIC,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
)
|
||||
from homeassistant.components.plex.const import PLEX_URI_SCHEME
|
||||
from homeassistant.components.plex.const import DOMAIN as PLEX_DOMAIN, PLEX_URI_SCHEME
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
async def test_plex_play_media(hass, async_autosetup_sonos):
|
||||
"""Test playing media via the Plex integration."""
|
||||
mock_plex_server = Mock()
|
||||
mock_lookup = mock_plex_server.lookup_media
|
||||
|
||||
media_player = "media_player.zone_a"
|
||||
media_content_id = (
|
||||
'{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}'
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sonos.media_player.lookup_plex_media"
|
||||
) as mock_lookup, patch(
|
||||
"soco.plugins.plex.PlexPlugin.add_to_queue"
|
||||
) as mock_add_to_queue, patch(
|
||||
"homeassistant.components.plex.services.get_plex_server",
|
||||
return_value=mock_plex_server,
|
||||
), patch("soco.plugins.plex.PlexPlugin.add_to_queue") as mock_add_to_queue, patch(
|
||||
"homeassistant.components.sonos.media_player.SonosMediaPlayerEntity.set_shuffle"
|
||||
) as mock_shuffle:
|
||||
# Test successful Plex service call
|
||||
|
@ -44,8 +47,8 @@ async def test_plex_play_media(hass, async_autosetup_sonos):
|
|||
assert len(mock_lookup.mock_calls) == 1
|
||||
assert len(mock_add_to_queue.mock_calls) == 1
|
||||
assert not mock_shuffle.called
|
||||
assert mock_lookup.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC
|
||||
assert mock_lookup.mock_calls[0][1][2] == media_content_id
|
||||
assert mock_lookup.mock_calls[0][1][0] == MEDIA_TYPE_MUSIC
|
||||
assert mock_lookup.mock_calls[0][2] == json.loads(media_content_id)
|
||||
|
||||
# Test handling shuffle in payload
|
||||
mock_lookup.reset_mock()
|
||||
|
@ -66,8 +69,8 @@ async def test_plex_play_media(hass, async_autosetup_sonos):
|
|||
assert mock_shuffle.called
|
||||
assert len(mock_lookup.mock_calls) == 1
|
||||
assert len(mock_add_to_queue.mock_calls) == 1
|
||||
assert mock_lookup.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC
|
||||
assert mock_lookup.mock_calls[0][1][2] == media_content_id
|
||||
assert mock_lookup.mock_calls[0][1][0] == MEDIA_TYPE_MUSIC
|
||||
assert mock_lookup.mock_calls[0][2] == json.loads(media_content_id)
|
||||
|
||||
# Test failed Plex service call
|
||||
mock_lookup.reset_mock()
|
||||
|
@ -87,3 +90,32 @@ async def test_plex_play_media(hass, async_autosetup_sonos):
|
|||
)
|
||||
assert mock_lookup.called
|
||||
assert not mock_add_to_queue.called
|
||||
|
||||
# Test new media browser payload format
|
||||
mock_lookup.reset_mock()
|
||||
mock_lookup.side_effect = None
|
||||
mock_add_to_queue.reset_mock()
|
||||
|
||||
server_id = "unique_id_123"
|
||||
plex_item_key = 300
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.plex.services.get_plex_server",
|
||||
return_value=mock_plex_server,
|
||||
):
|
||||
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: f"{PLEX_URI_SCHEME}{server_id}/{plex_item_key}?shuffle=1",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_lookup.mock_calls) == 1
|
||||
assert len(mock_add_to_queue.mock_calls) == 1
|
||||
assert mock_shuffle.called
|
||||
assert mock_lookup.mock_calls[0][1][0] == PLEX_DOMAIN
|
||||
assert mock_lookup.mock_calls[0][2] == {"plex_key": plex_item_key}
|
||||
|
|
Loading…
Add table
Reference in a new issue