diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 7796ab27a4..715f61999d 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -11618,11 +11618,11 @@ export const FaceApiAxiosParamCreator = function (configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise => { + reassignFace: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined - assertParamExists('reassignFacesById', 'id', id) + assertParamExists('reassignFace', 'id', id) // verify required parameter 'faceDto' is not null or undefined - assertParamExists('reassignFacesById', 'faceDto', faceDto) + assertParamExists('reassignFace', 'faceDto', faceDto) const localVarPath = `/face/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. @@ -11728,8 +11728,8 @@ export const FaceApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options); + async reassignFace(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFace(id, faceDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -11763,12 +11763,12 @@ export const FaceApiFactory = function (configuration?: Configuration, basePath? }, /** * - * @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters. + * @param {FaceApiReassignFaceRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath)); + reassignFace(requestParameters: FaceApiReassignFaceRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.reassignFace(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath)); }, /** * @@ -11797,22 +11797,22 @@ export interface FaceApiGetFacesRequest { } /** - * Request parameters for reassignFacesById operation in FaceApi. + * Request parameters for reassignFace operation in FaceApi. * @export - * @interface FaceApiReassignFacesByIdRequest + * @interface FaceApiReassignFaceRequest */ -export interface FaceApiReassignFacesByIdRequest { +export interface FaceApiReassignFaceRequest { /** * * @type {string} - * @memberof FaceApiReassignFacesById + * @memberof FaceApiReassignFace */ readonly id: string /** * * @type {FaceDto} - * @memberof FaceApiReassignFacesById + * @memberof FaceApiReassignFace */ readonly faceDto: FaceDto } @@ -11851,13 +11851,13 @@ export class FaceApi extends BaseAPI { /** * - * @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters. + * @param {FaceApiReassignFaceRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof FaceApi */ - public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) { - return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath)); + public reassignFace(requestParameters: FaceApiReassignFaceRequest, options?: AxiosRequestConfig) { + return FaceApiFp(this.configuration).reassignFace(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath)); } /** @@ -14037,6 +14037,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + unassignFaces: async (assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'assetFaceUpdateDto' is not null or undefined + assertParamExists('unassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto) + const localVarPath = `/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: 'DELETE', ...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) + + + localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -14232,6 +14276,16 @@ export const PersonApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async unassignFaces(assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.unassignFaces(assetFaceUpdateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {PeopleUpdateDto} peopleUpdateDto @@ -14334,6 +14388,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PersonApiUnassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. @@ -14467,6 +14530,20 @@ export interface PersonApiReassignFacesRequest { readonly assetFaceUpdateDto: AssetFaceUpdateDto } +/** + * Request parameters for unassignFaces operation in PersonApi. + * @export + * @interface PersonApiUnassignFacesRequest + */ +export interface PersonApiUnassignFacesRequest { + /** + * + * @type {AssetFaceUpdateDto} + * @memberof PersonApiUnassignFaces + */ + readonly assetFaceUpdateDto: AssetFaceUpdateDto +} + /** * Request parameters for updatePeople operation in PersonApi. * @export @@ -14596,6 +14673,17 @@ export class PersonApi extends BaseAPI { return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {PersonApiUnassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PersonApi + */ + public unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig) { + return PersonApiFp(this.configuration).unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 8cbda59bf6..6e42cabed5 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -134,7 +134,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face | -*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} | +*FaceApi* | [**reassignFace**](doc//FaceApi.md#reassignface) | **PUT** /face/{id} | *FaceApi* | [**unassignFace**](doc//FaceApi.md#unassignface) | **DELETE** /face/{id} | *JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs | *JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} | @@ -164,6 +164,7 @@ Class | Method | HTTP request | Description *PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | *PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge | *PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign | +*PersonApi* | [**unassignFaces**](doc//PersonApi.md#unassignfaces) | **DELETE** /person | *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | diff --git a/mobile/openapi/doc/FaceApi.md b/mobile/openapi/doc/FaceApi.md index a58e2a029b..bcb476a287 100644 --- a/mobile/openapi/doc/FaceApi.md +++ b/mobile/openapi/doc/FaceApi.md @@ -10,7 +10,7 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- [**getFaces**](FaceApi.md#getfaces) | **GET** /face | -[**reassignFacesById**](FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} | +[**reassignFace**](FaceApi.md#reassignface) | **PUT** /face/{id} | [**unassignFace**](FaceApi.md#unassignface) | **DELETE** /face/{id} | @@ -69,8 +69,8 @@ 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) -# **reassignFacesById** -> PersonResponseDto reassignFacesById(id, faceDto) +# **reassignFace** +> PersonResponseDto reassignFace(id, faceDto) @@ -97,10 +97,10 @@ final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final faceDto = FaceDto(); // FaceDto | try { - final result = api_instance.reassignFacesById(id, faceDto); + final result = api_instance.reassignFace(id, faceDto); print(result); } catch (e) { - print('Exception when calling FaceApi->reassignFacesById: $e\n'); + print('Exception when calling FaceApi->reassignFace: $e\n'); } ``` diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index 6ad2d19853..cb2c86afa0 100644 --- a/mobile/openapi/doc/PersonApi.md +++ b/mobile/openapi/doc/PersonApi.md @@ -17,6 +17,7 @@ Method | HTTP request | Description [**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | [**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge | [**reassignFaces**](PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign | +[**unassignFaces**](PersonApi.md#unassignfaces) | **DELETE** /person | [**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person | [**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} | @@ -461,6 +462,61 @@ 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) +# **unassignFaces** +> List unassignFaces(assetFaceUpdateDto) + + + +### 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 = PersonApi(); +final assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto | + +try { + final result = api_instance.unassignFaces(assetFaceUpdateDto); + print(result); +} catch (e) { + print('Exception when calling PersonApi->unassignFaces: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.md)| | + +### Return type + +[**List**](BulkIdResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **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) + # **updatePeople** > List updatePeople(peopleUpdateDto) diff --git a/mobile/openapi/lib/api/face_api.dart b/mobile/openapi/lib/api/face_api.dart index f88965815f..d8bf1ed23b 100644 --- a/mobile/openapi/lib/api/face_api.dart +++ b/mobile/openapi/lib/api/face_api.dart @@ -74,7 +74,7 @@ class FaceApi { /// * [String] id (required): /// /// * [FaceDto] faceDto (required): - Future reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async { + Future reassignFaceWithHttpInfo(String id, FaceDto faceDto,) async { // ignore: prefer_const_declarations final path = r'/face/{id}' .replaceAll('{id}', id); @@ -105,8 +105,8 @@ class FaceApi { /// * [String] id (required): /// /// * [FaceDto] faceDto (required): - Future reassignFacesById(String id, FaceDto faceDto,) async { - final response = await reassignFacesByIdWithHttpInfo(id, faceDto,); + Future reassignFace(String id, FaceDto faceDto,) async { + final response = await reassignFaceWithHttpInfo(id, faceDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/person_api.dart b/mobile/openapi/lib/api/person_api.dart index b603df6d3b..337dfdefd7 100644 --- a/mobile/openapi/lib/api/person_api.dart +++ b/mobile/openapi/lib/api/person_api.dart @@ -413,6 +413,56 @@ class PersonApi { return null; } + /// Performs an HTTP 'DELETE /person' operation and returns the [Response]. + /// Parameters: + /// + /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): + Future unassignFacesWithHttpInfo(AssetFaceUpdateDto assetFaceUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/person'; + + // ignore: prefer_final_locals + Object? postBody = assetFaceUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): + Future?> unassignFaces(AssetFaceUpdateDto assetFaceUpdateDto,) async { + final response = await unassignFacesWithHttpInfo(assetFaceUpdateDto,); + 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; + } + /// Performs an HTTP 'PUT /person' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/test/face_api_test.dart b/mobile/openapi/test/face_api_test.dart index 55c289e041..bb0feab28c 100644 --- a/mobile/openapi/test/face_api_test.dart +++ b/mobile/openapi/test/face_api_test.dart @@ -22,8 +22,8 @@ void main() { // TODO }); - //Future reassignFacesById(String id, FaceDto faceDto) async - test('test reassignFacesById', () async { + //Future reassignFace(String id, FaceDto faceDto) async + test('test reassignFace', () async { // TODO }); diff --git a/mobile/openapi/test/person_api_test.dart b/mobile/openapi/test/person_api_test.dart index dd112eeaae..b45ae639dd 100644 --- a/mobile/openapi/test/person_api_test.dart +++ b/mobile/openapi/test/person_api_test.dart @@ -57,6 +57,11 @@ void main() { // TODO }); + //Future> unassignFaces(AssetFaceUpdateDto assetFaceUpdateDto) async + test('test unassignFaces', () async { + // TODO + }); + //Future> updatePeople(PeopleUpdateDto peopleUpdateDto) async test('test updatePeople', () async { // TODO diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 1201483313..465d0434e8 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3307,7 +3307,7 @@ ] }, "put": { - "operationId": "reassignFacesById", + "operationId": "reassignFace", "parameters": [ { "name": "id", @@ -4119,6 +4119,49 @@ } }, "/person": { + "delete": { + "operationId": "unassignFaces", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetFaceUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Person" + ] + }, "get": { "operationId": "getAllPeople", "parameters": [ diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index 2f43fe730e..ddcb8601a8 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -105,6 +105,11 @@ export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { person!: PersonResponseDto | null; } +export class FaceDto { + @ValidateUUID() + id!: string; +} + export class AssetFaceUpdateDto { @IsArray() @ValidateNested({ each: true }) @@ -112,11 +117,6 @@ export class AssetFaceUpdateDto { data!: AssetFaceUpdateItem[]; } -export class FaceDto { - @ValidateUUID() - id!: string; -} - export class AssetFaceUpdateItem { @ValidateUUID() personId!: string; diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 3cb58dad5c..629a035e11 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -406,7 +406,7 @@ describe(PersonService.name, () => { }); }); - describe('reassignFacesById', () => { + describe('reassignFace', () => { it('should create a new person', async () => { accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); @@ -415,7 +415,7 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValue(personStub.noName); personMock.getRandomFace.mockResolvedValue(null); await expect( - sut.reassignFacesById(authStub.admin, personStub.noName.id, { + sut.reassignFace(authStub.admin, personStub.noName.id, { id: faceStub.face1.id, }), ).resolves.toEqual({ @@ -437,7 +437,7 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValue(personStub.noName); personMock.getRandomFace.mockResolvedValue(null); await expect( - sut.reassignFacesById(authStub.admin, personStub.noName.id, { + sut.reassignFace(authStub.admin, personStub.noName.id, { id: faceStub.face1.id, }), ).rejects.toBeInstanceOf(BadRequestException); @@ -471,6 +471,21 @@ describe(PersonService.name, () => { }); }); + describe('unassignFaces', () => { + it('should unassign a face', async () => { + personMock.getFacesByIds.mockResolvedValueOnce([faceStub.face1]); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); + accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); + personMock.reassignFace.mockResolvedValue(1); + personMock.getRandomFace.mockResolvedValue(null); + personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace); + + await expect( + sut.unassignFaces(authStub.admin, { data: [{ assetId: faceStub.face1.id, personId: 'person-1' }] }), + ).resolves.toStrictEqual([{ id: 'assetFaceId', success: true }]); + }); + }); + describe('handlePersonDelete', () => { it('should stop if a person has not be found', async () => { personMock.getById.mockResolvedValue(null); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 88387a10b1..fb3061e891 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -87,6 +87,24 @@ export class PersonService { return this.repository.create({ ownerId: authUser.id }); } + async reassignFace(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise { + await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); + + await this.access.requirePermission(authUser, Permission.PERSON_CREATE, dto.id); + const face = await this.repository.getFaceById(dto.id); + const person = await this.findOrFail(personId); + + await this.repository.reassignFace(face.id, personId); + if (person.faceAssetId === null) { + await this.createNewFeaturePhoto([person.id]); + } + if (face.person && face.person.faceAssetId === face.id) { + await this.createNewFeaturePhoto([face.person.id]); + } + + return await this.findOrFail(personId).then(mapPerson); + } + async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise { await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); const person = await this.findOrFail(personId); @@ -131,22 +149,32 @@ export class PersonService { return mapFace(face, authUser); } - async reassignFacesById(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); + async unassignFaces(authUser: AuthUserDto, dto: AssetFaceUpdateDto): Promise { + const changeFeaturePhoto: string[] = []; + const results: BulkIdResponseDto[] = []; - await this.access.requirePermission(authUser, Permission.PERSON_CREATE, dto.id); - const face = await this.repository.getFaceById(dto.id); - const person = await this.findOrFail(personId); + for (const data of dto.data) { + const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); - await this.repository.reassignFace(face.id, personId); - if (person.faceAssetId === null) { - await this.createNewFeaturePhoto([person.id]); + for (const face of faces) { + await this.access.requirePermission(authUser, Permission.PERSON_CREATE, face.id); + if (face.personId) { + await this.access.requirePermission(authUser, Permission.PERSON_WRITE, face.personId); + } + + await this.repository.reassignFace(face.id, null); + if (face.person && face.person.faceAssetId === face.id) { + changeFeaturePhoto.push(face.person.id); + } + results.push({ id: face.id, success: true }); + } } - if (face.person && face.person.faceAssetId === face.id) { - await this.createNewFeaturePhoto([face.person.id]); + if (changeFeaturePhoto.length > 0) { + // Remove duplicates + await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto))); } - return await this.findOrFail(personId).then(mapPerson); + return results; } async getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise { diff --git a/server/src/immich/controllers/face.controller.ts b/server/src/immich/controllers/face.controller.ts index 5b5b559a84..7a05a1bf6b 100644 --- a/server/src/immich/controllers/face.controller.ts +++ b/server/src/immich/controllers/face.controller.ts @@ -18,12 +18,12 @@ export class FaceController { } @Put(':id') - reassignFacesById( + reassignFace( @AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: FaceDto, ): Promise { - return this.service.reassignFacesById(authUser, id, dto); + return this.service.reassignFace(authUser, id, dto); } @Delete(':id') diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 51f222c7d6..1318c2327d 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -13,7 +13,7 @@ import { PersonStatisticsResponseDto, PersonUpdateDto, } from '@app/domain'; -import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AuthUser, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -49,6 +49,11 @@ export class PersonController { return this.service.reassignFaces(authUser, id, dto); } + @Delete() + unassignFaces(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetFaceUpdateDto): Promise { + return this.service.unassignFaces(authUser, dto); + } + @Put() updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updatePeople(authUser, dto); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 7796ab27a4..715f61999d 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -11618,11 +11618,11 @@ export const FaceApiAxiosParamCreator = function (configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise => { + reassignFace: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined - assertParamExists('reassignFacesById', 'id', id) + assertParamExists('reassignFace', 'id', id) // verify required parameter 'faceDto' is not null or undefined - assertParamExists('reassignFacesById', 'faceDto', faceDto) + assertParamExists('reassignFace', 'faceDto', faceDto) const localVarPath = `/face/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. @@ -11728,8 +11728,8 @@ export const FaceApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options); + async reassignFace(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFace(id, faceDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -11763,12 +11763,12 @@ export const FaceApiFactory = function (configuration?: Configuration, basePath? }, /** * - * @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters. + * @param {FaceApiReassignFaceRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath)); + reassignFace(requestParameters: FaceApiReassignFaceRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.reassignFace(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath)); }, /** * @@ -11797,22 +11797,22 @@ export interface FaceApiGetFacesRequest { } /** - * Request parameters for reassignFacesById operation in FaceApi. + * Request parameters for reassignFace operation in FaceApi. * @export - * @interface FaceApiReassignFacesByIdRequest + * @interface FaceApiReassignFaceRequest */ -export interface FaceApiReassignFacesByIdRequest { +export interface FaceApiReassignFaceRequest { /** * * @type {string} - * @memberof FaceApiReassignFacesById + * @memberof FaceApiReassignFace */ readonly id: string /** * * @type {FaceDto} - * @memberof FaceApiReassignFacesById + * @memberof FaceApiReassignFace */ readonly faceDto: FaceDto } @@ -11851,13 +11851,13 @@ export class FaceApi extends BaseAPI { /** * - * @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters. + * @param {FaceApiReassignFaceRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof FaceApi */ - public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) { - return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath)); + public reassignFace(requestParameters: FaceApiReassignFaceRequest, options?: AxiosRequestConfig) { + return FaceApiFp(this.configuration).reassignFace(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath)); } /** @@ -14037,6 +14037,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + unassignFaces: async (assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'assetFaceUpdateDto' is not null or undefined + assertParamExists('unassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto) + const localVarPath = `/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: 'DELETE', ...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) + + + localVarHeaderParameter['Content-Type'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -14232,6 +14276,16 @@ export const PersonApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async unassignFaces(assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.unassignFaces(assetFaceUpdateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {PeopleUpdateDto} peopleUpdateDto @@ -14334,6 +14388,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PersonApiUnassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. @@ -14467,6 +14530,20 @@ export interface PersonApiReassignFacesRequest { readonly assetFaceUpdateDto: AssetFaceUpdateDto } +/** + * Request parameters for unassignFaces operation in PersonApi. + * @export + * @interface PersonApiUnassignFacesRequest + */ +export interface PersonApiUnassignFacesRequest { + /** + * + * @type {AssetFaceUpdateDto} + * @memberof PersonApiUnassignFaces + */ + readonly assetFaceUpdateDto: AssetFaceUpdateDto +} + /** * Request parameters for updatePeople operation in PersonApi. * @export @@ -14596,6 +14673,17 @@ export class PersonApi extends BaseAPI { return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {PersonApiUnassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PersonApi + */ + public unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig) { + return PersonApiFp(this.configuration).unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index cf61f1913c..acf2956a3c 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -197,14 +197,14 @@ const personId = selectedPersonToReassign[i]?.id; if (personId) { - await api.faceApi.reassignFacesById({ + await api.faceApi.reassignFace({ id: personId, faceDto: { id: peopleWithFaces[i].id }, }); } else if (selectedPersonToCreate[i]) { const { data } = await api.personApi.createPerson(); idsOfPersonToCreate.push(data.id); - await api.faceApi.reassignFacesById({ + await api.faceApi.reassignFace({ id: data.id, faceDto: { id: peopleWithFaces[i].id }, }); @@ -212,14 +212,14 @@ } for (const face of selectedPersonToAdd) { if (face.person) { - await api.faceApi.reassignFacesById({ + await api.faceApi.reassignFace({ id: face.person.id, faceDto: { id: face.id }, }); } else { const { data } = await api.personApi.createPerson(); idsOfPersonToCreate.push(data.id); - await api.faceApi.reassignFacesById({ + await api.faceApi.reassignFace({ id: data.id, faceDto: { id: face.id }, }); diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte index 8a7f49cdc3..8849e3cb7e 100644 --- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte +++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte @@ -6,7 +6,7 @@ import { api, AssetFaceUpdateItem, type PersonResponseDto } from '@api'; import ControlAppBar from '../shared-components/control-app-bar.svelte'; import Button from '../elements/buttons/button.svelte'; - import { mdiPlus, mdiMerge } from '@mdi/js'; + import { mdiPlus, mdiMerge, mdiTagRemove } from '@mdi/js'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { handleError } from '$lib/utils/handle-error'; import { notificationController, NotificationType } from '../shared-components/notification/notification'; @@ -21,6 +21,7 @@ let disableButtons = false; let showLoadingSpinnerCreate = false; let showLoadingSpinnerReassign = false; + let showLoadingSpinnerUnassign = false; let hasSelection = false; let screenHeight: number; @@ -33,7 +34,9 @@ const selectedPeople: AssetFaceUpdateItem[] = []; for (const assetId of assetIds) { + console.log('h'); selectedPeople.push({ assetId, personId: personAssets.id }); + console.log(selectedPeople); } onMount(async () => { @@ -109,6 +112,29 @@ showLoadingSpinnerReassign = false; dispatch('confirm'); }; + + const handleUnassign = async () => { + const timeout = setTimeout(() => (showLoadingSpinnerUnassign = true), 100); + + try { + disableButtons = true; + await api.personApi.unassignFaces({ + assetFaceUpdateDto: { data: selectedPeople }, + }); + + notificationController.show({ + message: `Un-assigned ${assetIds.length} asset${assetIds.length > 1 ? 's' : ''}`, + type: NotificationType.Info, + }); + } catch (error) { + handleError(error, 'Unable to unassign assets'); + } finally { + clearTimeout(timeout); + } + + showLoadingSpinnerCreate = false; + dispatch('confirm'); + }; @@ -124,6 +150,21 @@
+