diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 405bcd1d5a7b2..824dd38835bcb 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -12139,6 +12139,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchPerson: async (name: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'name' is not null or undefined + assertParamExists('searchPerson', 'name', name) + const localVarPath = `/search/person`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (name !== undefined) { + localVarQueryParameter['name'] = name; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -12192,6 +12237,16 @@ export const SearchApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchPerson(name: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchPerson(name, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -12219,6 +12274,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SearchApiSearchPersonRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.searchPerson(requestParameters.name, options).then((request) => request(axios, basePath)); + }, }; }; @@ -12341,6 +12405,20 @@ export interface SearchApiSearchRequest { readonly motion?: boolean } +/** + * Request parameters for searchPerson operation in SearchApi. + * @export + * @interface SearchApiSearchPersonRequest + */ +export interface SearchApiSearchPersonRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchPerson + */ + readonly name: string +} + /** * SearchApi - object-oriented interface * @export @@ -12368,6 +12446,17 @@ export class SearchApi extends BaseAPI { public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) { return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {SearchApiSearchPersonRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig) { + return SearchApiFp(this.configuration).searchPerson(requestParameters.name, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 8cf1cca47e9e7..2865b1308a873 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -154,6 +154,7 @@ Class | Method | HTTP request | Description *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | +*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index 5cc36956a0ff7..6bb2c0e938941 100644 --- a/mobile/openapi/doc/SearchApi.md +++ b/mobile/openapi/doc/SearchApi.md @@ -11,6 +11,7 @@ Method | HTTP request | Description ------------- | ------------- | ------------- [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | [**search**](SearchApi.md#search) | **GET** /search | +[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person | # **getExploreData** @@ -149,3 +150,58 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **searchPerson** +> List searchPerson(name) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final name = name_example; // String | + +try { + final result = api_instance.searchPerson(name); + print(result); +} catch (e) { + print('Exception when calling SearchApi->searchPerson: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **name** | **String**| | + +### Return type + +[**List**](PersonResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 9393b5c61dcf6..870c5dcd3afed 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -215,4 +215,56 @@ class SearchApi { } return null; } + + /// Performs an HTTP 'GET /search/person' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] name (required): + Future searchPersonWithHttpInfo(String name,) async { + // ignore: prefer_const_declarations + final path = r'/search/person'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'name', name)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] name (required): + Future?> searchPerson(String name,) async { + final response = await searchPersonWithHttpInfo(name,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } } diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 8365eff0762ed..49055132829e4 100644 --- a/mobile/openapi/test/search_api_test.dart +++ b/mobile/openapi/test/search_api_test.dart @@ -27,5 +27,10 @@ void main() { // TODO }); + //Future> searchPerson(String name) async + test('test searchPerson', () async { + // TODO + }); + }); } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index cb3d107fcb7f4..5a5ecd9adfcbd 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3789,6 +3789,50 @@ ] } }, + "/search/person": { + "get": { + "operationId": "searchPerson", + "parameters": [ + { + "name": "name", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PersonResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/server-info": { "get": { "operationId": "getServerInfo", diff --git a/server/src/domain/repositories/person.repository.ts b/server/src/domain/repositories/person.repository.ts index 667d780e0a2f4..d39b4468b14fb 100644 --- a/server/src/domain/repositories/person.repository.ts +++ b/server/src/domain/repositories/person.repository.ts @@ -22,6 +22,7 @@ export interface IPersonRepository { getAllForUser(userId: string, options: PersonSearchOptions): Promise; getAllWithoutFaces(): Promise; getById(personId: string): Promise; + getByName(userId: string, personName: string): Promise; getAssets(personId: string): Promise; prepareReassignFaces(data: UpdateFacesData): Promise; diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 44d54f7a71a47..0d6def96ccdde 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -85,3 +85,9 @@ export class SearchDto { @Transform(toBoolean) motion?: boolean; } + +export class SearchPeopleDto { + @IsString() + @IsNotEmpty() + name!: string; +} diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 049e0fe00ae91..5100e1b4acf12 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -5,6 +5,7 @@ import { AssetResponseDto, mapAsset } from '../asset'; import { AuthUserDto } from '../auth'; import { usePagination } from '../domain.util'; import { IAssetFaceJob, IBulkEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; +import { PersonResponseDto } from '../person/person.dto'; import { AssetFaceId, IAlbumRepository, @@ -21,7 +22,7 @@ import { SearchStrategy, } from '../repositories'; import { FeatureFlag, SystemConfigCore } from '../system-config'; -import { SearchDto } from './dto'; +import { SearchDto, SearchPeopleDto } from './dto'; import { SearchResponseDto } from './response-dto'; interface SyncQueue { @@ -158,6 +159,10 @@ export class SearchService { }; } + async searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise { + return await this.personRepository.getByName(authUser.id, dto.name); + } + async handleIndexAlbums() { if (!this.enabled) { return false; diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index 9351669251411..ffa454cd500ba 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -1,4 +1,12 @@ -import { AuthUserDto, SearchDto, SearchExploreResponseDto, SearchResponseDto, SearchService } from '@app/domain'; +import { + AuthUserDto, + PersonResponseDto, + SearchDto, + SearchExploreResponseDto, + SearchPeopleDto, + SearchResponseDto, + SearchService, +} from '@app/domain'; import { Controller, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthUser, Authenticated } from '../app.guard'; @@ -11,6 +19,11 @@ import { UseValidation } from '../app.utils'; export class SearchController { constructor(private service: SearchService) {} + @Get('person') + searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise { + return this.service.searchPerson(authUser, dto); + } + @Get() search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise { return this.service.search(authUser, dto); diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index c444ee694f8c4..bfd101c6bf17b 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -95,6 +95,16 @@ export class PersonRepository implements IPersonRepository { return this.personRepository.findOne({ where: { id: personId } }); } + getByName(userId: string, personName: string): Promise { + return this.personRepository + .createQueryBuilder('person') + .leftJoin('person.faces', 'face') + .where('person.ownerId = :userId', { userId }) + .andWhere('LOWER(person.name) LIKE :name', { name: `${personName.toLowerCase()}%` }) + .limit(20) + .getMany(); + } + getAssets(personId: string): Promise { return this.assetRepository.find({ where: { diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 52a8e5d9a54c9..d942bafd63adc 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -9,6 +9,8 @@ export const newPersonRepositoryMock = (): jest.Mocked => { getAssets: jest.fn(), getAllWithoutFaces: jest.fn(), + getByName: jest.fn(), + create: jest.fn(), update: jest.fn(), deleteAll: jest.fn(), diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 405bcd1d5a7b2..824dd38835bcb 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -12139,6 +12139,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchPerson: async (name: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'name' is not null or undefined + assertParamExists('searchPerson', 'name', name) + const localVarPath = `/search/person`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (name !== undefined) { + localVarQueryParameter['name'] = name; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -12192,6 +12237,16 @@ export const SearchApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchPerson(name: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchPerson(name, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -12219,6 +12274,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SearchApiSearchPersonRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.searchPerson(requestParameters.name, options).then((request) => request(axios, basePath)); + }, }; }; @@ -12341,6 +12405,20 @@ export interface SearchApiSearchRequest { readonly motion?: boolean } +/** + * Request parameters for searchPerson operation in SearchApi. + * @export + * @interface SearchApiSearchPersonRequest + */ +export interface SearchApiSearchPersonRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchPerson + */ + readonly name: string +} + /** * SearchApi - object-oriented interface * @export @@ -12368,6 +12446,17 @@ export class SearchApi extends BaseAPI { public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) { return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {SearchApiSearchPersonRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig) { + return SearchApiFp(this.configuration).searchPerson(requestParameters.name, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 3fbfa9c7a2c8a..070f143a33503 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -1,5 +1,5 @@