feat: Allow matching of folders based on parsed date (#2074)

- For: #1748
This commit is contained in:
Dermot Duffy
2025-05-31 14:12:09 -07:00
committed by GitHub
parent dc63dcdab9
commit c55a0f00fd
6 changed files with 248 additions and 88 deletions
+31
View File
@@ -72,6 +72,27 @@ to perform multiple tests. A given match may match multiple items. If an item
does not match, it will not be returned to the user nor (in case of subfolders) does not match, it will not be returned to the user nor (in case of subfolders)
feature in future traversals. feature in future traversals.
?> The higher in the path you can match, the more performant the query.
##### Matcher: `date` / `startdate`
Match if the media was started more recently than the provided date information.
?> Matching based on date requires the media has been parsed with the [`date` parser](#matcher-date--startdate) somewhere above or equal to the position of the matcher in the `path` hierarchy.
```yaml
type: date
# [...]
```
| Parameter | Default | Description |
| --------------- | ------- | ------------------------------------------ |
| `since.minutes` | 0 | Media no older than this many minutes ago. |
| `since.hours` | 0 | Media no older than this many hours ago. |
| `since.days` | 0 | Media no older than this many days ago. |
| `since.months` | 0 | Media no older than this many months ago. |
| `since.years` | 0 | Media no older than this many years ago. |
##### Matcher: `or` ##### Matcher: `or`
Match if any single matcher matches. Match if any single matcher matches.
@@ -300,4 +321,14 @@ folders:
- matchers: - matchers:
- type: template - type: template
value_template: "{{ acc.media.title == now().strftime('%Y/%-m/%d') }}" value_template: "{{ acc.media.title == now().strftime('%Y/%-m/%d') }}"
- parsers:
- type: date
- matchers:
- type: date
since:
minutes: 1
hours: 2
days: 3
months: 4
years: 5
``` ```
+60 -55
View File
@@ -373,9 +373,10 @@ folders:
- {} - {}
``` ```
### Folder Parsing ### Folder date parsing and matching
This example parses dates from a folder, and times from the media items themselves. This example parses dates from a folder, and matches only those dates in the
last two days. It then parses times from the media items themselves.
```yaml ```yaml
type: custom:advanced-camera-card type: custom:advanced-camera-card
@@ -394,13 +395,68 @@ folders:
title: Low title: Low
# Parses the date out of the next level (auto-detected format). # Parses the date out of the next level (auto-detected format).
- parsers: - parsers:
- type: startdate - type: date
matchers:
- type: date
since:
days: 2
# Parses the time out of the items themselves (user-specified format). # Parses the time out of the items themselves (user-specified format).
- parsers: - parsers:
- type: startdate - type: date
format: 'HH:mm:ss' format: 'HH:mm:ss'
``` ```
#### Folder date matching by `template`
This example dynamically includes media from two subfolders, one for today and
one for yesterday both in `%Y/%-m/%d`
[format](https://www.man7.org/linux/man-pages/man3/strftime.3.html).
[Templating](https://www.home-assistant.io/docs/configuration/templating/#time)
is used to dynamically refer to "today" and "yesterday".
?> Using a `date` matcher (as above) should be preferred for matching dates,
this example is included for illustration.
```yaml
type: custom:advanced-camera-card
cameras:
- camera_entity: camera.office
folders:
- type: ha
ha:
url: https://my-ha-instance.local/media-browser/browser/app%2Cmedia-source%3A%2F%2Freolink/playlist%2Cmedia-source%3A%2F%2Freolink%2FCAM%7C01J8XAATNH77WE5D654K07KY1F%7C0
path:
- matchers:
- type: title
title: 'Low resolution'
- matchers:
- type: or
matchers:
- type: template
value_template: "{{ acc.media.title == now().strftime('%Y/%-m/%d') }}"
- type: template
value_template: "{{ acc.media.title == (now() - dt.timedelta(days=1)) | timestamp_custom('%Y/%-m/%d') }}"
- {}
```
### Folder Paths
This example starts with the `media-source://frigate` folder, and looks for a
precisely titled `Clips [my-instance]` folder within that. The resulting media
will be the contents of that folder (if found).
```yaml
type: custom:advanced-camera-card
cameras:
- camera_entity: camera.office
folders:
- type: ha
ha:
path:
- id: 'media-source://frigate'
- title: 'Clips [my-instance]'
```
### Folder URLs ### Folder URLs
This example uses the `url` parameter to establish the root of the query. Within This example uses the `url` parameter to establish the root of the query. Within
@@ -426,57 +482,6 @@ folders:
regexp: 'Person.*' regexp: 'Person.*'
``` ```
### Folder `or` matching
This example dynamically includes media from two subfolders, one for today and
one for yesterday both in `%Y/%-m/%d`
[format](https://www.man7.org/linux/man-pages/man3/strftime.3.html).
[Templating](https://www.home-assistant.io/docs/configuration/templating/#time)
is used to dynamically refer to "today" and "yesterday".
```yaml
type: custom:advanced-camera-card
cameras:
- camera_entity: camera.office
folders:
- type: ha
ha:
url: https://my-ha-instance.local/media-browser/browser/app%2Cmedia-source%3A%2F%2Freolink/playlist%2Cmedia-source%3A%2F%2Freolink%2FCAM%7C01J8XAATNH77WE5D654K07KY1F%7C0
path:
- matchers:
- type: title
title: 'Low resolution'
- parsers:
- type: startdate
matchers:
- type: or
matchers:
- type: template
value_template: "{{ acc.media.title == now().strftime('%Y/%-m/%d') }}"
- type: template
value_template: "{{ acc.media.title == (now() - dt.timedelta(days=1)) | timestamp_custom('%Y/%-m/%d') }}"
- parsers:
- type: startdate
```
### Folder Paths
This example starts with the `media-source://frigate` folder, and looks for a
precisely titled `Clips [my-instance]` folder within that. The resulting media
will be the contents of that folder (if found).
```yaml
type: custom:advanced-camera-card
cameras:
- camera_entity: camera.office
folders:
- type: ha
ha:
path:
- id: 'media-source://frigate'
- title: 'Clips [my-instance]'
```
## Human interaction ## Human interaction
This example will automatically use a HD live substream when This example will automatically use a HD live substream when
+1 -1
View File
@@ -164,7 +164,7 @@ export class HAFoldersEngine implements FoldersEngine {
this._metadataGenerator.generate(media, parent, nextComponent?.ha?.parsers), this._metadataGenerator.generate(media, parent, nextComponent?.ha?.parsers),
...(nextComponent && { ...(nextComponent && {
matcher: (media: BrowseMedia) => matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
this._mediaMatcher.match(hass, media, { this._mediaMatcher.match(hass, media, {
matchers: nextComponent.ha?.matchers, matchers: nextComponent.ha?.matchers,
// Set foldersOnly to true if there are more stages in the path, // Set foldersOnly to true if there are more stages in the path,
@@ -1,6 +1,13 @@
import { sub } from 'date-fns';
import { ConditionState } from '../../../conditions/types'; import { ConditionState } from '../../../conditions/types';
import { Matcher, TemplateMatcher, TitleMatcher } from '../../../config/schema/folders'; import {
import { BrowseMedia } from '../../../ha/browse-media/types'; DateMatcher,
Matcher,
StartDateMatcher,
TemplateMatcher,
TitleMatcher,
} from '../../../config/schema/folders';
import { BrowseMediaMetadata, RichBrowseMedia } from '../../../ha/browse-media/types';
import { HomeAssistant } from '../../../ha/types'; import { HomeAssistant } from '../../../ha/types';
import { regexpExtract } from '../../../utils/regexp-extract'; import { regexpExtract } from '../../../utils/regexp-extract';
import { TemplateRenderer } from '../../templates'; import { TemplateRenderer } from '../../templates';
@@ -11,7 +18,7 @@ export class MediaMatcher {
public match( public match(
hass: HomeAssistant, hass: HomeAssistant,
media: BrowseMedia, media: RichBrowseMedia<BrowseMediaMetadata>,
options?: { options?: {
foldersOnly?: boolean; foldersOnly?: boolean;
matchers?: Matcher[]; matchers?: Matcher[];
@@ -24,16 +31,25 @@ export class MediaMatcher {
for (const matcher of options?.matchers ?? []) { for (const matcher of options?.matchers ?? []) {
switch (matcher.type) { switch (matcher.type) {
case 'date':
case 'startdate':
if (!this._matchStartDate(matcher, media)) {
return false;
}
break;
case 'template': case 'template':
if (!this._matchTemplate(hass, matcher, media, options?.conditionState)) { if (!this._matchTemplate(hass, matcher, media, options?.conditionState)) {
return false; return false;
} }
break; break;
case 'title': case 'title':
if (!this._matchTitle(matcher, media)) { if (!this._matchTitle(matcher, media)) {
return false; return false;
} }
break; break;
case 'or': case 'or':
if ( if (
!matcher.matchers.some((subMatcher) => !matcher.matchers.some((subMatcher) =>
@@ -53,10 +69,28 @@ export class MediaMatcher {
return true; return true;
} }
private _matchStartDate(
matcher: DateMatcher | StartDateMatcher,
media: RichBrowseMedia<BrowseMediaMetadata>,
): boolean {
const startDate = media._metadata?.startDate;
return (
!!startDate &&
startDate >=
sub(new Date(), {
years: matcher.since.years ?? 0,
months: matcher.since.months ?? 0,
days: matcher.since.days ?? 0,
hours: matcher.since.hours ?? 0,
minutes: matcher.since.minutes ?? 0,
})
);
}
private _matchTemplate( private _matchTemplate(
hass: HomeAssistant, hass: HomeAssistant,
matcher: TemplateMatcher, matcher: TemplateMatcher,
media: BrowseMedia, media: RichBrowseMedia<BrowseMediaMetadata>,
conditionState?: ConditionState, conditionState?: ConditionState,
): boolean { ): boolean {
return ( return (
@@ -70,7 +104,10 @@ export class MediaMatcher {
); );
} }
private _matchTitle(matcher: TitleMatcher, media: BrowseMedia): boolean { private _matchTitle(
matcher: TitleMatcher,
media: RichBrowseMedia<BrowseMediaMetadata>,
): boolean {
const valueToMatch = matcher.regexp const valueToMatch = matcher.regexp
? regexpExtract(matcher.regexp, media.title, { groupName: REGEXP_GROUP_VALUE_KEY }) ? regexpExtract(matcher.regexp, media.title, { groupName: REGEXP_GROUP_VALUE_KEY })
: media.title; : media.title;
+24 -6
View File
@@ -14,12 +14,10 @@ const folderConfigDefault = {
ha: {}, ha: {},
}; };
const parserBaseSchema = z.object({ const startdateParserSchema = z.object({
regexp: regexSchema.optional(),
});
const startdateParserSchema = parserBaseSchema.extend({
type: z.literal('startdate'), type: z.literal('startdate'),
format: z.string().optional(), format: z.string().optional(),
regexp: regexSchema.optional(),
}); });
// Simple alias date -> startdate. // Simple alias date -> startdate.
const dateParserSchema = startdateParserSchema.extend({ const dateParserSchema = startdateParserSchema.extend({
@@ -31,13 +29,31 @@ const parserSchema = z.discriminatedUnion('type', [
]); ]);
export type Parser = z.infer<typeof parserSchema>; export type Parser = z.infer<typeof parserSchema>;
const templateMatcherSchema = parserBaseSchema.extend({ const startDateMatcherSchema = z.object({
type: z.literal('startdate'),
since: z.object({
years: z.number().int().min(0).optional(),
months: z.number().int().min(0).optional(),
days: z.number().int().min(0).optional(),
hours: z.number().int().min(0).optional(),
minutes: z.number().int().min(0).optional(),
}),
});
export type StartDateMatcher = z.infer<typeof startDateMatcherSchema>;
// Simple alias date -> startdate.
const dateMatcherSchema = startDateMatcherSchema.extend({
type: z.literal('date'),
});
export type DateMatcher = z.infer<typeof dateMatcherSchema>;
const templateMatcherSchema = z.object({
type: z.literal('template'), type: z.literal('template'),
value_template: z.string(), value_template: z.string(),
}); });
export type TemplateMatcher = z.infer<typeof templateMatcherSchema>; export type TemplateMatcher = z.infer<typeof templateMatcherSchema>;
const titleMatcherSchema = parserBaseSchema.extend({ const titleMatcherSchema = z.object({
type: z.literal('title'), type: z.literal('title'),
regexp: regexSchema.optional(), regexp: regexSchema.optional(),
title: z.string().optional(), title: z.string().optional(),
@@ -53,7 +69,9 @@ const orMatcherSchema: z.ZodSchema<OrMatcher, z.ZodTypeDef> = z.object({
matchers: z.array(z.lazy(() => matcherSchema)), matchers: z.array(z.lazy(() => matcherSchema)),
}); });
export const matcherSchema = z.union([ export const matcherSchema = z.union([
dateMatcherSchema,
orMatcherSchema, orMatcherSchema,
startDateMatcherSchema,
templateMatcherSchema, templateMatcherSchema,
titleMatcherSchema, titleMatcherSchema,
]); ]);
@@ -2,8 +2,12 @@ import { renderTemplate } from 'ha-nunjucks';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { MediaMatcher } from '../../../../src/card-controller/folders/ha/media-matcher'; import { MediaMatcher } from '../../../../src/card-controller/folders/ha/media-matcher';
import { Matcher } from '../../../../src/config/schema/folders'; import { Matcher } from '../../../../src/config/schema/folders';
import { BrowseMedia } from '../../../../src/ha/browse-media/types'; import {
BrowseMediaMetadata,
RichBrowseMedia,
} from '../../../../src/ha/browse-media/types';
import { createHASS } from '../../../test-utils'; import { createHASS } from '../../../test-utils';
import { sub } from 'date-fns';
vi.mock('ha-nunjucks'); vi.mock('ha-nunjucks');
@@ -11,9 +15,9 @@ describe('MediaMatcher', () => {
describe('match', () => { describe('match', () => {
const createMediaItem = ( const createMediaItem = (
title: string, title: string,
can_expand: boolean, can_expand = false,
media_class = 'image', media_class = 'image',
): BrowseMedia => ({ ): RichBrowseMedia<BrowseMediaMetadata> => ({
title, title,
media_class, media_class,
media_content_type: media_class === 'directory' ? 'directory' : 'image/jpeg', media_content_type: media_class === 'directory' ? 'directory' : 'image/jpeg',
@@ -25,7 +29,7 @@ describe('MediaMatcher', () => {
it('should return false if foldersOnly is true and media.can_expand is false', () => { it('should return false if foldersOnly is true and media.can_expand is false', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test File', false); const media = createMediaItem('Test File');
expect( expect(
mediaMatcher.match(createHASS(), media, { matchers: [], foldersOnly: true }), mediaMatcher.match(createHASS(), media, { matchers: [], foldersOnly: true }),
).toBe(false); ).toBe(false);
@@ -41,13 +45,13 @@ describe('MediaMatcher', () => {
it('should return true if matchers array is empty', () => { it('should return true if matchers array is empty', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media', false); const media = createMediaItem('Test Media');
expect(mediaMatcher.match(createHASS(), media, { matchers: [] })).toBe(true); expect(mediaMatcher.match(createHASS(), media, { matchers: [] })).toBe(true);
}); });
it('should return true if matchers array is undefined', () => { it('should return true if matchers array is undefined', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media', false); const media = createMediaItem('Test Media');
expect(mediaMatcher.match(createHASS(), media, { matchers: undefined })).toBe( expect(mediaMatcher.match(createHASS(), media, { matchers: undefined })).toBe(
true, true,
); );
@@ -56,21 +60,21 @@ describe('MediaMatcher', () => {
describe('with title matcher', () => { describe('with title matcher', () => {
it('should return true when title matches exactly', () => { it('should return true when title matches exactly', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Exact Title', false); const media = createMediaItem('Exact Title');
const matchers: Matcher[] = [{ type: 'title', title: 'Exact Title' }]; const matchers: Matcher[] = [{ type: 'title', title: 'Exact Title' }];
expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(true); expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(true);
}); });
it('should return false when title does not match exactly', () => { it('should return false when title does not match exactly', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('DOES NOT MATCH', false); const media = createMediaItem('DOES NOT MATCH');
const matchers: Matcher[] = [{ type: 'title', title: 'Exact Title' }]; const matchers: Matcher[] = [{ type: 'title', title: 'Exact Title' }];
expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(false); expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(false);
}); });
it('should return true when title matches regexp and extracted value matches matcher.title', () => { it('should return true when title matches regexp and extracted value matches matcher.title', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Prefix-ImportantPart-Suffix', false); const media = createMediaItem('Prefix-ImportantPart-Suffix');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ {
type: 'title', type: 'title',
@@ -83,7 +87,7 @@ describe('MediaMatcher', () => {
it('should return false when title matches regexp but extracted value does not match matcher.title', () => { it('should return false when title matches regexp but extracted value does not match matcher.title', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Prefix-ImportantPart-Suffix', false); const media = createMediaItem('Prefix-ImportantPart-Suffix');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ {
type: 'title', type: 'title',
@@ -96,7 +100,7 @@ describe('MediaMatcher', () => {
it('should return true when title matches regexp with an explicit title value', () => { it('should return true when title matches regexp with an explicit title value', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Prefix-ImportantPart-Suffix', false); const media = createMediaItem('Prefix-ImportantPart-Suffix');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ {
type: 'title', type: 'title',
@@ -109,7 +113,7 @@ describe('MediaMatcher', () => {
it('should return false when title does not match regexp', () => { it('should return false when title does not match regexp', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Unrelated Title', false); const media = createMediaItem('Unrelated Title');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ {
type: 'title', type: 'title',
@@ -122,7 +126,7 @@ describe('MediaMatcher', () => {
it('should return false when regexp is provided but does not extract the required group', () => { it('should return false when regexp is provided but does not extract the required group', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Prefix-ImportantPart-Suffix', false); const media = createMediaItem('Prefix-ImportantPart-Suffix');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ {
type: 'title', type: 'title',
@@ -135,7 +139,7 @@ describe('MediaMatcher', () => {
it('should return true when no regexp and no matcher.title (matches any title)', () => { it('should return true when no regexp and no matcher.title (matches any title)', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Any Title Will Do', false); const media = createMediaItem('Any Title Will Do');
const matchers: Matcher[] = [{ type: 'title' }]; const matchers: Matcher[] = [{ type: 'title' }];
expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(true); expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(true);
}); });
@@ -145,7 +149,7 @@ describe('MediaMatcher', () => {
it('should return true when template value matches', () => { it('should return true when template value matches', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const title = 'Any Title Will Do'; const title = 'Any Title Will Do';
const media = createMediaItem(title, false); const media = createMediaItem(title);
vi.mocked(renderTemplate).mockReturnValue(true); vi.mocked(renderTemplate).mockReturnValue(true);
@@ -181,7 +185,7 @@ describe('MediaMatcher', () => {
it('should return false when template value does not match', () => { it('should return false when template value does not match', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const title = 'Any Title Will Do'; const title = 'Any Title Will Do';
const media = createMediaItem(title, false); const media = createMediaItem(title);
vi.mocked(renderTemplate).mockReturnValue(false); vi.mocked(renderTemplate).mockReturnValue(false);
@@ -218,7 +222,7 @@ describe('MediaMatcher', () => {
describe('with or matcher', () => { describe('with or matcher', () => {
it('should return true if at least one sub-matcher matches', () => { it('should return true if at least one sub-matcher matches', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media', false); const media = createMediaItem('Test Media');
const matcher: Matcher = { const matcher: Matcher = {
type: 'or', type: 'or',
matchers: [ matchers: [
@@ -233,7 +237,7 @@ describe('MediaMatcher', () => {
it('should return false if no sub-matcher matches', () => { it('should return false if no sub-matcher matches', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media', false); const media = createMediaItem('Test Media');
const matcher: Matcher = { const matcher: Matcher = {
type: 'or', type: 'or',
matchers: [ matchers: [
@@ -247,9 +251,74 @@ describe('MediaMatcher', () => {
}); });
}); });
describe('with date matcher', () => {
it('should not match without metadata', () => {
const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media');
const matcher: Matcher = {
type: 'date',
since: { days: 1 },
};
expect(mediaMatcher.match(createHASS(), media, { matchers: [matcher] })).toBe(
false,
);
});
it.each([
[
{
type: 'date' as const,
since: { days: 2, minutes: 1 },
},
],
[
{
type: 'date' as const,
since: { days: 2, hours: 1 },
},
],
[
{
type: 'date' as const,
since: { months: 1 },
},
],
[
{
type: 'date' as const,
since: { years: 1 },
},
],
])('should match with date more recent than matcher %s', (matcher: Matcher) => {
const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media');
media._metadata = {
startDate: sub(new Date(), { days: 1 }),
};
expect(mediaMatcher.match(createHASS(), media, { matchers: [matcher] })).toBe(
true,
);
});
it('should not match with date less recent than matcher', () => {
const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media');
media._metadata = {
startDate: sub(new Date(), { days: 2 }),
};
const matcher: Matcher = {
type: 'date',
since: { days: 1 },
};
expect(mediaMatcher.match(createHASS(), media, { matchers: [matcher] })).toBe(
false,
);
});
});
it('should return false if one of multiple matchers fails', () => { it('should return false if one of multiple matchers fails', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media One', false); const media = createMediaItem('Test Media One');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ type: 'title', title: 'Test Media One' }, // Pass { type: 'title', title: 'Test Media One' }, // Pass
{ type: 'title', title: 'Test Media Two' }, // Fail { type: 'title', title: 'Test Media Two' }, // Fail
@@ -259,7 +328,7 @@ describe('MediaMatcher', () => {
it('should return true if all multiple matchers pass', () => { it('should return true if all multiple matchers pass', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media One', false); const media = createMediaItem('Test Media One');
const matchers: Matcher[] = [ const matchers: Matcher[] = [
{ type: 'title', title: 'Test Media One' }, { type: 'title', title: 'Test Media One' },
{ {
@@ -273,7 +342,7 @@ describe('MediaMatcher', () => {
it('should ignore matchers of unknown types', () => { it('should ignore matchers of unknown types', () => {
const mediaMatcher = new MediaMatcher(); const mediaMatcher = new MediaMatcher();
const media = createMediaItem('Test Media', false); const media = createMediaItem('Test Media');
const matchers: Matcher[] = [{ type: 'unknownMatcherType' as 'title' }]; const matchers: Matcher[] = [{ type: 'unknownMatcherType' as 'title' }];
expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(true); expect(mediaMatcher.match(createHASS(), media, { matchers })).toBe(true);