From 134c0d4dfbc949932d2b33b80c587201d4397382 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 28 May 2026 17:01:47 -0400 Subject: [PATCH] feat: search by album name and id (#28672) --- mobile/openapi/lib/api/albums_api.dart | 24 ++++++++++++++++++--- open-api/immich-openapi-specs.json | 20 +++++++++++++++++ packages/sdk/src/fetch-client.ts | 8 +++++-- server/src/dtos/album.dto.ts | 2 ++ server/src/repositories/album.repository.ts | 7 +++++- server/src/services/album.service.ts | 7 ++---- 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index e0fc383c1d..c22a8ced07 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -508,12 +508,18 @@ class AlbumsApi { /// * [String] assetId: /// Filter albums containing this asset ID (ignores other parameters) /// + /// * [String] id: + /// Album ID + /// /// * [bool] isOwned: /// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter /// /// * [bool] isShared: /// Filter by shared status: true = only shared, false = not shared, undefined = no filter - Future getAllAlbumsWithHttpInfo({ String? assetId, bool? isOwned, bool? isShared, }) async { + /// + /// * [String] name: + /// Album name (exact match) + Future getAllAlbumsWithHttpInfo({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -527,12 +533,18 @@ class AlbumsApi { if (assetId != null) { queryParams.addAll(_queryParams('', 'assetId', assetId)); } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } if (isOwned != null) { queryParams.addAll(_queryParams('', 'isOwned', isOwned)); } if (isShared != null) { queryParams.addAll(_queryParams('', 'isShared', isShared)); } + if (name != null) { + queryParams.addAll(_queryParams('', 'name', name)); + } const contentTypes = []; @@ -557,13 +569,19 @@ class AlbumsApi { /// * [String] assetId: /// Filter albums containing this asset ID (ignores other parameters) /// + /// * [String] id: + /// Album ID + /// /// * [bool] isOwned: /// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter /// /// * [bool] isShared: /// Filter by shared status: true = only shared, false = not shared, undefined = no filter - Future?> getAllAlbums({ String? assetId, bool? isOwned, bool? isShared, }) async { - final response = await getAllAlbumsWithHttpInfo( assetId: assetId, isOwned: isOwned, isShared: isShared, ); + /// + /// * [String] name: + /// Album name (exact match) + Future?> getAllAlbums({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async { + final response = await getAllAlbumsWithHttpInfo( assetId: assetId, id: id, isOwned: isOwned, isShared: isShared, name: name, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 9fda205b9a..5857e6b1ce 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1627,6 +1627,17 @@ "type": "string" } }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Album ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, { "name": "isOwned", "required": false, @@ -1644,6 +1655,15 @@ "schema": { "type": "boolean" } + }, + { + "name": "name", + "required": false, + "in": "query", + "description": "Album name (exact match)", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 3f328088ee..66d7a996a3 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -3597,18 +3597,22 @@ export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility } /** * List all albums */ -export function getAllAlbums({ assetId, isOwned, isShared }: { +export function getAllAlbums({ assetId, id, isOwned, isShared, name }: { assetId?: string; + id?: string; isOwned?: boolean; isShared?: boolean; + name?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumResponseDto[]; }>(`/albums${QS.query(QS.explode({ assetId, + id, isOwned, - isShared + isShared, + name }))}`, { ...opts })); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 095e399b96..100550659d 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -65,6 +65,8 @@ const UpdateAlbumSchema = z const GetAlbumsSchema = z .object({ + id: z.uuidv4().optional().describe('Album ID'), + name: z.string().optional().describe('Album name (exact match)'), isOwned: stringToBool .optional() .describe('Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter'), diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index a712151355..724788fa74 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -209,11 +209,16 @@ export class AlbumRepository { } @GenerateSql({ params: [DummyValue.UUID, { isOwned: true, isShared: true }] }) - getAll(ownerId: string, options: { isOwned?: boolean; isShared?: boolean } = {}): Promise { + getAll( + ownerId: string, + options: { id?: string; isOwned?: boolean; isShared?: boolean; name?: string } = {}, + ): Promise { return this.buildAlbumBaseQuery(ownerId, options) .selectAll('album') .select(withAlbumUsers(ownerId)) .select(withSharedLink) + .$if(!!options.id, (qb) => qb.where('album.id', '=', options.id!)) + .$if(!!options.name, (qb) => qb.where('album.albumName', '=', options.name!)) .orderBy('album.createdAt', 'desc') .execute(); } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 723288e5b5..31c4ff2e38 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -37,15 +37,12 @@ export class AlbumService extends BaseService { }; } - async getAll( - { user: { id: ownerId } }: AuthDto, - { assetId, isOwned, isShared }: GetAlbumsDto, - ): Promise { + async getAll({ user: { id: ownerId } }: AuthDto, { assetId, ...rest }: GetAlbumsDto): Promise { await this.albumRepository.updateThumbnails(); const albums = assetId ? await this.albumRepository.getByAssetId(ownerId, assetId) - : await this.albumRepository.getAll(ownerId, { isOwned, isShared }); + : await this.albumRepository.getAll(ownerId, rest); if (albums.length === 0) { return [];