Home Assistant Unofficial Reference 2024.12.1
media_source.py
Go to the documentation of this file.
1 """Xbox Media Source Implementation."""
2 
3 from __future__ import annotations
4 
5 from contextlib import suppress
6 from dataclasses import dataclass
7 
8 from pydantic import ValidationError
9 from xbox.webapi.api.client import XboxLiveClient
10 from xbox.webapi.api.provider.catalog.models import FieldsTemplate, Image
11 from xbox.webapi.api.provider.gameclips.models import GameclipsResponse
12 from xbox.webapi.api.provider.screenshots.models import ScreenshotResponse
13 from xbox.webapi.api.provider.smartglass.models import InstalledPackage
14 
15 from homeassistant.components.media_player import MediaClass
17  BrowseMediaSource,
18  MediaSource,
19  MediaSourceItem,
20  PlayMedia,
21 )
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.util import dt as dt_util
24 
25 from .browse_media import _find_media_image
26 from .const import DOMAIN
27 
28 MIME_TYPE_MAP = {
29  "gameclips": "video/mp4",
30  "screenshots": "image/png",
31 }
32 
33 MEDIA_CLASS_MAP = {
34  "gameclips": MediaClass.VIDEO,
35  "screenshots": MediaClass.IMAGE,
36 }
37 
38 
39 async def async_get_media_source(hass: HomeAssistant):
40  """Set up Xbox media source."""
41  entry = hass.config_entries.async_entries(DOMAIN)[0]
42  client = hass.data[DOMAIN][entry.entry_id]["client"]
43  return XboxSource(hass, client)
44 
45 
46 @callback
48  item: MediaSourceItem,
49 ) -> tuple[str, str, str]:
50  """Parse identifier."""
51  identifier = item.identifier or ""
52  start = ["", "", ""]
53  items = identifier.lstrip("/").split("~~", 2)
54  return tuple(items + start[len(items) :]) # type: ignore[return-value]
55 
56 
57 @dataclass
59  """Represents gameclip/screenshot media."""
60 
61  caption: str
62  thumbnail: str
63  uri: str
64  media_class: str
65 
66 
68  """Provide Xbox screenshots and gameclips as media sources."""
69 
70  name: str = "Xbox Game Media"
71 
72  def __init__(self, hass: HomeAssistant, client: XboxLiveClient) -> None:
73  """Initialize Xbox source."""
74  super().__init__(DOMAIN)
75 
76  self.hass: HomeAssistant = hass
77  self.client: XboxLiveClient = client
78 
79  async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
80  """Resolve media to a url."""
81  _, category, url = async_parse_identifier(item)
82  kind = category.split("#", 1)[1]
83  return PlayMedia(url, MIME_TYPE_MAP[kind])
84 
85  async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
86  """Return media."""
87  title, category, _ = async_parse_identifier(item)
88 
89  if not title:
90  return await self._build_game_library_build_game_library()
91 
92  if not category:
93  return _build_categories(title)
94 
95  return await self._build_media_items_build_media_items(title, category)
96 
97  async def _build_game_library(self):
98  """Display installed games across all consoles."""
99  apps = await self.client.smartglass.get_installed_apps()
100  games = {
101  game.one_store_product_id: game
102  for game in apps.result
103  if game.is_game and game.title_id
104  }
105 
106  app_details = await self.client.catalog.get_products(
107  games.keys(),
108  FieldsTemplate.BROWSE,
109  )
110 
111  images = {
112  prod.product_id: prod.localized_properties[0].images
113  for prod in app_details.products
114  }
115 
116  return BrowseMediaSource(
117  domain=DOMAIN,
118  identifier="",
119  media_class=MediaClass.DIRECTORY,
120  media_content_type="",
121  title="Xbox Game Media",
122  can_play=False,
123  can_expand=True,
124  children=[_build_game_item(game, images) for game in games.values()],
125  children_media_class=MediaClass.GAME,
126  )
127 
128  async def _build_media_items(self, title, category):
129  """Fetch requested gameclip/screenshot media."""
130  title_id, _, thumbnail = title.split("#", 2)
131  owner, kind = category.split("#", 1)
132 
133  items: list[XboxMediaItem] = []
134  with suppress(ValidationError): # Unexpected API response
135  if kind == "gameclips":
136  if owner == "my":
137  response: GameclipsResponse = (
138  await self.client.gameclips.get_recent_clips_by_xuid(
139  self.client.xuid, title_id
140  )
141  )
142  elif owner == "community":
143  response: GameclipsResponse = await self.client.gameclips.get_recent_community_clips_by_title_id(
144  title_id
145  )
146  else:
147  return None
148  items = [
150  item.user_caption
151  or dt_util.as_local(
152  dt_util.parse_datetime(item.date_recorded)
153  ).strftime("%b. %d, %Y %I:%M %p"),
154  item.thumbnails[0].uri,
155  item.game_clip_uris[0].uri,
156  MediaClass.VIDEO,
157  )
158  for item in response.game_clips
159  ]
160  elif kind == "screenshots":
161  if owner == "my":
162  response: ScreenshotResponse = (
163  await self.client.screenshots.get_recent_screenshots_by_xuid(
164  self.client.xuid, title_id
165  )
166  )
167  elif owner == "community":
168  response: ScreenshotResponse = await self.client.screenshots.get_recent_community_screenshots_by_title_id(
169  title_id
170  )
171  else:
172  return None
173  items = [
175  item.user_caption
176  or dt_util.as_local(item.date_taken).strftime(
177  "%b. %d, %Y %I:%M%p"
178  ),
179  item.thumbnails[0].uri,
180  item.screenshot_uris[0].uri,
181  MediaClass.IMAGE,
182  )
183  for item in response.screenshots
184  ]
185 
186  return BrowseMediaSource(
187  domain=DOMAIN,
188  identifier=f"{title}~~{category}",
189  media_class=MediaClass.DIRECTORY,
190  media_content_type="",
191  title=f"{owner.title()} {kind.title()}",
192  can_play=False,
193  can_expand=True,
194  children=[_build_media_item(title, category, item) for item in items],
195  children_media_class=MEDIA_CLASS_MAP[kind],
196  thumbnail=thumbnail,
197  )
198 
199 
200 def _build_game_item(item: InstalledPackage, images: dict[str, list[Image]]):
201  """Build individual game."""
202  thumbnail = ""
203  image = _find_media_image(images.get(item.one_store_product_id, []))
204  if image is not None:
205  thumbnail = image.uri
206  if thumbnail[0] == "/":
207  thumbnail = f"https:{thumbnail}"
208 
209  return BrowseMediaSource(
210  domain=DOMAIN,
211  identifier=f"{item.title_id}#{item.name}#{thumbnail}",
212  media_class=MediaClass.GAME,
213  media_content_type="",
214  title=item.name,
215  can_play=False,
216  can_expand=True,
217  children_media_class=MediaClass.DIRECTORY,
218  thumbnail=thumbnail,
219  )
220 
221 
222 def _build_categories(title):
223  """Build base categories for Xbox media."""
224  _, name, thumbnail = title.split("#", 2)
225  base = BrowseMediaSource(
226  domain=DOMAIN,
227  identifier=f"{title}",
228  media_class=MediaClass.GAME,
229  media_content_type="",
230  title=name,
231  can_play=False,
232  can_expand=True,
233  children=[],
234  children_media_class=MediaClass.DIRECTORY,
235  thumbnail=thumbnail,
236  )
237 
238  owners = ["my", "community"]
239  kinds = ["gameclips", "screenshots"]
240  for owner in owners:
241  for kind in kinds:
242  base.children.append(
244  domain=DOMAIN,
245  identifier=f"{title}~~{owner}#{kind}",
246  media_class=MediaClass.DIRECTORY,
247  media_content_type="",
248  title=f"{owner.title()} {kind.title()}",
249  can_play=False,
250  can_expand=True,
251  children_media_class=MEDIA_CLASS_MAP[kind],
252  )
253  )
254 
255  return base
256 
257 
258 def _build_media_item(title: str, category: str, item: XboxMediaItem):
259  """Build individual media item."""
260  kind = category.split("#", 1)[1]
261  return BrowseMediaSource(
262  domain=DOMAIN,
263  identifier=f"{title}~~{category}~~{item.uri}",
264  media_class=item.media_class,
265  media_content_type=MIME_TYPE_MAP[kind],
266  title=item.caption,
267  can_play=True,
268  can_expand=False,
269  thumbnail=item.thumbnail,
270  )
BrowseMediaSource async_browse_media(self, MediaSourceItem item)
Definition: media_source.py:85
None __init__(self, HomeAssistant hass, XboxLiveClient client)
Definition: media_source.py:72
PlayMedia async_resolve_media(self, MediaSourceItem item)
Definition: media_source.py:79
Image|None _find_media_image(list[Image] images)
def async_get_media_source(HomeAssistant hass)
Definition: media_source.py:39
def _build_game_item(InstalledPackage item, dict[str, list[Image]] images)
tuple[str, str, str] async_parse_identifier(MediaSourceItem item)
Definition: media_source.py:49
def _build_media_item(str title, str category, XboxMediaItem item)