From 419175d79b2c44c10ff996434b3785e9e05ce3ec Mon Sep 17 00:00:00 2001 From: CJPeckover Date: Mon, 14 Jul 2025 13:44:05 -0400 Subject: [PATCH] New getAllAlbumsSlim endpoint - Add new endpoint - Add slim option to albumService.getAll (default false) - Use getAllAlbumsSlim in search-albums-section --- open-api/immich-openapi-specs.json | 54 +++++++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 14 +++++ .../src/controllers/album.controller.spec.ts | 19 +++++++ server/src/controllers/album.controller.ts | 7 +++ server/src/services/album.service.ts | 26 +++++---- .../search-bar/search-albums-section.svelte | 4 +- 6 files changed, 113 insertions(+), 11 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 1624d0ed7e..35d59ed5fe 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -870,6 +870,60 @@ ] } }, + "/albums/slim": { + "get": { + "operationId": "getAllAlbumsSlim", + "parameters": [ + { + "name": "assetId", + "required": false, + "in": "query", + "description": "Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "shared", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AlbumResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Albums" + ] + } + }, "/albums/statistics": { "get": { "operationId": "getAlbumStatistics", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 55991b8599..a2205de0c8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1790,6 +1790,20 @@ export function createAlbum({ createAlbumDto }: { body: createAlbumDto }))); } +export function getAllAlbumsSlim({ assetId, shared }: { + assetId?: string; + shared?: boolean; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AlbumResponseDto[]; + }>(`/albums/slim${QS.query(QS.explode({ + assetId, + shared + }))}`, { + ...opts + })); +} export function getAlbumStatistics(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts index 9b8a19c129..519d91b3c8 100644 --- a/server/src/controllers/album.controller.spec.ts +++ b/server/src/controllers/album.controller.spec.ts @@ -37,6 +37,25 @@ describe(AlbumController.name, () => { }); }); + describe('GET /albums/slim', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/albums/slim'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject an invalid shared param', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/albums/slim?shared=invalid'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value'])); + }); + + it('should reject an invalid assetId param', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/albums/slim?assetId=invalid'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID'])); + }); + }); + describe('GET /albums/:id', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`); diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 49ec5a82ea..691c1305dc 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -28,6 +28,13 @@ export class AlbumController { return this.service.getAll(auth, query); } + @Get('slim') + @Authenticated({ permission: Permission.ALBUM_READ }) + getAllAlbumsSlim(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { + return this.service.getAll(auth, query, true); + //asdf + } + @Post() @Authenticated({ permission: Permission.ALBUM_CREATE }) createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index e49d4bc5fe..be6e67e7a5 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -37,7 +37,11 @@ export class AlbumService extends BaseService { }; } - async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise { + async getAll( + { user: { id: ownerId } }: AuthDto, + { assetId, shared }: GetAlbumsDto, + slim: boolean = false, + ): Promise { await this.albumRepository.updateThumbnails(); let albums: MapAlbumDto[]; @@ -53,20 +57,24 @@ export class AlbumService extends BaseService { // Get asset count for each album. Then map the result to an object: // { [albumId]: assetCount } - const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); const albumMetadata: Record = {}; - for (const metadata of results) { - albumMetadata[metadata.albumId] = metadata; + if (!slim) { + const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); + for (const metadata of results) { + albumMetadata[metadata.albumId] = metadata; + } } return albums.map((album) => ({ ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - startDate: albumMetadata[album.id]?.startDate ?? undefined, - endDate: albumMetadata[album.id]?.endDate ?? undefined, - assetCount: albumMetadata[album.id]?.assetCount ?? 0, - // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need - lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined, + ...(!slim && { + startDate: albumMetadata[album.id]?.startDate ?? undefined, + endDate: albumMetadata[album.id]?.endDate ?? undefined, + assetCount: albumMetadata[album.id]?.assetCount ?? 0, + // lastModifiedAssetTimestamp is only used in mobile app, please remove if not need + lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined, + }), })); } diff --git a/web/src/lib/components/shared-components/search-bar/search-albums-section.svelte b/web/src/lib/components/shared-components/search-bar/search-albums-section.svelte index 7a72bb16aa..6ffaef91e3 100644 --- a/web/src/lib/components/shared-components/search-bar/search-albums-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-albums-section.svelte @@ -2,7 +2,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; import { getAssetThumbnailUrl } from '$lib/utils'; - import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; + import { getAllAlbumsSlim, type AlbumResponseDto } from '@immich/sdk'; import { mdiClose } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -23,7 +23,7 @@ ); onMount(async () => { - allAlbums = await getAllAlbums({}); + allAlbums = await getAllAlbumsSlim({}); }); const handleSelect = (option?: ComboBoxOption) => {