fix(media_player): D1 — browse builds real BrowseMedia nodes, not BrowseMediaSource (t37)

`homeassistant.components.media_player` has not exported `BrowseMediaSource`
for years: the class lives in `homeassistant.components.media_source.models`,
where it exists to build `media_source://<domain>` ids and is the only place a
`domain` parameter belongs (t30 G18's note was about THAT class). The local
import therefore raised ImportError, and with it every real browse failed —
while the suite stayed green because the harness stubbed a `BrowseMediaSource`
that real HA does not expose from there.

`async_browse_media` now builds the standard tree exactly as core's own
androidtv_remote/media_player.py does: `BrowseMedia` imported from
homeassistant.components.media_player at module scope, every node carrying an
explicit media_content_id/media_content_type (MediaType.APP/APPS),
`children_media_class=MediaClass.APP` on the directory, and no `domain`.
Verified against a real install: the old import raises ImportError, the old
`domain=` kwarg raises TypeError, and the new node constructs.

The harness is now faithful so this cannot hide again:
* `entity_stublib` exposes a `BrowseMedia` with the real keyword-only
  signature, `as_dict()` and `calculate_children_class()`, plus `MediaType`,
  and deliberately NOT `BrowseMediaSource` — so both the drifted import and the
  drifted kwarg fail in-suite;
* the app-directory test asserts the standard shape (classes, explicit ids and
  types, children_media_class, the serialised payload) instead of the removed
  `domain` attribute;
* TestFaithfulHaStubs pins the harness contract on the AST (no production
  module imports BrowseMediaSource) rather than on prose.

Verified: 284 passed, 2 skipped (D1 stage); ruff clean. Full gate re-run on the
completed branch: 289 passed / 2 skipped and 290 passed / 1 skipped.
This commit is contained in:
James Mackay committed 2026-10-10 21:19:53 +01:00
1 parent 5ede4fe722
commit 37b2accfda
3 files changed
+160 -28

No files matched your search

+20 -13
View File
@@ -26,10 +26,13 @@ import time
import voluptuous as vol
from homeassistant.components.media_player import (
BrowseMedia,
MediaClass,
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_PAUSED, STATE_PLAYING
@@ -961,36 +964,40 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity):
# ── BrowseMedia app directory (P4-2c) ──
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Root lists the apps map (or the curated default) → play_media(app)."""
from homeassistant.components.media_player import (
BrowseMediaSource,
MediaClass,
)
"""Root lists the apps map (or the curated default) → play_media(app).
t37 D1: the standard media_player tree, exactly as core's own
androidtv_remote/media_player.py builds it — ``BrowseMedia`` from
homeassistant.components.media_player, every node carrying an explicit
media_content_id/media_content_type, and the directory naming the
class of its children. ``BrowseMediaSource`` only ever existed in
homeassistant.components.media_source.models (t30 G18's domain
parameter is ITS contract: it builds ``media_source://<domain>`` ids),
so an app-directory node must never claim one — and the import above
removed a name media_player has not exported for a long time, which
made every real browse raise.
"""
app_map = self._entry.options.get(CONF_APP_NAME_MAP) or DEFAULT_APP_DIRECTORY
# G18 (2026-05-20 blog): BrowseMediaSource must carry the domain
# parameter so the frontend can attribute the media tree.
children = [
BrowseMediaSource(
BrowseMedia(
title=name,
media_class=MediaClass.APP,
media_content_type="app",
media_content_type=MediaType.APP,
media_content_id=pkg,
can_play=True,
can_expand=False,
domain=DOMAIN,
)
for pkg, name in app_map.items()
]
return BrowseMediaSource(
return BrowseMedia(
title="Apps",
media_class=MediaClass.DIRECTORY,
media_content_type="apps",
media_content_type=MediaType.APPS,
media_content_id="apps",
children_media_class=MediaClass.APP,
can_play=False,
can_expand=True,
children=children,
domain=DOMAIN,
)
# ── play_media (§7): channel digits, app/url launch ──