Add support for accept keyword in media selector (#25808)

This commit is contained in:
Paulus Schoutsen 2025-06-22 14:02:39 -04:00 committed by GitHub
parent fdd6ccf379
commit 589fa75b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 150 additions and 78 deletions

View File

@ -1,6 +1,6 @@
import { mdiPlayBox, mdiPlus } from "@mdi/js"; import { mdiPlayBox, mdiPlus } from "@mdi/js";
import type { PropertyValues } from "lit"; 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 { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@ -84,11 +84,19 @@ export class HaMediaSelector extends LitElement {
(stateObj && (stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
return html`<ha-entity-picker const hasAccept = this.selector.media?.accept?.length;
return html`
${hasAccept
? nothing
: html`
<ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.entity_id} .value=${this.value?.entity_id}
.label=${this.label || .label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")} this.hass.localize(
"ui.components.selectors.media.pick_media_player"
)}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .helper=${this.helper}
.required=${this.required} .required=${this.required}
@ -96,8 +104,10 @@ export class HaMediaSelector extends LitElement {
allow-custom-entity allow-custom-entity
@value-changed=${this._entityChanged} @value-changed=${this._entityChanged}
></ha-entity-picker> ></ha-entity-picker>
`}
${!supportsBrowse ${!supportsBrowse
? html`<ha-alert> ? html`
<ha-alert>
${this.hass.localize( ${this.hass.localize(
"ui.components.selectors.media.browse_not_supported" "ui.components.selectors.media.browse_not_supported"
)} )}
@ -107,11 +117,15 @@ export class HaMediaSelector extends LitElement {
.data=${this.value} .data=${this.value}
.schema=${MANUAL_SCHEMA} .schema=${MANUAL_SCHEMA}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
></ha-form>` ></ha-form>
: html`<ha-card `
: html`
<ha-card
outlined outlined
@click=${this._pickMedia} @click=${this._pickMedia}
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""} class=${this.disabled || (!this.value?.entity_id && !hasAccept)
? "disabled"
: ""}
> >
<div <div
class="thumbnail ${classMap({ class="thumbnail ${classMap({
@ -147,8 +161,10 @@ export class HaMediaSelector extends LitElement {
? mdiPlus ? mdiPlus
: this.value?.metadata?.media_class : this.value?.metadata?.media_class
? MediaClassBrowserSettings[ ? MediaClassBrowserSettings[
this.value.metadata.media_class === "directory" this.value.metadata.media_class ===
? this.value.metadata.children_media_class || "directory"
? this.value.metadata
.children_media_class ||
this.value.metadata.media_class this.value.metadata.media_class
: this.value.metadata.media_class : this.value.metadata.media_class
].icon ].icon
@ -159,10 +175,14 @@ export class HaMediaSelector extends LitElement {
</div> </div>
<div class="title"> <div class="title">
${!this.value?.media_content_id ${!this.value?.media_content_id
? this.hass.localize("ui.components.selectors.media.pick_media") ? this.hass.localize(
"ui.components.selectors.media.pick_media"
)
: this.value.metadata?.title || this.value.media_content_id} : this.value.metadata?.title || this.value.media_content_id}
</div> </div>
</ha-card>`}`; </ha-card>
`}
`;
} }
private _computeLabelCallback = ( private _computeLabelCallback = (
@ -184,8 +204,9 @@ export class HaMediaSelector extends LitElement {
private _pickMedia() { private _pickMedia() {
showMediaBrowserDialog(this, { showMediaBrowserDialog(this, {
action: "pick", action: "pick",
entityId: this.value!.entity_id!, entityId: this.value?.entity_id,
navigateIds: this.value!.metadata?.navigateIds, navigateIds: this.value?.metadata?.navigateIds,
accept: this.selector.media?.accept,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {

View File

@ -80,7 +80,16 @@ const SELECTOR_SCHEMAS = {
] as const, ] as const,
icon: [] as const, icon: [] as const,
location: [] as const, location: [] as const,
media: [] as const, media: [
{
name: "accept",
selector: {
text: {
multiple: true,
},
},
},
] as const,
number: [ number: [
{ {
name: "min", name: "min",

View File

@ -164,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement {
.navigateIds=${this._navigateIds} .navigateIds=${this._navigateIds}
.action=${this._action} .action=${this._action}
.preferredLayout=${this._preferredLayout} .preferredLayout=${this._preferredLayout}
.accept=${this._params.accept}
@close-dialog=${this.closeDialog} @close-dialog=${this.closeDialog}
@media-picked=${this._mediaPicked} @media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed} @media-browsed=${this._mediaBrowsed}

View File

@ -78,7 +78,7 @@ export interface MediaPlayerItemId {
export class HaMediaPlayerBrowse extends LitElement { export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string; @property({ attribute: false }) public entityId?: string;
@property() public action: MediaPlayerBrowseAction = "play"; @property() public action: MediaPlayerBrowseAction = "play";
@ -89,6 +89,8 @@ export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public navigateIds: MediaPlayerItemId[] = []; @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 // @todo Consider reworking to eliminate need for attribute since it is manipulated internally
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean, reflect: true }) public narrow = false;
@ -250,6 +252,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}); });
} else if ( } else if (
err.code === "entity_not_found" && err.code === "entity_not_found" &&
this.entityId &&
isUnavailableState(this.hass.states[this.entityId]?.state) isUnavailableState(this.hass.states[this.entityId]?.state)
) { ) {
this._setError({ this._setError({
@ -334,7 +337,37 @@ export class HaMediaPlayerBrowse extends LitElement {
const subtitle = this.hass.localize( const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}` `ui.components.media-browser.class.${currentItem.media_class}`
); );
const children = currentItem.children || []; let children = currentItem.children || [];
const canPlayChildren = new Set<string>();
// 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 mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[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` ? html`
<ha-fab <ha-fab
mini mini
@ -748,11 +786,11 @@ export class HaMediaPlayerBrowse extends LitElement {
}; };
private async _fetchData( private async _fetchData(
entityId: string, entityId: string | undefined,
mediaContentId?: string, mediaContentId?: string,
mediaContentType?: string mediaContentType?: string
): Promise<MediaPlayerItem> { ): Promise<MediaPlayerItem> {
return entityId !== BROWSER_PLAYER return entityId && entityId !== BROWSER_PLAYER
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType) ? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
: browseLocalMediaPlayer(this.hass, mediaContentId); : browseLocalMediaPlayer(this.hass, mediaContentId);
} }

View File

@ -7,10 +7,11 @@ import type { MediaPlayerItemId } from "./ha-media-player-browse";
export interface MediaPlayerBrowseDialogParams { export interface MediaPlayerBrowseDialogParams {
action: MediaPlayerBrowseAction; action: MediaPlayerBrowseAction;
entityId: string; entityId?: string;
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void; mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
navigateIds?: MediaPlayerItemId[]; navigateIds?: MediaPlayerItemId[];
minimumNavigateLevel?: number; minimumNavigateLevel?: number;
accept?: string[];
} }
export const showMediaBrowserDialog = ( export const showMediaBrowserDialog = (

View File

@ -303,7 +303,9 @@ export interface LocationSelectorValue {
} }
export interface MediaSelector { export interface MediaSelector {
media: {} | null; media: {
accept?: string[];
} | null;
} }
export interface MediaSelectorValue { export interface MediaSelectorValue {