diff --git a/src/components/ha-selector/ha-selector-media.ts b/src/components/ha-selector/ha-selector-media.ts index 11a55d001d..def2f3d9bc 100644 --- a/src/components/ha-selector/ha-selector-media.ts +++ b/src/components/ha-selector/ha-selector-media.ts @@ -1,6 +1,6 @@ import { mdiPlayBox, mdiPlus } from "@mdi/js"; import type { PropertyValues } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../common/dom/fire_event"; @@ -84,20 +84,30 @@ export class HaMediaSelector extends LitElement { (stateObj && supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); - return html` + const hasAccept = this.selector.media?.accept?.length; + + return html` + ${hasAccept + ? nothing + : html` + + `} ${!supportsBrowse - ? html` + ? html` + ${this.hass.localize( "ui.components.selectors.media.browse_not_supported" )} @@ -107,62 +117,72 @@ export class HaMediaSelector extends LitElement { .data=${this.value} .schema=${MANUAL_SCHEMA} .computeLabel=${this._computeLabelCallback} - >` - : html` -
+ ` + : html` + - ${this.value?.metadata?.thumbnail - ? html` -
+ ${this.value?.metadata?.thumbnail + ? html` +
- ` - : html` -
- -
- `} -
-
- ${!this.value?.media_content_id - ? this.hass.localize("ui.components.selectors.media.pick_media") - : this.value.metadata?.title || this.value.media_content_id} -
-
`}`; + style=${this._thumbnailUrl + ? `background-image: url(${this._thumbnailUrl});` + : ""} + >
+ ` + : html` +
+ +
+ `} + +
+ ${!this.value?.media_content_id + ? this.hass.localize( + "ui.components.selectors.media.pick_media" + ) + : this.value.metadata?.title || this.value.media_content_id} +
+
+ `} + `; } private _computeLabelCallback = ( @@ -184,8 +204,9 @@ export class HaMediaSelector extends LitElement { private _pickMedia() { showMediaBrowserDialog(this, { action: "pick", - entityId: this.value!.entity_id!, - navigateIds: this.value!.metadata?.navigateIds, + entityId: this.value?.entity_id, + navigateIds: this.value?.metadata?.navigateIds, + accept: this.selector.media?.accept, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { fireEvent(this, "value-changed", { value: { diff --git a/src/components/ha-selector/ha-selector-selector.ts b/src/components/ha-selector/ha-selector-selector.ts index af38d908d6..0cb50aa39a 100644 --- a/src/components/ha-selector/ha-selector-selector.ts +++ b/src/components/ha-selector/ha-selector-selector.ts @@ -80,7 +80,16 @@ const SELECTOR_SCHEMAS = { ] as const, icon: [] as const, location: [] as const, - media: [] as const, + media: [ + { + name: "accept", + selector: { + text: { + multiple: true, + }, + }, + }, + ] as const, number: [ { name: "min", diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 7210065897..1254579492 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -164,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement { .navigateIds=${this._navigateIds} .action=${this._action} .preferredLayout=${this._preferredLayout} + .accept=${this._params.accept} @close-dialog=${this.closeDialog} @media-picked=${this._mediaPicked} @media-browsed=${this._mediaBrowsed} diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index aa7e8ad8b0..0a0d6fb3f1 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -78,7 +78,7 @@ export interface MediaPlayerItemId { export class HaMediaPlayerBrowse extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entityId!: string; + @property({ attribute: false }) public entityId?: string; @property() public action: MediaPlayerBrowseAction = "play"; @@ -89,6 +89,8 @@ export class HaMediaPlayerBrowse extends LitElement { @property({ attribute: false }) public navigateIds: MediaPlayerItemId[] = []; + @property({ attribute: false }) public accept?: string[]; + // @todo Consider reworking to eliminate need for attribute since it is manipulated internally @property({ type: Boolean, reflect: true }) public narrow = false; @@ -250,6 +252,7 @@ export class HaMediaPlayerBrowse extends LitElement { }); } else if ( err.code === "entity_not_found" && + this.entityId && isUnavailableState(this.hass.states[this.entityId]?.state) ) { this._setError({ @@ -334,7 +337,37 @@ export class HaMediaPlayerBrowse extends LitElement { const subtitle = this.hass.localize( `ui.components.media-browser.class.${currentItem.media_class}` ); - const children = currentItem.children || []; + let children = currentItem.children || []; + const canPlayChildren = new Set(); + + // Filter children based on accept property if provided + if (this.accept && children.length > 0) { + let checks: ((t: string) => boolean)[] = []; + + for (const type of this.accept) { + if (type.endsWith("/*")) { + const baseType = type.slice(0, -1); + checks.push((t) => t.startsWith(baseType)); + } else if (type === "*") { + checks = [() => true]; + break; + } else { + checks.push((t) => t === type); + } + } + + children = children.filter((child) => { + const contentType = child.media_content_type.toLowerCase(); + const canPlay = + child.media_content_type && + checks.some((check) => check(contentType)); + if (canPlay) { + canPlayChildren.add(child.media_content_id); + } + return !child.media_content_type || child.can_expand || canPlay; + }); + } + const mediaClass = MediaClassBrowserSettings[currentItem.media_class]; const childrenMediaClass = currentItem.children_media_class ? MediaClassBrowserSettings[currentItem.children_media_class] @@ -367,7 +400,12 @@ export class HaMediaPlayerBrowse extends LitElement { "" )}" > - ${this.narrow && currentItem?.can_play + ${this.narrow && + currentItem?.can_play && + (!this.accept || + canPlayChildren.has( + currentItem.media_content_id + )) ? html` { - return entityId !== BROWSER_PLAYER + return entityId && entityId !== BROWSER_PLAYER ? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType) : browseLocalMediaPlayer(this.hass, mediaContentId); } diff --git a/src/components/media-player/show-media-browser-dialog.ts b/src/components/media-player/show-media-browser-dialog.ts index 9136be8db0..5fdfeac789 100644 --- a/src/components/media-player/show-media-browser-dialog.ts +++ b/src/components/media-player/show-media-browser-dialog.ts @@ -7,10 +7,11 @@ import type { MediaPlayerItemId } from "./ha-media-player-browse"; export interface MediaPlayerBrowseDialogParams { action: MediaPlayerBrowseAction; - entityId: string; + entityId?: string; mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void; navigateIds?: MediaPlayerItemId[]; minimumNavigateLevel?: number; + accept?: string[]; } export const showMediaBrowserDialog = ( diff --git a/src/data/selector.ts b/src/data/selector.ts index c76a458a05..abac290475 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -303,7 +303,9 @@ export interface LocationSelectorValue { } export interface MediaSelector { - media: {} | null; + media: { + accept?: string[]; + } | null; } export interface MediaSelectorValue {