Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Browse Media to Xbox #41776

Merged
merged 5 commits into from Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -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
Expand Down
178 changes: 178 additions & 0 deletions homeassistant/components/xbox/browse_media.py
@@ -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
35 changes: 35 additions & 0 deletions homeassistant/components/xbox/media_player.py
Expand Up @@ -19,9 +19,11 @@
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,
Expand All @@ -30,6 +32,7 @@
)
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__)
Expand All @@ -43,6 +46,8 @@
| SUPPORT_PAUSE
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_MUTE
| SUPPORT_BROWSE_MEDIA
| SUPPORT_PLAY_MEDIA
)

XBOX_STATE_MAP = {
Expand All @@ -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
)
Expand Down Expand Up @@ -146,6 +156,12 @@ async def async_update(self) -> None:
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
Expand Down Expand Up @@ -216,6 +232,25 @@ async def async_media_next_track(self):
"""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."""
Expand Down