Add Browse Media to Xbox (#41776)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
5dbb5f12eb
commit
c3ccea52a5
3 changed files with 214 additions and 0 deletions
|
@ -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
|
||||
|
|
178
homeassistant/components/xbox/browse_media.py
Normal file
178
homeassistant/components/xbox/browse_media.py
Normal 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
|
|
@ -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."""
|
||||
|
|
Loading…
Add table
Reference in a new issue