Skip to content

Commit 6fa971d

Browse files
authored
Refactor media player browse media in Xbox integration (home-assistant#156672)
1 parent 6deff1c commit 6fa971d

File tree

7 files changed

+1176
-92
lines changed

7 files changed

+1176
-92
lines changed

homeassistant/components/xbox/browse_media.py

Lines changed: 84 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import NamedTuple
5+
from typing import TYPE_CHECKING, NamedTuple
66

77
from pythonxbox.api.client import XboxLiveClient
88
from pythonxbox.api.provider.catalog.const import HOME_APP_IDS
@@ -42,118 +42,114 @@ class MediaTypeDetails(NamedTuple):
4242
async def build_item_response(
4343
client: XboxLiveClient,
4444
device_id: str,
45-
media_content_type: str,
46-
media_content_id: str,
47-
) -> BrowseMedia | None:
45+
media_content_type: MediaType | str | None = None,
46+
media_content_id: str | None = None,
47+
) -> BrowseMedia:
4848
"""Create response payload for the provided media query."""
4949
apps: InstalledPackagesList = await client.smartglass.get_installed_apps(device_id)
5050

51-
if not media_content_type or media_content_type == "library":
52-
children: list[BrowseMedia] = []
53-
library_info = BrowseMedia(
51+
if media_content_type is not None and media_content_id is not None:
52+
app_details = await client.catalog.get_products(
53+
[
54+
app.one_store_product_id
55+
for app in apps.result
56+
if app.content_type == media_content_id and app.one_store_product_id
57+
],
58+
FieldsTemplate.BROWSE,
59+
)
60+
61+
images = {
62+
prod.product_id: prod.localized_properties[0].images
63+
for prod in app_details.products
64+
}
65+
66+
return BrowseMedia(
5467
media_class=MediaClass.DIRECTORY,
55-
media_content_id="library",
56-
media_content_type="library",
57-
title="Installed Applications",
68+
media_content_id=media_content_id,
69+
media_content_type=media_content_type,
70+
title=f"{media_content_id}s",
5871
can_play=False,
5972
can_expand=True,
60-
children=children,
73+
children=[
74+
item_payload(app, images)
75+
for app in apps.result
76+
if app.content_type == media_content_id and app.one_store_product_id
77+
],
78+
children_media_class=TYPE_MAP[media_content_id].cls,
6179
)
6280

63-
# Add Home
64-
id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID
65-
home_catalog: CatalogResponse = (
66-
await client.catalog.get_product_from_alternate_id(
67-
HOME_APP_IDS[id_type], id_type
68-
)
69-
)
70-
home_thumb = _find_media_image(
71-
home_catalog.products[0].localized_properties[0].images
72-
)
73-
children.append(
74-
BrowseMedia(
75-
media_class=MediaClass.APP,
76-
media_content_id="Home",
77-
media_content_type=MediaType.APP,
78-
title="Home",
79-
can_play=True,
80-
can_expand=False,
81-
thumbnail=None if home_thumb is None else home_thumb.uri,
82-
)
83-
)
81+
children: list[BrowseMedia] = []
82+
library_info = BrowseMedia(
83+
media_class=MediaClass.DIRECTORY,
84+
media_content_id="library",
85+
media_content_type="library",
86+
title="Installed Applications",
87+
can_play=False,
88+
can_expand=True,
89+
children=children,
90+
)
8491

85-
content_types = sorted(
86-
{app.content_type for app in apps.result if app.content_type in TYPE_MAP}
87-
)
88-
children.extend(
89-
BrowseMedia(
90-
media_class=MediaClass.DIRECTORY,
91-
media_content_id=c_type,
92-
media_content_type=TYPE_MAP[c_type].type,
93-
title=f"{c_type}s",
94-
can_play=False,
95-
can_expand=True,
96-
children_media_class=TYPE_MAP[c_type].cls,
97-
)
98-
for c_type in content_types
92+
# Add Home
93+
id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID
94+
home_catalog: CatalogResponse = await client.catalog.get_product_from_alternate_id(
95+
HOME_APP_IDS[id_type], id_type
96+
)
97+
home_thumb = _find_media_image(
98+
home_catalog.products[0].localized_properties[0].images
99+
)
100+
children.append(
101+
BrowseMedia(
102+
media_class=MediaClass.APP,
103+
media_content_id="Home",
104+
media_content_type=MediaType.APP,
105+
title="Home",
106+
can_play=True,
107+
can_expand=False,
108+
thumbnail=home_thumb,
99109
)
100-
101-
return library_info
102-
103-
app_details = await client.catalog.get_products(
104-
[
105-
app.one_store_product_id
106-
for app in apps.result
107-
if app.content_type == media_content_id and app.one_store_product_id
108-
],
109-
FieldsTemplate.BROWSE,
110110
)
111111

112-
images = {
113-
prod.product_id: prod.localized_properties[0].images
114-
for prod in app_details.products
115-
}
116-
117-
return BrowseMedia(
118-
media_class=MediaClass.DIRECTORY,
119-
media_content_id=media_content_id,
120-
media_content_type=media_content_type,
121-
title=f"{media_content_id}s",
122-
can_play=False,
123-
can_expand=True,
124-
children=[
125-
item_payload(app, images)
126-
for app in apps.result
127-
if app.content_type == media_content_id and app.one_store_product_id
128-
],
129-
children_media_class=TYPE_MAP[media_content_id].cls,
112+
content_types = sorted(
113+
{app.content_type for app in apps.result if app.content_type in TYPE_MAP}
130114
)
115+
children.extend(
116+
BrowseMedia(
117+
media_class=MediaClass.DIRECTORY,
118+
media_content_id=c_type,
119+
media_content_type=TYPE_MAP[c_type].type,
120+
title=f"{c_type}s",
121+
can_play=False,
122+
can_expand=True,
123+
children_media_class=TYPE_MAP[c_type].cls,
124+
)
125+
for c_type in content_types
126+
)
127+
128+
return library_info
131129

132130

133131
def item_payload(item: InstalledPackage, images: dict[str, list[Image]]) -> BrowseMedia:
134132
"""Create response payload for a single media item."""
135-
thumbnail = None
136-
image = _find_media_image(images.get(item.one_store_product_id, [])) # type: ignore[arg-type]
137-
if image is not None:
138-
thumbnail = image.uri
139-
if thumbnail[0] == "/":
140-
thumbnail = f"https:{thumbnail}"
133+
if TYPE_CHECKING:
134+
assert item.one_store_product_id
135+
assert item.name
141136

142137
return BrowseMedia(
143138
media_class=TYPE_MAP[item.content_type].cls,
144-
media_content_id=item.one_store_product_id, # type: ignore[arg-type]
139+
media_content_id=item.one_store_product_id,
145140
media_content_type=TYPE_MAP[item.content_type].type,
146-
title=item.name, # type: ignore[arg-type]
141+
title=item.name,
147142
can_play=True,
148143
can_expand=False,
149-
thumbnail=thumbnail,
144+
thumbnail=_find_media_image(images.get(item.one_store_product_id, [])),
150145
)
151146

152147

153-
def _find_media_image(images: list[Image]) -> Image | None:
154-
purpose_order = ["Poster", "Tile", "Logo", "BoxArt"]
148+
def _find_media_image(images: list[Image]) -> str | None:
149+
purpose_order = ["BrandedKeyArt", "Poster", "BoxArt", "Tile"]
155150
for purpose in purpose_order:
156-
for image in images:
157-
if image.image_purpose == purpose and image.width >= 300:
158-
return image
151+
if match := next(
152+
(image for image in images if image.image_purpose == purpose), None
153+
):
154+
return f"https:{match.uri}" if match.uri.startswith("/") else match.uri
159155
return None

homeassistant/components/xbox/media_player.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,9 @@ async def async_browse_media(
178178
return await build_item_response(
179179
self.client,
180180
self._console.id,
181-
media_content_type or "",
182-
media_content_id or "",
183-
) # type: ignore[return-value]
181+
media_content_type,
182+
media_content_id,
183+
)
184184

185185
async def async_play_media(
186186
self, media_type: MediaType | str, media_id: str, **kwargs: Any

tests/components/xbox/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pythonxbox.api.provider.people.models import PeopleResponse
1111
from pythonxbox.api.provider.screenshots.models import ScreenshotResponse
1212
from pythonxbox.api.provider.smartglass.models import (
13+
InstalledPackagesList,
1314
SmartglassConsoleList,
1415
SmartglassConsoleStatus,
1516
)
@@ -97,11 +98,17 @@ def mock_xbox_live_client() -> Generator[AsyncMock]:
9798
client.smartglass.get_console_status.return_value = SmartglassConsoleStatus(
9899
**load_json_object_fixture("smartglass_console_status.json", DOMAIN)
99100
)
101+
client.smartglass.get_installed_apps.return_value = InstalledPackagesList(
102+
**load_json_object_fixture("smartglass_installed_applications.json", DOMAIN)
103+
)
100104

101105
client.catalog = AsyncMock()
102106
client.catalog.get_product_from_alternate_id.return_value = CatalogResponse(
103107
**load_json_object_fixture("catalog_product_lookup.json", DOMAIN)
104108
)
109+
client.catalog.get_products.return_value = CatalogResponse(
110+
**load_json_object_fixture("catalog_product_lookup.json", DOMAIN)
111+
)
105112

106113
client.people = AsyncMock()
107114
client.people.get_friends_by_xuid.return_value = PeopleResponse(

0 commit comments

Comments
 (0)