Add Browse Media to Xbox (#41776)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Jason Hunter 2020-10-13 11:51:51 -04:00 committed by GitHub
parent 5dbb5f12eb
commit c3ccea52a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 0 deletions

View file

@ -1004,6 +1004,7 @@ omit =
homeassistant/components/x10/light.py
homeassistant/components/xbox/__init__.py
homeassistant/components/xbox/api.py
homeassistant/components/xbox/browse_media.py
homeassistant/components/xbox/media_player.py
homeassistant/components/xbox_live/sensor.py
homeassistant/components/xeoma/camera.py

View file

@ -0,0 +1,178 @@
"""Support for media browsing."""
from typing import Dict, List, Optional
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP
from xbox.webapi.api.provider.catalog.models import (
AlternateIdType,
CatalogResponse,
FieldsTemplate,
Image,
)
from xbox.webapi.api.provider.smartglass.models import (
InstalledPackage,
InstalledPackagesList,
)
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_GAME,
MEDIA_TYPE_APP,
MEDIA_TYPE_GAME,
)
TYPE_MAP = {
"App": {
"type": MEDIA_TYPE_APP,
"class": MEDIA_CLASS_APP,
},
"Game": {
"type": MEDIA_TYPE_GAME,
"class": MEDIA_CLASS_GAME,
},
}
async def build_item_response(
client: XboxLiveClient,
device_id: str,
tv_configured: bool,
media_content_type: str,
media_content_id: str,
) -> Optional[BrowseMedia]:
"""Create response payload for the provided media query."""
apps: InstalledPackagesList = await client.smartglass.get_installed_apps(device_id)
if media_content_type in [None, "library"]:
library_info = BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
title="Installed Applications",
can_play=False,
can_expand=True,
children=[],
)
# Add Home
id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID
home_catalog: CatalogResponse = (
await client.catalog.get_product_from_alternate_id(
HOME_APP_IDS[id_type], id_type
)
)
home_thumb = _find_media_image(
home_catalog.products[0].localized_properties[0].images
)
library_info.children.append(
BrowseMedia(
media_class=MEDIA_CLASS_APP,
media_content_id="Home",
media_content_type=MEDIA_TYPE_APP,
title="Home",
can_play=True,
can_expand=False,
thumbnail=home_thumb.uri,
)
)
# Add TV if configured
if tv_configured:
tv_catalog: CatalogResponse = (
await client.catalog.get_product_from_alternate_id(
SYSTEM_PFN_ID_MAP["Microsoft.Xbox.LiveTV_8wekyb3d8bbwe"][id_type],
id_type,
)
)
tv_thumb = _find_media_image(
tv_catalog.products[0].localized_properties[0].images
)
library_info.children.append(
BrowseMedia(
media_class=MEDIA_CLASS_APP,
media_content_id="TV",
media_content_type=MEDIA_TYPE_APP,
title="Live TV",
can_play=True,
can_expand=False,
thumbnail=tv_thumb.uri,
)
)
content_types = sorted(
{app.content_type for app in apps.result if app.content_type in TYPE_MAP}
)
for c_type in content_types:
library_info.children.append(
BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=c_type,
media_content_type=TYPE_MAP[c_type]["type"],
title=f"{c_type}s",
can_play=False,
can_expand=True,
children_media_class=TYPE_MAP[c_type]["class"],
)
)
return library_info
app_details = await client.catalog.get_products(
[
app.one_store_product_id
for app in apps.result
if app.content_type == media_content_id and app.one_store_product_id
],
FieldsTemplate.BROWSE,
)
images = {
prod.product_id: prod.localized_properties[0].images
for prod in app_details.products
}
return BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=media_content_id,
media_content_type=media_content_type,
title=f"{media_content_id}s",
can_play=False,
can_expand=True,
children=[
item_payload(app, images)
for app in apps.result
if app.content_type == media_content_id and app.one_store_product_id
],
children_media_class=TYPE_MAP[media_content_id]["class"],
)
def item_payload(item: InstalledPackage, images: Dict[str, List[Image]]):
"""Create response payload for a single media item."""
thumbnail = None
image = _find_media_image(images.get(item.one_store_product_id, []))
if image is not None:
thumbnail = image.uri
if thumbnail[0] == "/":
thumbnail = f"https:{thumbnail}"
return BrowseMedia(
media_class=TYPE_MAP[item.content_type]["class"],
media_content_id=item.one_store_product_id,
media_content_type=TYPE_MAP[item.content_type]["type"],
title=item.name,
can_play=True,
can_expand=False,
thumbnail=thumbnail,
)
def _find_media_image(images=List[Image]) -> Optional[Image]:
purpose_order = ["Poster", "Tile", "Logo", "BoxArt"]
for purpose in purpose_order:
for image in images:
if image.image_purpose == purpose and image.width >= 300:
return image
return None

View file

@ -19,9 +19,11 @@ from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_APP,
MEDIA_TYPE_GAME,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
@ -30,6 +32,7 @@ from homeassistant.components.media_player.const import (
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING
from .browse_media import build_item_response
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -43,6 +46,8 @@ SUPPORT_XBOX = (
| SUPPORT_PAUSE
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_MUTE
| SUPPORT_BROWSE_MEDIA
| SUPPORT_PLAY_MEDIA
)
XBOX_STATE_MAP = {
@ -60,6 +65,11 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Xbox media_player from a config entry."""
client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]
consoles: SmartglassConsoleList = await client.smartglass.get_console_list()
_LOGGER.debug(
"Found %d consoles: %s",
len(consoles.result),
consoles.dict(),
)
async_add_entities(
[XboxMediaPlayer(client, console) for console in consoles.result], True
)
@ -146,6 +156,12 @@ class XboxMediaPlayer(MediaPlayerEntity):
await self.client.smartglass.get_console_status(self._console.id)
)
_LOGGER.debug(
"%s status: %s",
self._console.name,
status.dict(),
)
if status.focus_app_aumid:
if (
not self._console_status
@ -216,6 +232,25 @@ class XboxMediaPlayer(MediaPlayerEntity):
"""Send next track command."""
await self.client.smartglass.next(self._console.id)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
return await build_item_response(
self.client,
self._console.id,
self._console_status.is_tv_configured,
media_content_type,
media_content_id,
)
async def async_play_media(self, media_type, media_id, **kwargs):
"""Launch an app on the Xbox."""
if media_id == "Home":
await self.client.smartglass.go_home(self._console.id)
elif media_id == "TV":
await self.client.smartglass.show_tv_guide(self._console.id)
else:
await self.client.smartglass.launch_app(self._console.id, media_id)
@property
def device_info(self):
"""Return a device description for device registry."""