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:
parent
c8821d930e
commit
e6e4c9cf59
7 changed files with 110 additions and 44 deletions
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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, {})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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://"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue