fix: Remove the unnecessary item limit from folder media (#2688)

- Fixed: #2573
This commit is contained in:
Dermot Duffy
2026-08-17 21:49:11 -07:00
committed by GitHub
parent 858c5d0610
commit 84b90bcc7e
12 changed files with 41 additions and 146 deletions
+3 -3
View File
@@ -313,7 +313,7 @@ folders:
id: my-folder
ha:
url: >-
https://ha.ondu.org/media-browser/browser/app%2Cmedia-source%3A%2F%2Fmedia_source
/media-browser/browser/app%2Cmedia-source%3A%2F%2Fmedia_source
path:
# Match everything, parse nothing.
- {}
@@ -331,7 +331,7 @@ folders:
id: my-folder
ha:
url: >-
https://ha.ondu.org/media-browser/browser/app%2Cmedia-source%3A%2F%2Fmedia_source
/media-browser/browser/app%2Cmedia-source%3A%2F%2Fmedia_source
path:
- {}
# At the final level, match everything, parse the date and time.
@@ -350,7 +350,7 @@ folders:
id: my-folder
ha:
url: >-
https://ha.ondu.org/media-browser/browser/app%2Cmedia-source%3A%2F%2Fmedia_source
/media-browser/browser/app%2Cmedia-source%3A%2F%2Fmedia_source
path:
- {}
# At the final level, match everything, parse the date and time.
+7 -7
View File
@@ -22,13 +22,13 @@ performance:
# [...]
```
| Option | Default | Description |
| ---------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `animated_progress_indicator` | `true` | Will show the animated progress indicator 'spinners' when `true`. |
| `card_loading_effects` | `true` | Will occasionally show (silly) visual effects during card loading (e.g. fireworks on New Year's Day). |
| `card_loading_indicator` | `true` | Will show the card loading indicator (spinner & version number) and effects (see `card_loading_effects` below) when `true`. |
| `media_chunk_size` | `50` | How many media items to fetch and render at a time (e.g. thumbnails under a live view, or number of snapshots to load in the media viewer). This may only make partial sense in some contexts (e.g. the 'infinite gallery' is still infinite, it just loads thumbnails this many items at a time) or not at all (e.g. the timeline will show the number of events dictated by the time span the user navigates to). |
| `max_simultaneous_engine_requests` | _Infinity_ | How many camera engine requests to allow occur in parallel. Setting lower values will slow the card down since more requests will run in sequence, but it will increase the chances of positive cache hit rates and reduce the chances of overwhelming the backend. |
| Option | Default | Description |
| ---------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `animated_progress_indicator` | `true` | Will show the animated progress indicator 'spinners' when `true`. |
| `card_loading_effects` | `true` | Will occasionally show (silly) visual effects during card loading (e.g. fireworks on New Year's Day). |
| `card_loading_indicator` | `true` | Will show the card loading indicator (spinner & version number) and effects (see `card_loading_effects` below) when `true`. |
| `media_chunk_size` | `50` | How many media items to fetch and render at a time (e.g. thumbnails under a live view, or number of snapshots to load in the media viewer). This may only make partial sense in some contexts (e.g. the 'infinite gallery' is still infinite, it just loads thumbnails this many items at a time) or not at all (e.g. the timeline will show the number of events dictated by the time span the user navigates to, and [`folders`](./folders.md) always show every matching item). |
| `max_simultaneous_engine_requests` | _Infinity_ | How many camera engine requests to allow occur in parallel. Setting lower values will slow the card down since more requests will run in sequence, but it will increase the chances of positive cache hit rates and reduce the chances of overwhelming the backend. |
### `style`
+1 -8
View File
@@ -166,7 +166,6 @@ export class HAFoldersEngine implements FoldersEngine {
targets: BrowseMediaTarget<BrowseMediaMetadata>[],
): BrowseMediaStep<BrowseMediaMetadata>[] => {
const nextComponent = pathComponents.shift();
const limit = query.limit ?? null;
return [
{
@@ -174,11 +173,6 @@ export class HAFoldersEngine implements FoldersEngine {
metadataGenerator: (media, parent) =>
this._metadataGenerator.generate(media, parent, nextComponent?.ha?.parsers),
// At the final step (no nextComponent), apply limit via earlyExit.
...(limit && {
earlyExit: (media) => media.length >= limit,
}),
...(nextComponent && {
matcher: (media) =>
this._mediaMatcher.match(hass, media, {
@@ -202,11 +196,10 @@ export class HAFoldersEngine implements FoldersEngine {
},
);
const results = getViewItemsFromBrowseMediaArray(browseMedia, {
return getViewItemsFromBrowseMediaArray(browseMedia, {
folder: query.folder,
path: query.path,
});
return query.limit ? results.slice(0, query.limit) : results;
}
public generateChildFolderQuery(
-1
View File
@@ -39,7 +39,6 @@ export interface FolderQuery extends BaseQuery, QueryFilters {
// A trail of paths to navigate back to the "root", with the last path being
// the path that this query directly refers to.
path: NonEmptyTuple<FolderPathComponent>;
limit?: number;
}
// ===============
@@ -205,9 +205,7 @@ export class ViewQueryExecutor {
case 'folders':
viewModifiers.push(
...(await executeQuery(
builder.buildDefaultFolderQuery(queryExecutorOptions?.folder, {
limit: this._getLimit(),
}),
builder.buildDefaultFolderQuery(queryExecutorOptions?.folder),
)),
);
break;
-8
View File
@@ -10,8 +10,6 @@ import type { UnifiedQueryBuilder } from '../view/unified-query-builder';
export interface FolderNavigationParamaters {
viewManagerEpoch: ViewManagerEpoch;
builder: UnifiedQueryBuilder;
limit?: number;
}
export interface MediaNavigationParamaters {
@@ -51,9 +49,6 @@ export const navigateUp = (options?: FolderNavigationParamaters | null): void =>
const query = options?.builder.buildFolderQueryWithPath(
folderQuery.folder,
nonEmptyPath,
{
limit: options?.limit,
},
);
void options?.viewManagerEpoch.manager.setViewByParametersWithExistingQuery({
@@ -73,9 +68,6 @@ export const navigateToFolder = (
const query = options?.builder.buildFolderQueryWithPath(
item.getFolder(),
nonEmptyPath,
{
limit: options?.limit,
},
);
void options?.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
-9
View File
@@ -26,7 +26,6 @@ import {
import type { ConditionStateManagerReadonlyInterface } from '../../condition-trigger/conditions/types.js';
import type { MediaGalleryConfig } from '../../config/schema/media-gallery.js';
import type { CardWideConfig } from '../../config/schema/types.js';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../../const.js';
import type { HomeAssistant } from '../../ha/types.js';
import galleryStyle from '../../scss/gallery.scss?inline';
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
@@ -101,19 +100,11 @@ export class AdvancedCameraCardGallery extends LitElement {
}
}
private _getLimit(): number {
return (
this.cardWideConfig?.performance?.features?.media_chunk_size ??
MEDIA_CHUNK_SIZE_DEFAULT
);
}
private _getFolderNavigationParameters(): FolderNavigationParamaters | null {
return this._builder && this.viewManagerEpoch
? {
builder: this._builder,
viewManagerEpoch: this.viewManagerEpoch,
limit: this._getLimit(),
}
: null;
}
-9
View File
@@ -23,7 +23,6 @@ import {
} from '../components-lib/navigation.js';
import type { ThumbnailsControlConfig } from '../config/schema/common/controls/thumbnails.js';
import type { CardWideConfig } from '../config/schema/types.js';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
import type { HomeAssistant } from '../ha/types.js';
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss?inline';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
@@ -73,19 +72,11 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
private _thumbnails: TemplateResult[] = [];
private _builder: UnifiedQueryBuilder | null = null;
private _getLimit(): number {
return (
this.cardWideConfig?.performance?.features?.media_chunk_size ??
MEDIA_CHUNK_SIZE_DEFAULT
);
}
private _getFolderNavOptions(): FolderNavigationParamaters | undefined {
return this._builder && this.viewManagerEpoch
? {
builder: this._builder,
viewManagerEpoch: this.viewManagerEpoch,
limit: this._getLimit(),
}
: undefined;
}
+2 -4
View File
@@ -267,14 +267,12 @@ export class UnifiedQueryBuilder {
public buildFolderQueryWithPath(
folder: FolderConfig,
path: NonEmptyTuple<FolderPathComponent>,
options?: QueryFiltersOptions,
): UnifiedQuery {
const query = new UnifiedQuery();
const folderQuery: FolderQuery = {
source: QuerySource.Folder,
folder,
path,
...(options?.limit !== undefined && { limit: options.limit }),
};
query.addNode(folderQuery);
return query;
@@ -282,7 +280,7 @@ export class UnifiedQueryBuilder {
public buildDefaultFolderQuery(
folderID?: string,
options?: QueryFiltersOptions,
options?: QueryFilters,
): UnifiedQuery | null {
const query = new UnifiedQuery();
this._addNode(query, this._buildFolderQueryNode(folderID, options));
@@ -317,7 +315,7 @@ export class UnifiedQueryBuilder {
return params
? {
...params,
...options,
...this._extractFilterOptions(options),
}
: null;
}
@@ -238,12 +238,11 @@ describe('HAFoldersEngine', () => {
expect(await engine.expandFolder(createHASS(), query)).toBeNull();
});
it('should early exit when limit is reached', async () => {
it('should return every matching media item without a cap', async () => {
const query: FolderQuery = {
source: QuerySource.Folder,
folder: { type: 'ha', id: 'test' },
path: [{ ha: { id: 'media-source://id' } }],
limit: 1,
};
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(
@@ -266,8 +265,7 @@ describe('HAFoldersEngine', () => {
const engine = new HAFoldersEngine(templateManager);
const results = await engine.expandFolder(createHASS(), query);
// Even though there are 2 children, the limit of 1 should trigger earlyExit.
expect(results?.length).toBe(1);
expect(results?.length).toBe(2);
});
// See additional matcher testing in media-matcher.test.ts .
-54
View File
@@ -157,35 +157,6 @@ describe('navigateUp', () => {
path: [{ ha: { id: 'one' } }, { ha: { id: 'two' } }],
});
});
it('should go up in the folder hierarchy with limit', () => {
const api = createCardAPI();
const folder = createFolder();
const view = createView({
query: createFolderQuery(folder, [{ ha: { id: 'one' } }, { ha: { id: 'two' } }]),
});
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
const epoch: ViewManagerEpoch = {
manager: api.getViewManager(),
};
const builder = new UnifiedQueryBuilder(
mock<CameraManager>(),
mock<FoldersManager>(),
);
const options: FolderNavigationParamaters = {
builder,
viewManagerEpoch: epoch,
limit: 50,
};
navigateUp(options);
const query = vi.mocked(api.getViewManager().setViewByParametersWithExistingQuery)
.mock.calls[0][0]?.params?.query as UnifiedQuery;
expect(query.getNodes()[0].limit).toBe(50);
});
});
describe('navigateToFolder', () => {
@@ -241,31 +212,6 @@ describe('navigateToFolder', () => {
expect(nodes?.[0]).toHaveProperty('path');
expect((nodes?.[0] as { path: readonly unknown[] }).path).toHaveLength(2);
});
it('should navigate into folder with limit', () => {
const api = createCardAPI();
const folder = createFolder();
const epoch: ViewManagerEpoch = {
manager: api.getViewManager(),
};
const builder = new UnifiedQueryBuilder(
mock<CameraManager>(),
mock<FoldersManager>(),
);
const options: FolderNavigationParamaters = {
builder,
viewManagerEpoch: epoch,
limit: 100,
};
const item = new ViewFolder(folder, [{ ha: { id: 'root' } }]);
navigateToFolder(item, options);
const query = vi.mocked(api.getViewManager().setViewByParametersWithExistingQuery)
.mock.calls[0][0]?.params?.query;
expect(query?.getNodes()[0].limit).toBe(100);
});
});
describe('getUpFolderItem', () => {
+25 -36
View File
@@ -499,22 +499,7 @@ describe('UnifiedQueryBuilder', () => {
});
});
it('should apply limit option', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const folder = createFolder({ id: 'folder1', title: 'Test' });
const path: [FolderPathComponent] = [{ ha: { id: 'Root' } }];
const query = builder.buildFolderQueryWithPath(folder, path, {
limit: 10,
});
expect(query.getNodes()[0]).toMatchObject({
source: QuerySource.Folder,
limit: 10,
});
});
it('should not include limit when not provided', () => {
it('should not limit the number of items', () => {
const { cameraManager, foldersManager } = createMocks();
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const folder = createFolder({ id: 'folder1', title: 'Test' });
@@ -568,26 +553,6 @@ describe('UnifiedQueryBuilder', () => {
expect(query).toBeNull();
});
it('should apply limit option', () => {
const { cameraManager, foldersManager } = createMocks();
const folder = createFolder({ id: 'folder1', title: 'Test' });
const path: [FolderPathComponent] = [{ ha: { id: 'Root' } }];
foldersManager.getFolder.mockReturnValue(folder);
foldersManager.getDefaultQueryParameters.mockReturnValue(
createFolderQueryParams(folder, path),
);
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultFolderQuery('folder1', { limit: 20 });
assert(query);
expect(query.getNodes()[0]).toMatchObject({
source: QuerySource.Folder,
limit: 20,
});
});
it('should use default folder when no ID provided', () => {
const { cameraManager, foldersManager } = createMocks();
foldersManager.getFolder.mockReturnValue(null);
@@ -1010,6 +975,30 @@ describe('UnifiedQueryBuilder', () => {
});
});
it('should not limit a folder query built for folder media type', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));
store.getCameraConfig.mockReturnValue(
createCameraConfig({ media: { type: 'folder', folders: ['folder1'] } }),
);
cameraManager.getCameraCapabilities.mockReturnValue(createCapabilities());
const folder = createFolder({ id: 'folder1', title: 'Test Folder' });
const path: [FolderPathComponent] = [{ ha: { id: 'Root' } }];
foldersManager.getFolder.mockReturnValue(folder);
foldersManager.getDefaultQueryParameters.mockReturnValue(
createFolderQueryParams(folder, path),
);
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
const query = builder.buildDefaultCameraQuery(undefined, { limit: 50 });
assert(query);
expect(query.getNodes()[0]).not.toHaveProperty('limit');
});
it('should build default folder query for folder media type without specific folders', () => {
const { cameraManager, foldersManager, store } = createMocks();
store.getCameraIDs.mockReturnValue(new Set(['camera.office']));