mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 23:27:37 +00:00
Add Browse Media to Xbox (#41776)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
5dbb5f12eb
commit
c3ccea52a5
@ -1004,6 +1004,7 @@ omit =
|
|||||||
homeassistant/components/x10/light.py
|
homeassistant/components/x10/light.py
|
||||||
homeassistant/components/xbox/__init__.py
|
homeassistant/components/xbox/__init__.py
|
||||||
homeassistant/components/xbox/api.py
|
homeassistant/components/xbox/api.py
|
||||||
|
homeassistant/components/xbox/browse_media.py
|
||||||
homeassistant/components/xbox/media_player.py
|
homeassistant/components/xbox/media_player.py
|
||||||
homeassistant/components/xbox_live/sensor.py
|
homeassistant/components/xbox_live/sensor.py
|
||||||
homeassistant/components/xeoma/camera.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 (
|
from homeassistant.components.media_player.const import (
|
||||||
MEDIA_TYPE_APP,
|
MEDIA_TYPE_APP,
|
||||||
MEDIA_TYPE_GAME,
|
MEDIA_TYPE_GAME,
|
||||||
|
SUPPORT_BROWSE_MEDIA,
|
||||||
SUPPORT_NEXT_TRACK,
|
SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE,
|
SUPPORT_PAUSE,
|
||||||
SUPPORT_PLAY,
|
SUPPORT_PLAY,
|
||||||
|
SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_TURN_OFF,
|
SUPPORT_TURN_OFF,
|
||||||
SUPPORT_TURN_ON,
|
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 homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING
|
||||||
|
|
||||||
|
from .browse_media import build_item_response
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -43,6 +46,8 @@ SUPPORT_XBOX = (
|
|||||||
| SUPPORT_PAUSE
|
| SUPPORT_PAUSE
|
||||||
| SUPPORT_VOLUME_STEP
|
| SUPPORT_VOLUME_STEP
|
||||||
| SUPPORT_VOLUME_MUTE
|
| SUPPORT_VOLUME_MUTE
|
||||||
|
| SUPPORT_BROWSE_MEDIA
|
||||||
|
| SUPPORT_PLAY_MEDIA
|
||||||
)
|
)
|
||||||
|
|
||||||
XBOX_STATE_MAP = {
|
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."""
|
"""Set up Xbox media_player from a config entry."""
|
||||||
client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]
|
client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]
|
||||||
consoles: SmartglassConsoleList = await client.smartglass.get_console_list()
|
consoles: SmartglassConsoleList = await client.smartglass.get_console_list()
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Found %d consoles: %s",
|
||||||
|
len(consoles.result),
|
||||||
|
consoles.dict(),
|
||||||
|
)
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[XboxMediaPlayer(client, console) for console in consoles.result], True
|
[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)
|
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 status.focus_app_aumid:
|
||||||
if (
|
if (
|
||||||
not self._console_status
|
not self._console_status
|
||||||
@ -216,6 +232,25 @@ class XboxMediaPlayer(MediaPlayerEntity):
|
|||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
await self.client.smartglass.next(self._console.id)
|
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
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user