Allow Cast to play Plex media (#41869)

* Allow Cast to play Plex media

* Add Plex to after_dependencies, add missing constant

* Extract function from Sonos service to allow media lookups

* Move to non-async method

* Check if media_id exists

* Add test to make codecov happy
This commit is contained in:
jjlawren 2020-10-19 16:34:22 -05:00 committed by GitHub
parent c8821d930e
commit e6e4c9cf59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 44 deletions

View file

@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==7.5.1"],
"after_dependencies": ["cloud", "http", "media_source", "tts", "zeroconf"],
"after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]
}

View file

@ -9,6 +9,7 @@ from typing import Optional
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.plex import PlexController
from pychromecast.quick_play import quick_play
from pychromecast.socket_client import (
CONNECTION_STATUS_CONNECTED,
@ -38,6 +39,8 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import lookup_plex_media
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
@ -536,6 +539,15 @@ class CastDevice(MediaPlayerEntity):
quick_play(self._chromecast, app_name, app_data)
except NotImplementedError:
_LOGGER.error("App %s not supported", app_name)
# Handle plex
elif media_id and media_id.startswith(PLEX_URI_SCHEME):
media_id = media_id[len(PLEX_URI_SCHEME) :]
media, _ = lookup_plex_media(self.hass, media_type, media_id)
if media is None:
return
controller = PlexController()
self._chromecast.register_handler(controller)
controller.play_media(media)
else:
self._chromecast.media_controller.play_media(
media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})

View file

@ -2,7 +2,6 @@
import asyncio
import functools
from functools import partial
import json
import logging
import plexapi.exceptions
@ -58,7 +57,7 @@ from .const import (
)
from .errors import ShouldUpdateConfigEntry
from .server import PlexServer
from .services import async_setup_services
from .services import async_setup_services, lookup_plex_media
_LOGGER = logging.getLogger(__package__)
@ -286,7 +285,7 @@ def play_on_sonos(hass, service_call):
"""Play Plex media on a linked Sonos device."""
entity_id = service_call.data[ATTR_ENTITY_ID]
content_id = service_call.data[ATTR_MEDIA_CONTENT_ID]
content = json.loads(content_id)
content_type = service_call.data.get(ATTR_MEDIA_CONTENT_TYPE)
sonos = hass.components.sonos
try:
@ -295,27 +294,9 @@ def play_on_sonos(hass, service_call):
_LOGGER.error("Cannot get Sonos device: %s", err)
return
if isinstance(content, int):
content = {"plex_key": content}
content_type = PLEX_DOMAIN
else:
content_type = "music"
plex_server_name = content.get("plex_server")
shuffle = content.pop("shuffle", 0)
plex_servers = hass.data[PLEX_DOMAIN][SERVERS].values()
if plex_server_name:
plex_server = [x for x in plex_servers if x.friendly_name == plex_server_name]
if not plex_server:
_LOGGER.error(
"Requested Plex server '%s' not found in %s",
plex_server_name,
[x.friendly_name for x in plex_servers],
)
return
else:
plex_server = next(iter(plex_servers))
media, plex_server = lookup_plex_media(hass, content_type, content_id)
if media is None:
return
sonos_speaker = plex_server.account.sonos_speaker(sonos_name)
if sonos_speaker is None:
@ -324,11 +305,4 @@ def play_on_sonos(hass, service_call):
)
return
media = plex_server.lookup_media(content_type, **content)
if media is None:
_LOGGER.error("Media could not be found: %s", content)
return
_LOGGER.debug("Attempting to play '%s' on %s", media, sonos_speaker)
playqueue = plex_server.create_playqueue(media, shuffle=shuffle)
sonos_speaker.playMedia(playqueue)
sonos_speaker.playMedia(media)

View file

@ -49,3 +49,5 @@ MANUAL_SETUP_STRING = "Configure Plex server manually"
SERVICE_PLAY_ON_SONOS = "play_on_sonos"
SERVICE_REFRESH_LIBRARY = "refresh_library"
SERVICE_SCAN_CLIENTS = "scan_for_clients"
PLEX_URI_SCHEME = "plex://"

View file

@ -1,4 +1,5 @@
"""Services for the Plex integration."""
import json
import logging
from plexapi.exceptions import NotFound
@ -73,19 +74,47 @@ def get_plex_server(hass, plex_server_name=None):
plex_servers = hass.data[DOMAIN][SERVERS].values()
if plex_server_name:
plex_server = [x for x in plex_servers if x.friendly_name == plex_server_name]
if not plex_server:
_LOGGER.error(
"Requested Plex server '%s' not found in %s",
plex_server_name,
[x.friendly_name for x in plex_servers],
)
return None
elif len(plex_servers) == 1:
plex_server = next(
(x for x in plex_servers if x.friendly_name == plex_server_name), None
)
if plex_server is not None:
return plex_server
_LOGGER.error(
"Requested Plex server '%s' not found in %s",
plex_server_name,
[x.friendly_name for x in plex_servers],
)
return None
if len(plex_servers) == 1:
return next(iter(plex_servers))
_LOGGER.error(
"Multiple Plex servers configured and no selection made: %s",
"Multiple Plex servers configured, choose with 'plex_server' key: %s",
[x.friendly_name for x in plex_servers],
)
return None
def lookup_plex_media(hass, content_type, content_id):
"""Look up Plex media using media_player.play_media service payloads."""
content = json.loads(content_id)
if isinstance(content, int):
content = {"plex_key": content}
content_type = DOMAIN
plex_server_name = content.pop("plex_server", None)
shuffle = content.pop("shuffle", 0)
plex_server = get_plex_server(hass, plex_server_name=plex_server_name)
if not plex_server:
return (None, None)
media = plex_server.lookup_media(content_type, **content)
if media is None:
_LOGGER.error("Media could not be found: %s", content)
return (None, None)
playqueue = plex_server.create_playqueue(media, shuffle=shuffle)
return (playqueue, plex_server)

View file

@ -43,6 +43,16 @@ DEFAULT_DATA = {
},
const.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][const.CONF_SERVER_IDENTIFIER],
}
SECONDARY_DATA = {
const.CONF_SERVER: MOCK_SERVERS[1][const.CONF_SERVER],
const.PLEX_SERVER_CONFIG: {
CONF_CLIENT_ID: "00000000-0000-0000-0000-000000000002",
CONF_TOKEN: MOCK_TOKEN,
CONF_URL: f"https://{MOCK_SERVERS[1][CONF_HOST]}:{MOCK_SERVERS[1][CONF_PORT]}",
CONF_VERIFY_SSL: True,
},
const.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][const.CONF_SERVER_IDENTIFIER],
}
DEFAULT_OPTIONS = {
MP_DOMAIN: {

View file

@ -6,11 +6,19 @@ from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_TYPE,
MEDIA_TYPE_MUSIC,
)
from homeassistant.components.plex.const import DOMAIN, SERVERS, SERVICE_PLAY_ON_SONOS
from homeassistant.components.plex.const import (
CONF_SERVER,
DOMAIN,
SERVERS,
SERVICE_PLAY_ON_SONOS,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
from .const import DEFAULT_OPTIONS, SECONDARY_DATA
from tests.async_mock import patch
from tests.common import MockConfigEntry
async def test_sonos_playback(hass, mock_plex_server):
@ -108,6 +116,8 @@ async def test_sonos_playback(hass, mock_plex_server):
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
), patch(
"plexapi.playqueue.PlayQueue.create"
):
assert await hass.services.async_call(
DOMAIN,
@ -119,3 +129,32 @@ async def test_sonos_playback(hass, mock_plex_server):
},
True,
)
async def test_playback_multiple_servers(hass, mock_websocket, setup_plex_server):
"""Test playing media when multiple servers available."""
secondary_entry = MockConfigEntry(
domain=DOMAIN,
data=SECONDARY_DATA,
options=DEFAULT_OPTIONS,
unique_id=SECONDARY_DATA["server_id"],
)
await setup_plex_server()
await setup_plex_server(config_entry=secondary_entry)
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
), patch("plexapi.playqueue.PlayQueue.create"):
assert await hass.services.async_call(
DOMAIN,
SERVICE_PLAY_ON_SONOS,
{
ATTR_ENTITY_ID: "media_player.sonos_kitchen",
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{SECONDARY_DATA[CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}',
},
True,
)