diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 54cc92149..f8b8188ac 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -586,6 +586,142 @@ export const AssetBulkUploadCheckResultReasonEnum = { export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum]; +/** + * + * @export + * @interface AssetFaceResponseDto + */ +export interface AssetFaceResponseDto { + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxX1': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxX2': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxY1': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxY2': number; + /** + * + * @type {string} + * @memberof AssetFaceResponseDto + */ + 'id': string; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'imageHeight': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'imageWidth': number; + /** + * + * @type {PersonResponseDto} + * @memberof AssetFaceResponseDto + */ + 'person': PersonResponseDto | null; +} +/** + * + * @export + * @interface AssetFaceUpdateDto + */ +export interface AssetFaceUpdateDto { + /** + * + * @type {Array} + * @memberof AssetFaceUpdateDto + */ + 'data': Array; +} +/** + * + * @export + * @interface AssetFaceUpdateItem + */ +export interface AssetFaceUpdateItem { + /** + * + * @type {string} + * @memberof AssetFaceUpdateItem + */ + 'assetId': string; + /** + * + * @type {string} + * @memberof AssetFaceUpdateItem + */ + 'personId': string; +} +/** + * + * @export + * @interface AssetFaceWithoutPersonResponseDto + */ +export interface AssetFaceWithoutPersonResponseDto { + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxX1': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxX2': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxY1': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxY2': number; + /** + * + * @type {string} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'id': string; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'imageHeight': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'imageWidth': number; +} /** * * @export @@ -842,10 +978,10 @@ export interface AssetResponseDto { 'ownerId': string; /** * - * @type {Array} + * @type {Array} * @memberof AssetResponseDto */ - 'people'?: Array; + 'people'?: Array; /** * * @type {boolean} @@ -1672,6 +1808,19 @@ export interface ExifResponseDto { */ 'timeZone'?: string | null; } +/** + * + * @export + * @interface FaceDto + */ +export interface FaceDto { + /** + * + * @type {string} + * @memberof FaceDto + */ + 'id': string; +} /** * * @export @@ -2564,6 +2713,49 @@ export interface PersonUpdateDto { */ 'name'?: string; } +/** + * + * @export + * @interface PersonWithFacesResponseDto + */ +export interface PersonWithFacesResponseDto { + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'birthDate': string | null; + /** + * + * @type {Array} + * @memberof PersonWithFacesResponseDto + */ + 'faces': Array; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'id': string; + /** + * + * @type {boolean} + * @memberof PersonWithFacesResponseDto + */ + 'isHidden': boolean; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'name': string; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'thumbnailPath': string; +} /** * * @export @@ -11349,6 +11541,233 @@ export class AuthenticationApi extends BaseAPI { } +/** + * FaceApi - axios parameter creator + * @export + */ +export const FaceApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getFaces', 'id', id) + const localVarPath = `/face`; + // 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 (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {FaceDto} faceDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('reassignFacesById', 'id', id) + // verify required parameter 'faceDto' is not null or undefined + assertParamExists('reassignFacesById', 'faceDto', faceDto) + const localVarPath = `/face/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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: 'PUT', ...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); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * FaceApi - functional programming interface + * @export + */ +export const FaceApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration) + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {FaceDto} faceDto + * @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); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * FaceApi - factory interface + * @export + */ +export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = FaceApiFp(configuration) + return { + /** + * + * @param {FaceApiGetFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {FaceApiReassignFacesByIdRequest} 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)); + }, + }; +}; + +/** + * Request parameters for getFaces operation in FaceApi. + * @export + * @interface FaceApiGetFacesRequest + */ +export interface FaceApiGetFacesRequest { + /** + * + * @type {string} + * @memberof FaceApiGetFaces + */ + readonly id: string +} + +/** + * Request parameters for reassignFacesById operation in FaceApi. + * @export + * @interface FaceApiReassignFacesByIdRequest + */ +export interface FaceApiReassignFacesByIdRequest { + /** + * + * @type {string} + * @memberof FaceApiReassignFacesById + */ + readonly id: string + + /** + * + * @type {FaceDto} + * @memberof FaceApiReassignFacesById + */ + readonly faceDto: FaceDto +} + +/** + * FaceApi - object-oriented interface + * @export + * @class FaceApi + * @extends {BaseAPI} + */ +export class FaceApi extends BaseAPI { + /** + * + * @param {FaceApiGetFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FaceApi + */ + public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) { + return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {FaceApiReassignFacesByIdRequest} 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)); + } +} + + /** * JobApi - axios parameter creator * @export @@ -13180,6 +13599,44 @@ export class PartnerApi extends BaseAPI { */ export const PersonApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPerson: async (options: AxiosRequestConfig = {}): Promise => { + 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: 'POST', ...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) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {boolean} [withHidden] @@ -13439,6 +13896,54 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio options: localVarRequestOptions, }; }, + /** + * + * @param {string} id + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('reassignFaces', 'id', id) + // verify required parameter 'assetFaceUpdateDto' is not null or undefined + assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto) + const localVarPath = `/person/{id}/reassign` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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: 'PUT', ...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); + 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 {PeopleUpdateDto} peopleUpdateDto @@ -13541,6 +14046,15 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio export const PersonApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {boolean} [withHidden] @@ -13602,6 +14116,17 @@ export const PersonApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {PeopleUpdateDto} peopleUpdateDto @@ -13633,6 +14158,14 @@ export const PersonApiFp = function(configuration?: Configuration) { export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = PersonApiFp(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPerson(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.createPerson(options).then((request) => request(axios, basePath)); + }, /** * * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. @@ -13687,6 +14220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PersonApiReassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. @@ -13799,6 +14341,27 @@ export interface PersonApiMergePersonRequest { readonly mergePersonDto: MergePersonDto } +/** + * Request parameters for reassignFaces operation in PersonApi. + * @export + * @interface PersonApiReassignFacesRequest + */ +export interface PersonApiReassignFacesRequest { + /** + * + * @type {string} + * @memberof PersonApiReassignFaces + */ + readonly id: string + + /** + * + * @type {AssetFaceUpdateDto} + * @memberof PersonApiReassignFaces + */ + readonly assetFaceUpdateDto: AssetFaceUpdateDto +} + /** * Request parameters for updatePeople operation in PersonApi. * @export @@ -13841,6 +14404,16 @@ export interface PersonApiUpdatePersonRequest { * @extends {BaseAPI} */ export class PersonApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PersonApi + */ + public createPerson(options?: AxiosRequestConfig) { + return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. @@ -13907,6 +14480,17 @@ export class PersonApi extends BaseAPI { return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {PersonApiReassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PersonApi + */ + public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) { + return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index f54b788a4..1d72f2249 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -24,6 +24,10 @@ doc/AssetBulkUploadCheckDto.md doc/AssetBulkUploadCheckItem.md doc/AssetBulkUploadCheckResponseDto.md doc/AssetBulkUploadCheckResult.md +doc/AssetFaceResponseDto.md +doc/AssetFaceUpdateDto.md +doc/AssetFaceUpdateItem.md +doc/AssetFaceWithoutPersonResponseDto.md doc/AssetFileUploadResponseDto.md doc/AssetIdsDto.md doc/AssetIdsResponseDto.md @@ -60,6 +64,8 @@ doc/DownloadInfoDto.md doc/DownloadResponseDto.md doc/EntityType.md doc/ExifResponseDto.md +doc/FaceApi.md +doc/FaceDto.md doc/FileChecksumDto.md doc/FileChecksumResponseDto.md doc/FileReportDto.md @@ -100,6 +106,7 @@ doc/PersonApi.md doc/PersonResponseDto.md doc/PersonStatisticsResponseDto.md doc/PersonUpdateDto.md +doc/PersonWithFacesResponseDto.md doc/QueueStatusDto.md doc/ReactionLevel.md doc/ReactionType.md @@ -177,6 +184,7 @@ lib/api/api_key_api.dart lib/api/asset_api.dart lib/api/audit_api.dart lib/api/authentication_api.dart +lib/api/face_api.dart lib/api/job_api.dart lib/api/library_api.dart lib/api/o_auth_api.dart @@ -213,6 +221,10 @@ lib/model/asset_bulk_upload_check_dto.dart lib/model/asset_bulk_upload_check_item.dart lib/model/asset_bulk_upload_check_response_dto.dart lib/model/asset_bulk_upload_check_result.dart +lib/model/asset_face_response_dto.dart +lib/model/asset_face_update_dto.dart +lib/model/asset_face_update_item.dart +lib/model/asset_face_without_person_response_dto.dart lib/model/asset_file_upload_response_dto.dart lib/model/asset_ids_dto.dart lib/model/asset_ids_response_dto.dart @@ -247,6 +259,7 @@ lib/model/download_info_dto.dart lib/model/download_response_dto.dart lib/model/entity_type.dart lib/model/exif_response_dto.dart +lib/model/face_dto.dart lib/model/file_checksum_dto.dart lib/model/file_checksum_response_dto.dart lib/model/file_report_dto.dart @@ -282,6 +295,7 @@ lib/model/people_update_item.dart lib/model/person_response_dto.dart lib/model/person_statistics_response_dto.dart lib/model/person_update_dto.dart +lib/model/person_with_faces_response_dto.dart lib/model/queue_status_dto.dart lib/model/reaction_level.dart lib/model/reaction_type.dart @@ -367,6 +381,10 @@ test/asset_bulk_upload_check_dto_test.dart test/asset_bulk_upload_check_item_test.dart test/asset_bulk_upload_check_response_dto_test.dart test/asset_bulk_upload_check_result_test.dart +test/asset_face_response_dto_test.dart +test/asset_face_update_dto_test.dart +test/asset_face_update_item_test.dart +test/asset_face_without_person_response_dto_test.dart test/asset_file_upload_response_dto_test.dart test/asset_ids_dto_test.dart test/asset_ids_response_dto_test.dart @@ -403,6 +421,8 @@ test/download_info_dto_test.dart test/download_response_dto_test.dart test/entity_type_test.dart test/exif_response_dto_test.dart +test/face_api_test.dart +test/face_dto_test.dart test/file_checksum_dto_test.dart test/file_checksum_response_dto_test.dart test/file_report_dto_test.dart @@ -443,6 +463,7 @@ test/person_api_test.dart test/person_response_dto_test.dart test/person_statistics_response_dto_test.dart test/person_update_dto_test.dart +test/person_with_faces_response_dto_test.dart test/queue_status_dto_test.dart test/reaction_level_test.dart test/reaction_type_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 42680e679..7eb7f7f56 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -133,6 +133,8 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices | *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} | *JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs | *JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} | *LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library | @@ -153,12 +155,14 @@ Class | Method | HTTP request | Description *PartnerApi* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner | *PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} | *PartnerApi* | [**updatePartner**](doc//PartnerApi.md#updatepartner) | **PUT** /partner/{id} | +*PersonApi* | [**createPerson**](doc//PersonApi.md#createperson) | **POST** /person | *PersonApi* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person | *PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} | *PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | *PersonApi* | [**getPersonStatistics**](doc//PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics | *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* | [**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 | @@ -224,6 +228,10 @@ Class | Method | HTTP request | Description - [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md) - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) + - [AssetFaceResponseDto](doc//AssetFaceResponseDto.md) + - [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md) + - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) + - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) @@ -258,6 +266,7 @@ Class | Method | HTTP request | Description - [DownloadResponseDto](doc//DownloadResponseDto.md) - [EntityType](doc//EntityType.md) - [ExifResponseDto](doc//ExifResponseDto.md) + - [FaceDto](doc//FaceDto.md) - [FileChecksumDto](doc//FileChecksumDto.md) - [FileChecksumResponseDto](doc//FileChecksumResponseDto.md) - [FileReportDto](doc//FileReportDto.md) @@ -293,6 +302,7 @@ Class | Method | HTTP request | Description - [PersonResponseDto](doc//PersonResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md) + - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md) - [QueueStatusDto](doc//QueueStatusDto.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) diff --git a/mobile/openapi/doc/AssetFaceResponseDto.md b/mobile/openapi/doc/AssetFaceResponseDto.md new file mode 100644 index 000000000..00ded8b47 --- /dev/null +++ b/mobile/openapi/doc/AssetFaceResponseDto.md @@ -0,0 +1,22 @@ +# openapi.model.AssetFaceResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**boundingBoxX1** | **int** | | +**boundingBoxX2** | **int** | | +**boundingBoxY1** | **int** | | +**boundingBoxY2** | **int** | | +**id** | **String** | | +**imageHeight** | **int** | | +**imageWidth** | **int** | | +**person** | [**PersonResponseDto**](PersonResponseDto.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetFaceUpdateDto.md b/mobile/openapi/doc/AssetFaceUpdateDto.md new file mode 100644 index 000000000..eb4e4d387 --- /dev/null +++ b/mobile/openapi/doc/AssetFaceUpdateDto.md @@ -0,0 +1,15 @@ +# openapi.model.AssetFaceUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**data** | [**List**](AssetFaceUpdateItem.md) | | [default to const []] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetFaceUpdateItem.md b/mobile/openapi/doc/AssetFaceUpdateItem.md new file mode 100644 index 000000000..6a98288ed --- /dev/null +++ b/mobile/openapi/doc/AssetFaceUpdateItem.md @@ -0,0 +1,16 @@ +# openapi.model.AssetFaceUpdateItem + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assetId** | **String** | | +**personId** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md b/mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md new file mode 100644 index 000000000..5d421dbef --- /dev/null +++ b/mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md @@ -0,0 +1,21 @@ +# openapi.model.AssetFaceWithoutPersonResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**boundingBoxX1** | **int** | | +**boundingBoxX2** | **int** | | +**boundingBoxY1** | **int** | | +**boundingBoxY2** | **int** | | +**id** | **String** | | +**imageHeight** | **int** | | +**imageWidth** | **int** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index 8c4d1db4a..7c79f7418 100644 --- a/mobile/openapi/doc/AssetResponseDto.md +++ b/mobile/openapi/doc/AssetResponseDto.md @@ -30,7 +30,7 @@ Name | Type | Description | Notes **originalPath** | **String** | | **owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional] **ownerId** | **String** | | -**people** | [**List**](PersonResponseDto.md) | | [optional] [default to const []] +**people** | [**List**](PersonWithFacesResponseDto.md) | | [optional] [default to const []] **resized** | **bool** | | **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional] **stack** | [**List**](AssetResponseDto.md) | | [optional] [default to const []] diff --git a/mobile/openapi/doc/FaceApi.md b/mobile/openapi/doc/FaceApi.md new file mode 100644 index 000000000..84793a5c9 --- /dev/null +++ b/mobile/openapi/doc/FaceApi.md @@ -0,0 +1,127 @@ +# openapi.api.FaceApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**getFaces**](FaceApi.md#getfaces) | **GET** /face | +[**reassignFacesById**](FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} | + + +# **getFaces** +> List getFaces(id) + + + +### 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 = FaceApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + final result = api_instance.getFaces(id); + print(result); +} catch (e) { + print('Exception when calling FaceApi->getFaces: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +[**List**](AssetFaceResponseDto.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) + +# **reassignFacesById** +> PersonResponseDto reassignFacesById(id, faceDto) + + + +### 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 = FaceApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final faceDto = FaceDto(); // FaceDto | + +try { + final result = api_instance.reassignFacesById(id, faceDto); + print(result); +} catch (e) { + print('Exception when calling FaceApi->reassignFacesById: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **faceDto** | [**FaceDto**](FaceDto.md)| | + +### Return type + +[**PersonResponseDto**](PersonResponseDto.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) + diff --git a/mobile/openapi/doc/FaceDto.md b/mobile/openapi/doc/FaceDto.md new file mode 100644 index 000000000..21144a34e --- /dev/null +++ b/mobile/openapi/doc/FaceDto.md @@ -0,0 +1,15 @@ +# openapi.model.FaceDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**id** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index 73a35f0f3..6ad2d1985 100644 --- a/mobile/openapi/doc/PersonApi.md +++ b/mobile/openapi/doc/PersonApi.md @@ -9,16 +9,69 @@ All URIs are relative to */api* Method | HTTP request | Description ------------- | ------------- | ------------- +[**createPerson**](PersonApi.md#createperson) | **POST** /person | [**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person | [**getPerson**](PersonApi.md#getperson) | **GET** /person/{id} | [**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | [**getPersonStatistics**](PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics | [**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | [**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge | +[**reassignFaces**](PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign | [**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person | [**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} | +# **createPerson** +> PersonResponseDto createPerson() + + + +### 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(); + +try { + final result = api_instance.createPerson(); + print(result); +} catch (e) { + print('Exception when calling PersonApi->createPerson: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**PersonResponseDto**](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) + # **getAllPeople** > PeopleResponseDto getAllPeople(withHidden) @@ -351,6 +404,63 @@ 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) +# **reassignFaces** +> List reassignFaces(id, 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 id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto | + +try { + final result = api_instance.reassignFaces(id, assetFaceUpdateDto); + print(result); +} catch (e) { + print('Exception when calling PersonApi->reassignFaces: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.md)| | + +### 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**: 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/doc/PersonWithFacesResponseDto.md b/mobile/openapi/doc/PersonWithFacesResponseDto.md new file mode 100644 index 000000000..ddef73618 --- /dev/null +++ b/mobile/openapi/doc/PersonWithFacesResponseDto.md @@ -0,0 +1,20 @@ +# openapi.model.PersonWithFacesResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**birthDate** | [**DateTime**](DateTime.md) | | +**faces** | [**List**](AssetFaceWithoutPersonResponseDto.md) | | [default to const []] +**id** | **String** | | +**isHidden** | **bool** | | +**name** | **String** | | +**thumbnailPath** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 894162693..c0caf20e4 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -34,6 +34,7 @@ part 'api/album_api.dart'; part 'api/asset_api.dart'; part 'api/audit_api.dart'; part 'api/authentication_api.dart'; +part 'api/face_api.dart'; part 'api/job_api.dart'; part 'api/library_api.dart'; part 'api/o_auth_api.dart'; @@ -63,6 +64,10 @@ part 'model/asset_bulk_upload_check_dto.dart'; part 'model/asset_bulk_upload_check_item.dart'; part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_result.dart'; +part 'model/asset_face_response_dto.dart'; +part 'model/asset_face_update_dto.dart'; +part 'model/asset_face_update_item.dart'; +part 'model/asset_face_without_person_response_dto.dart'; part 'model/asset_file_upload_response_dto.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; @@ -97,6 +102,7 @@ part 'model/download_info_dto.dart'; part 'model/download_response_dto.dart'; part 'model/entity_type.dart'; part 'model/exif_response_dto.dart'; +part 'model/face_dto.dart'; part 'model/file_checksum_dto.dart'; part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; @@ -132,6 +138,7 @@ part 'model/people_update_item.dart'; part 'model/person_response_dto.dart'; part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; +part 'model/person_with_faces_response_dto.dart'; part 'model/queue_status_dto.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; diff --git a/mobile/openapi/lib/api/face_api.dart b/mobile/openapi/lib/api/face_api.dart new file mode 100644 index 000000000..bce681407 --- /dev/null +++ b/mobile/openapi/lib/api/face_api.dart @@ -0,0 +1,122 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class FaceApi { + FaceApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /face' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getFacesWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/face'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'id', id)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future?> getFaces(String id,) async { + final response = await getFacesWithHttpInfo(id,); + 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 /face/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [FaceDto] faceDto (required): + Future reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async { + // ignore: prefer_const_declarations + final path = r'/face/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = faceDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [FaceDto] faceDto (required): + Future reassignFacesById(String id, FaceDto faceDto,) async { + final response = await reassignFacesByIdWithHttpInfo(id, faceDto,); + 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) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api/person_api.dart b/mobile/openapi/lib/api/person_api.dart index e4ab011a6..b603df6d3 100644 --- a/mobile/openapi/lib/api/person_api.dart +++ b/mobile/openapi/lib/api/person_api.dart @@ -16,6 +16,47 @@ class PersonApi { final ApiClient apiClient; + /// Performs an HTTP 'POST /person' operation and returns the [Response]. + Future createPersonWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/person'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future createPerson() async { + final response = await createPersonWithHttpInfo(); + 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) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /person' operation and returns the [Response]. /// Parameters: /// @@ -317,6 +358,61 @@ class PersonApi { return null; } + /// Performs an HTTP 'PUT /person/{id}/reassign' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): + Future reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/person/{id}/reassign' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = assetFaceUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetFaceUpdateDto] assetFaceUpdateDto (required): + Future?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async { + final response = await reassignFacesWithHttpInfo(id, 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/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 42a0e5cbb..7d376949e 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -215,6 +215,14 @@ class ApiClient { return AssetBulkUploadCheckResponseDto.fromJson(value); case 'AssetBulkUploadCheckResult': return AssetBulkUploadCheckResult.fromJson(value); + case 'AssetFaceResponseDto': + return AssetFaceResponseDto.fromJson(value); + case 'AssetFaceUpdateDto': + return AssetFaceUpdateDto.fromJson(value); + case 'AssetFaceUpdateItem': + return AssetFaceUpdateItem.fromJson(value); + case 'AssetFaceWithoutPersonResponseDto': + return AssetFaceWithoutPersonResponseDto.fromJson(value); case 'AssetFileUploadResponseDto': return AssetFileUploadResponseDto.fromJson(value); case 'AssetIdsDto': @@ -283,6 +291,8 @@ class ApiClient { return EntityTypeTypeTransformer().decode(value); case 'ExifResponseDto': return ExifResponseDto.fromJson(value); + case 'FaceDto': + return FaceDto.fromJson(value); case 'FileChecksumDto': return FileChecksumDto.fromJson(value); case 'FileChecksumResponseDto': @@ -353,6 +363,8 @@ class ApiClient { return PersonStatisticsResponseDto.fromJson(value); case 'PersonUpdateDto': return PersonUpdateDto.fromJson(value); + case 'PersonWithFacesResponseDto': + return PersonWithFacesResponseDto.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); case 'ReactionLevel': diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart new file mode 100644 index 000000000..642622999 --- /dev/null +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -0,0 +1,158 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetFaceResponseDto { + /// Returns a new [AssetFaceResponseDto] instance. + AssetFaceResponseDto({ + required this.boundingBoxX1, + required this.boundingBoxX2, + required this.boundingBoxY1, + required this.boundingBoxY2, + required this.id, + required this.imageHeight, + required this.imageWidth, + required this.person, + }); + + int boundingBoxX1; + + int boundingBoxX2; + + int boundingBoxY1; + + int boundingBoxY2; + + String id; + + int imageHeight; + + int imageWidth; + + PersonResponseDto? person; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxY2 == boundingBoxY2 && + other.id == id && + other.imageHeight == imageHeight && + other.imageWidth == imageWidth && + other.person == person; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (boundingBoxX1.hashCode) + + (boundingBoxX2.hashCode) + + (boundingBoxY1.hashCode) + + (boundingBoxY2.hashCode) + + (id.hashCode) + + (imageHeight.hashCode) + + (imageWidth.hashCode) + + (person == null ? 0 : person!.hashCode); + + @override + String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person]'; + + Map toJson() { + final json = {}; + json[r'boundingBoxX1'] = this.boundingBoxX1; + json[r'boundingBoxX2'] = this.boundingBoxX2; + json[r'boundingBoxY1'] = this.boundingBoxY1; + json[r'boundingBoxY2'] = this.boundingBoxY2; + json[r'id'] = this.id; + json[r'imageHeight'] = this.imageHeight; + json[r'imageWidth'] = this.imageWidth; + if (this.person != null) { + json[r'person'] = this.person; + } else { + // json[r'person'] = null; + } + return json; + } + + /// Returns a new [AssetFaceResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetFaceResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AssetFaceResponseDto( + boundingBoxX1: mapValueOfType(json, r'boundingBoxX1')!, + boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, + boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, + boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, + id: mapValueOfType(json, r'id')!, + imageHeight: mapValueOfType(json, r'imageHeight')!, + imageWidth: mapValueOfType(json, r'imageWidth')!, + person: PersonResponseDto.fromJson(json[r'person']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetFaceResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetFaceResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetFaceResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetFaceResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'boundingBoxX1', + 'boundingBoxX2', + 'boundingBoxY1', + 'boundingBoxY2', + 'id', + 'imageHeight', + 'imageWidth', + 'person', + }; +} + diff --git a/mobile/openapi/lib/model/asset_face_update_dto.dart b/mobile/openapi/lib/model/asset_face_update_dto.dart new file mode 100644 index 000000000..89a11b0ab --- /dev/null +++ b/mobile/openapi/lib/model/asset_face_update_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetFaceUpdateDto { + /// Returns a new [AssetFaceUpdateDto] instance. + AssetFaceUpdateDto({ + this.data = const [], + }); + + List data; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetFaceUpdateDto && + other.data == data; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (data.hashCode); + + @override + String toString() => 'AssetFaceUpdateDto[data=$data]'; + + Map toJson() { + final json = {}; + json[r'data'] = this.data; + return json; + } + + /// Returns a new [AssetFaceUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetFaceUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AssetFaceUpdateDto( + data: AssetFaceUpdateItem.listFromJson(json[r'data']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetFaceUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetFaceUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetFaceUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetFaceUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'data', + }; +} + diff --git a/mobile/openapi/lib/model/asset_face_update_item.dart b/mobile/openapi/lib/model/asset_face_update_item.dart new file mode 100644 index 000000000..ca078683a --- /dev/null +++ b/mobile/openapi/lib/model/asset_face_update_item.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetFaceUpdateItem { + /// Returns a new [AssetFaceUpdateItem] instance. + AssetFaceUpdateItem({ + required this.assetId, + required this.personId, + }); + + String assetId; + + String personId; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetFaceUpdateItem && + other.assetId == assetId && + other.personId == personId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (personId.hashCode); + + @override + String toString() => 'AssetFaceUpdateItem[assetId=$assetId, personId=$personId]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'personId'] = this.personId; + return json; + } + + /// Returns a new [AssetFaceUpdateItem] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetFaceUpdateItem? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AssetFaceUpdateItem( + assetId: mapValueOfType(json, r'assetId')!, + personId: mapValueOfType(json, r'personId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetFaceUpdateItem.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetFaceUpdateItem.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetFaceUpdateItem-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetFaceUpdateItem.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'personId', + }; +} + diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart new file mode 100644 index 000000000..8e1a9c66a --- /dev/null +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -0,0 +1,146 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetFaceWithoutPersonResponseDto { + /// Returns a new [AssetFaceWithoutPersonResponseDto] instance. + AssetFaceWithoutPersonResponseDto({ + required this.boundingBoxX1, + required this.boundingBoxX2, + required this.boundingBoxY1, + required this.boundingBoxY2, + required this.id, + required this.imageHeight, + required this.imageWidth, + }); + + int boundingBoxX1; + + int boundingBoxX2; + + int boundingBoxY1; + + int boundingBoxY2; + + String id; + + int imageHeight; + + int imageWidth; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxY2 == boundingBoxY2 && + other.id == id && + other.imageHeight == imageHeight && + other.imageWidth == imageWidth; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (boundingBoxX1.hashCode) + + (boundingBoxX2.hashCode) + + (boundingBoxY1.hashCode) + + (boundingBoxY2.hashCode) + + (id.hashCode) + + (imageHeight.hashCode) + + (imageWidth.hashCode); + + @override + String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth]'; + + Map toJson() { + final json = {}; + json[r'boundingBoxX1'] = this.boundingBoxX1; + json[r'boundingBoxX2'] = this.boundingBoxX2; + json[r'boundingBoxY1'] = this.boundingBoxY1; + json[r'boundingBoxY2'] = this.boundingBoxY2; + json[r'id'] = this.id; + json[r'imageHeight'] = this.imageHeight; + json[r'imageWidth'] = this.imageWidth; + return json; + } + + /// Returns a new [AssetFaceWithoutPersonResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AssetFaceWithoutPersonResponseDto( + boundingBoxX1: mapValueOfType(json, r'boundingBoxX1')!, + boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, + boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, + boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, + id: mapValueOfType(json, r'id')!, + imageHeight: mapValueOfType(json, r'imageHeight')!, + imageWidth: mapValueOfType(json, r'imageWidth')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetFaceWithoutPersonResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetFaceWithoutPersonResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetFaceWithoutPersonResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetFaceWithoutPersonResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'boundingBoxX1', + 'boundingBoxX2', + 'boundingBoxY1', + 'boundingBoxY2', + 'id', + 'imageHeight', + 'imageWidth', + }; +} + diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 746118681..bb7330d3e 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -104,7 +104,7 @@ class AssetResponseDto { String ownerId; - List people; + List people; bool resized; @@ -299,7 +299,7 @@ class AssetResponseDto { originalPath: mapValueOfType(json, r'originalPath')!, owner: UserResponseDto.fromJson(json[r'owner']), ownerId: mapValueOfType(json, r'ownerId')!, - people: PersonResponseDto.listFromJson(json[r'people']), + people: PersonWithFacesResponseDto.listFromJson(json[r'people']), resized: mapValueOfType(json, r'resized')!, smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']), stack: AssetResponseDto.listFromJson(json[r'stack']), diff --git a/mobile/openapi/lib/model/face_dto.dart b/mobile/openapi/lib/model/face_dto.dart new file mode 100644 index 000000000..556fd0efa --- /dev/null +++ b/mobile/openapi/lib/model/face_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class FaceDto { + /// Returns a new [FaceDto] instance. + FaceDto({ + required this.id, + }); + + String id; + + @override + bool operator ==(Object other) => identical(this, other) || other is FaceDto && + other.id == id; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (id.hashCode); + + @override + String toString() => 'FaceDto[id=$id]'; + + Map toJson() { + final json = {}; + json[r'id'] = this.id; + return json; + } + + /// Returns a new [FaceDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static FaceDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return FaceDto( + id: mapValueOfType(json, r'id')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = FaceDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = FaceDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of FaceDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = FaceDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'id', + }; +} + diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart new file mode 100644 index 000000000..764819430 --- /dev/null +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -0,0 +1,142 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PersonWithFacesResponseDto { + /// Returns a new [PersonWithFacesResponseDto] instance. + PersonWithFacesResponseDto({ + required this.birthDate, + this.faces = const [], + required this.id, + required this.isHidden, + required this.name, + required this.thumbnailPath, + }); + + DateTime? birthDate; + + List faces; + + String id; + + bool isHidden; + + String name; + + String thumbnailPath; + + @override + bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto && + other.birthDate == birthDate && + other.faces == faces && + other.id == id && + other.isHidden == isHidden && + other.name == name && + other.thumbnailPath == thumbnailPath; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (birthDate == null ? 0 : birthDate!.hashCode) + + (faces.hashCode) + + (id.hashCode) + + (isHidden.hashCode) + + (name.hashCode) + + (thumbnailPath.hashCode); + + @override + String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]'; + + Map toJson() { + final json = {}; + if (this.birthDate != null) { + json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); + } else { + // json[r'birthDate'] = null; + } + json[r'faces'] = this.faces; + json[r'id'] = this.id; + json[r'isHidden'] = this.isHidden; + json[r'name'] = this.name; + json[r'thumbnailPath'] = this.thumbnailPath; + return json; + } + + /// Returns a new [PersonWithFacesResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PersonWithFacesResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PersonWithFacesResponseDto( + birthDate: mapDateTime(json, r'birthDate', ''), + faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']), + id: mapValueOfType(json, r'id')!, + isHidden: mapValueOfType(json, r'isHidden')!, + name: mapValueOfType(json, r'name')!, + thumbnailPath: mapValueOfType(json, r'thumbnailPath')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PersonWithFacesResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PersonWithFacesResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PersonWithFacesResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PersonWithFacesResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'birthDate', + 'faces', + 'id', + 'isHidden', + 'name', + 'thumbnailPath', + }; +} + diff --git a/mobile/openapi/test/asset_face_response_dto_test.dart b/mobile/openapi/test/asset_face_response_dto_test.dart new file mode 100644 index 000000000..cfedbeca9 --- /dev/null +++ b/mobile/openapi/test/asset_face_response_dto_test.dart @@ -0,0 +1,62 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AssetFaceResponseDto +void main() { + // final instance = AssetFaceResponseDto(); + + group('test AssetFaceResponseDto', () { + // int boundingBoxX1 + test('to test the property `boundingBoxX1`', () async { + // TODO + }); + + // int boundingBoxX2 + test('to test the property `boundingBoxX2`', () async { + // TODO + }); + + // int boundingBoxY1 + test('to test the property `boundingBoxY1`', () async { + // TODO + }); + + // int boundingBoxY2 + test('to test the property `boundingBoxY2`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // int imageHeight + test('to test the property `imageHeight`', () async { + // TODO + }); + + // int imageWidth + test('to test the property `imageWidth`', () async { + // TODO + }); + + // PersonResponseDto person + test('to test the property `person`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_face_update_dto_test.dart b/mobile/openapi/test/asset_face_update_dto_test.dart new file mode 100644 index 000000000..5338a0bad --- /dev/null +++ b/mobile/openapi/test/asset_face_update_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AssetFaceUpdateDto +void main() { + // final instance = AssetFaceUpdateDto(); + + group('test AssetFaceUpdateDto', () { + // List data (default value: const []) + test('to test the property `data`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_face_update_item_test.dart b/mobile/openapi/test/asset_face_update_item_test.dart new file mode 100644 index 000000000..a3ef4c335 --- /dev/null +++ b/mobile/openapi/test/asset_face_update_item_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AssetFaceUpdateItem +void main() { + // final instance = AssetFaceUpdateItem(); + + group('test AssetFaceUpdateItem', () { + // String assetId + test('to test the property `assetId`', () async { + // TODO + }); + + // String personId + test('to test the property `personId`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_face_without_person_response_dto_test.dart b/mobile/openapi/test/asset_face_without_person_response_dto_test.dart new file mode 100644 index 000000000..5eb7e5d93 --- /dev/null +++ b/mobile/openapi/test/asset_face_without_person_response_dto_test.dart @@ -0,0 +1,57 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for AssetFaceWithoutPersonResponseDto +void main() { + // final instance = AssetFaceWithoutPersonResponseDto(); + + group('test AssetFaceWithoutPersonResponseDto', () { + // int boundingBoxX1 + test('to test the property `boundingBoxX1`', () async { + // TODO + }); + + // int boundingBoxX2 + test('to test the property `boundingBoxX2`', () async { + // TODO + }); + + // int boundingBoxY1 + test('to test the property `boundingBoxY1`', () async { + // TODO + }); + + // int boundingBoxY2 + test('to test the property `boundingBoxY2`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // int imageHeight + test('to test the property `imageHeight`', () async { + // TODO + }); + + // int imageWidth + test('to test the property `imageWidth`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/asset_response_dto_test.dart b/mobile/openapi/test/asset_response_dto_test.dart index 63668934a..b8a64b6a2 100644 --- a/mobile/openapi/test/asset_response_dto_test.dart +++ b/mobile/openapi/test/asset_response_dto_test.dart @@ -127,7 +127,7 @@ void main() { // TODO }); - // List people (default value: const []) + // List people (default value: const []) test('to test the property `people`', () async { // TODO }); diff --git a/mobile/openapi/test/face_api_test.dart b/mobile/openapi/test/face_api_test.dart new file mode 100644 index 000000000..3bd4d982f --- /dev/null +++ b/mobile/openapi/test/face_api_test.dart @@ -0,0 +1,31 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for FaceApi +void main() { + // final instance = FaceApi(); + + group('tests for FaceApi', () { + //Future> getFaces(String id) async + test('test getFaces', () async { + // TODO + }); + + //Future reassignFacesById(String id, FaceDto faceDto) async + test('test reassignFacesById', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/face_dto_test.dart b/mobile/openapi/test/face_dto_test.dart new file mode 100644 index 000000000..ea8091f2e --- /dev/null +++ b/mobile/openapi/test/face_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for FaceDto +void main() { + // final instance = FaceDto(); + + group('test FaceDto', () { + // String id + test('to test the property `id`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/person_api_test.dart b/mobile/openapi/test/person_api_test.dart index b0feeb116..dd112eeaa 100644 --- a/mobile/openapi/test/person_api_test.dart +++ b/mobile/openapi/test/person_api_test.dart @@ -17,6 +17,11 @@ void main() { // final instance = PersonApi(); group('tests for PersonApi', () { + //Future createPerson() async + test('test createPerson', () async { + // TODO + }); + //Future getAllPeople({ bool withHidden }) async test('test getAllPeople', () async { // TODO @@ -47,6 +52,11 @@ void main() { // TODO }); + //Future> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto) async + test('test reassignFaces', () async { + // TODO + }); + //Future> updatePeople(PeopleUpdateDto peopleUpdateDto) async test('test updatePeople', () async { // TODO diff --git a/mobile/openapi/test/person_with_faces_response_dto_test.dart b/mobile/openapi/test/person_with_faces_response_dto_test.dart new file mode 100644 index 000000000..7f7e0f89a --- /dev/null +++ b/mobile/openapi/test/person_with_faces_response_dto_test.dart @@ -0,0 +1,52 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for PersonWithFacesResponseDto +void main() { + // final instance = PersonWithFacesResponseDto(); + + group('test PersonWithFacesResponseDto', () { + // DateTime birthDate + test('to test the property `birthDate`', () async { + // TODO + }); + + // List faces (default value: const []) + test('to test the property `faces`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // bool isHidden + test('to test the property `isHidden`', () async { + // TODO + }); + + // String name + test('to test the property `name`', () async { + // TODO + }); + + // String thumbnailPath + test('to test the property `thumbnailPath`', () async { + // TODO + }); + + + }); + +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 55b4f81ab..d6355e894 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3220,6 +3220,103 @@ ] } }, + "/face": { + "get": { + "operationId": "getFaces", + "parameters": [ + { + "name": "id", + "required": true, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetFaceResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Face" + ] + } + }, + "/face/{id}": { + "put": { + "operationId": "reassignFacesById", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Face" + ] + } + }, "/jobs": { "get": { "operationId": "getAllJobsStatus", @@ -4022,6 +4119,36 @@ "Person" ] }, + "post": { + "operationId": "createPerson", + "parameters": [], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Person" + ] + }, "put": { "operationId": "updatePeople", "parameters": [], @@ -4258,6 +4385,61 @@ ] } }, + "/person/{id}/reassign": { + "put": { + "operationId": "reassignFaces", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetFaceUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PersonResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Person" + ] + } + }, "/person/{id}/statistics": { "get": { "operationId": "getPersonStatistics", @@ -6557,6 +6739,118 @@ ], "type": "object" }, + "AssetFaceResponseDto": { + "properties": { + "boundingBoxX1": { + "type": "integer" + }, + "boundingBoxX2": { + "type": "integer" + }, + "boundingBoxY1": { + "type": "integer" + }, + "boundingBoxY2": { + "type": "integer" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "imageHeight": { + "type": "integer" + }, + "imageWidth": { + "type": "integer" + }, + "person": { + "allOf": [ + { + "$ref": "#/components/schemas/PersonResponseDto" + } + ], + "nullable": true + } + }, + "required": [ + "id", + "imageHeight", + "imageWidth", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "person" + ], + "type": "object" + }, + "AssetFaceUpdateDto": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/AssetFaceUpdateItem" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "AssetFaceUpdateItem": { + "properties": { + "assetId": { + "format": "uuid", + "type": "string" + }, + "personId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "personId", + "assetId" + ], + "type": "object" + }, + "AssetFaceWithoutPersonResponseDto": { + "properties": { + "boundingBoxX1": { + "type": "integer" + }, + "boundingBoxX2": { + "type": "integer" + }, + "boundingBoxY1": { + "type": "integer" + }, + "boundingBoxY2": { + "type": "integer" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "imageHeight": { + "type": "integer" + }, + "imageWidth": { + "type": "integer" + } + }, + "required": [ + "id", + "imageHeight", + "imageWidth", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2" + ], + "type": "object" + }, "AssetFileUploadResponseDto": { "properties": { "duplicate": { @@ -6719,7 +7013,7 @@ }, "people": { "items": { - "$ref": "#/components/schemas/PersonResponseDto" + "$ref": "#/components/schemas/PersonWithFacesResponseDto" }, "type": "array" }, @@ -7452,6 +7746,18 @@ }, "type": "object" }, + "FaceDto": { + "properties": { + "id": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, "FileChecksumDto": { "properties": { "filenames": { @@ -8147,6 +8453,42 @@ }, "type": "object" }, + "PersonWithFacesResponseDto": { + "properties": { + "birthDate": { + "format": "date", + "nullable": true, + "type": "string" + }, + "faces": { + "items": { + "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "isHidden": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "thumbnailPath": { + "type": "string" + } + }, + "required": [ + "birthDate", + "faces", + "id", + "name", + "thumbnailPath", + "isHidden" + ], + "type": "object" + }, "QueueStatusDto": { "properties": { "isActive": { diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 862fafc32..f70d3d548 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -41,6 +41,8 @@ export enum Permission { PERSON_READ = 'person.read', PERSON_WRITE = 'person.write', PERSON_MERGE = 'person.merge', + PERSON_CREATE = 'person.create', + PERSON_REASSIGN = 'person.reassign', PARTNER_UPDATE = 'partner.update', } @@ -247,6 +249,12 @@ export class AccessCore { case Permission.PERSON_MERGE: return await this.repository.person.checkOwnerAccess(authUser.id, ids); + case Permission.PERSON_CREATE: + return this.repository.person.hasFaceOwnerAccess(authUser.id, ids); + + case Permission.PERSON_REASSIGN: + return this.repository.person.hasFaceOwnerAccess(authUser.id, ids); + case Permission.PARTNER_UPDATE: return await this.repository.partner.checkUpdateAccess(authUser.id, ids); } diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index 72c257256..c3a7491cf 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -1,6 +1,6 @@ -import { AssetEntity, AssetType } from '@app/infra/entities'; +import { AssetEntity, AssetFaceEntity, AssetType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { PersonResponseDto, mapFace } from '../../person/person.dto'; +import { PersonWithFacesResponseDto } from '../../person/person.dto'; import { TagResponseDto, mapTag } from '../../tag'; import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto'; import { ExifResponseDto, mapExif } from './exif-response.dto'; @@ -39,7 +39,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { exifInfo?: ExifResponseDto; smartInfo?: SmartInfoResponseDto; tags?: TagResponseDto[]; - people?: PersonResponseDto[]; + people?: PersonWithFacesResponseDto[]; /**base64 encoded sha1 hash */ checksum!: string; stackParentId?: string | null; @@ -53,6 +53,24 @@ export type AssetMapOptions = { withStack?: boolean; }; +const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => { + const result: PersonWithFacesResponseDto[] = []; + if (faces) { + faces.forEach((face) => { + if (face.person) { + const existingPersonEntry = result.find((item) => item.id === face.person!.id); + if (existingPersonEntry) { + existingPersonEntry.faces.push(face); + } else { + result.push({ ...face.person!, faces: [face] }); + } + } + }); + } + + return result; +}; + export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; @@ -96,16 +114,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, livePhotoVideoId: entity.livePhotoVideoId, tags: entity.tags?.map(mapTag), - people: entity.faces - ?.map(mapFace) - .filter((person): person is PersonResponseDto => person !== null) - .reduce((people, person) => { - const existingPerson = people.find((p) => p.id === person.id); - if (!existingPerson) { - people.push(person); - } - return people; - }, [] as PersonResponseDto[]), + people: peopleWithFaces(entity.faces), checksum: entity.checksum.toString('base64'), stackParentId: entity.stackParentId, stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined, diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 4082d5e90..d313302ea 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -201,7 +201,7 @@ export class JobService { const { id } = item.data; const person = await this.personRepository.getById(id); if (person) { - this.communicationRepository.send(CommunicationEvent.PERSON_THUMBNAIL, person.ownerId, id); + this.communicationRepository.send(CommunicationEvent.PERSON_THUMBNAIL, person.ownerId, person.id); } break; diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index b7acde73a..ed4693393 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -2,6 +2,7 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { AuthUserDto } from '../auth'; import { Optional, ValidateUUID, toBoolean } from '../domain.util'; export class PersonUpdateDto { @@ -73,6 +74,51 @@ export class PersonResponseDto { isHidden!: boolean; } +export class PersonWithFacesResponseDto extends PersonResponseDto { + faces!: AssetFaceWithoutPersonResponseDto[]; +} + +export class AssetFaceWithoutPersonResponseDto { + @ValidateUUID() + id!: string; + @ApiProperty({ type: 'integer' }) + imageHeight!: number; + @ApiProperty({ type: 'integer' }) + imageWidth!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxX1!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxX2!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxY1!: number; + @ApiProperty({ type: 'integer' }) + boundingBoxY2!: number; +} + +export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { + person!: PersonResponseDto | null; +} + +export class AssetFaceUpdateDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetFaceUpdateItem) + data!: AssetFaceUpdateItem[]; +} + +export class FaceDto { + @ValidateUUID() + id!: string; +} + +export class AssetFaceUpdateItem { + @ValidateUUID() + personId!: string; + + @ValidateUUID() + assetId!: string; +} + export class PersonStatisticsResponseDto { @ApiProperty({ type: 'integer' }) assets!: number; @@ -98,10 +144,15 @@ export function mapPerson(person: PersonEntity): PersonResponseDto { }; } -export function mapFace(face: AssetFaceEntity): PersonResponseDto | null { - if (face.person) { - return mapPerson(face.person); - } - - return null; +export function mapFaces(face: AssetFaceEntity, authUser: AuthUserDto): AssetFaceResponseDto { + return { + id: face.id, + imageHeight: face.imageHeight, + imageWidth: face.imageWidth, + boundingBoxX1: face.boundingBoxX1, + boundingBoxX2: face.boundingBoxX2, + boundingBoxY1: face.boundingBoxY1, + boundingBoxY2: face.boundingBoxY2, + person: face.person?.ownerId === authUser.id ? mapPerson(face.person) : null, + }; } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 44c20712b..0ce15f5ae 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -31,7 +31,7 @@ import { ISystemConfigRepository, WithoutProperty, } from '../repositories'; -import { PersonResponseDto } from './person.dto'; +import { PersonResponseDto, mapFaces } from './person.dto'; import { PersonService } from './person.service'; const responseDto: PersonResponseDto = { @@ -339,7 +339,7 @@ describe(PersonService.name, () => { ).resolves.toEqual(responseDto); expect(personMock.getById).toHaveBeenCalledWith('person-1'); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.assetId }); + expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id }); expect(personMock.getFacesByIds).toHaveBeenCalledWith([ { assetId: faceStub.face1.assetId, @@ -375,6 +375,139 @@ describe(PersonService.name, () => { }); }); + describe('reassignFaces', () => { + it('should throw an error if user has no access to the person', async () => { + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set()); + + await expect( + sut.reassignFaces(authStub.admin, personStub.noName.id, { + data: [{ personId: 'asset-face-1', assetId: '' }], + }), + ).rejects.toBeInstanceOf(BadRequestException); + expect(jobMock.queue).not.toHaveBeenCalledWith(); + }); + it('should reassign a face', async () => { + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.withName.id])); + personMock.getById.mockResolvedValue(personStub.noName); + accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); + personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); + personMock.reassignFace.mockResolvedValue(1); + personMock.getRandomFace.mockResolvedValue(faceStub.primaryFace1); + await expect( + sut.reassignFaces(authStub.admin, personStub.noName.id, { + data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }], + }), + ).resolves.toEqual([personStub.noName]); + + expect(jobMock.queue).toHaveBeenCalledWith({ + name: JobName.GENERATE_PERSON_THUMBNAIL, + data: { id: personStub.newThumbnail.id }, + }); + }); + }); + + describe('handlePersonMigration', () => { + it('should not move person files', async () => { + personMock.getById.mockResolvedValue(null); + await expect(sut.handlePersonMigration(personStub.noName)).resolves.toStrictEqual(false); + }); + }); + + describe('getFacesById', () => { + it('should get the bounding boxes for an asset', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId])); + personMock.getFaces.mockResolvedValue([faceStub.primaryFace1]); + await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([ + mapFaces(faceStub.primaryFace1, authStub.admin), + ]); + }); + it('should reject if the user has not access to the asset', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set()); + personMock.getFaces.mockResolvedValue([faceStub.primaryFace1]); + await expect(sut.getFacesById(authStub.admin, { id: faceStub.primaryFace1.assetId })).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + }); + + describe('createNewFeaturePhoto', () => { + it('should change person feature photo', async () => { + personMock.getRandomFace.mockResolvedValue(faceStub.primaryFace1); + await sut.createNewFeaturePhoto([personStub.newThumbnail.id]); + expect(jobMock.queue).toHaveBeenCalledWith({ + name: JobName.GENERATE_PERSON_THUMBNAIL, + data: { id: personStub.newThumbnail.id }, + }); + }); + }); + + describe('reassignFacesById', () => { + 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])); + personMock.getFaceById.mockResolvedValue(faceStub.face1); + personMock.reassignFace.mockResolvedValue(1); + personMock.getById.mockResolvedValue(personStub.noName); + personMock.getRandomFace.mockResolvedValue(null); + await expect( + sut.reassignFacesById(authStub.admin, personStub.noName.id, { + id: faceStub.face1.id, + }), + ).resolves.toEqual({ + birthDate: personStub.noName.birthDate, + isHidden: personStub.noName.isHidden, + id: personStub.noName.id, + name: personStub.noName.name, + thumbnailPath: personStub.noName.thumbnailPath, + }); + + expect(jobMock.queue).not.toHaveBeenCalledWith(); + }); + + it('should fail if user has not the correct permissions on the asset', async () => { + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); + accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set()); + personMock.getFaceById.mockResolvedValue(faceStub.face1); + personMock.reassignFace.mockResolvedValue(1); + personMock.getById.mockResolvedValue(personStub.noName); + personMock.getRandomFace.mockResolvedValue(null); + await expect( + sut.reassignFacesById(authStub.admin, personStub.noName.id, { + id: faceStub.face1.id, + }), + ).rejects.toBeInstanceOf(BadRequestException); + + expect(jobMock.queue).not.toHaveBeenCalledWith(); + }); + }); + + describe('createPerson', () => { + it('should create a new person', async () => { + personMock.create.mockResolvedValue(personStub.primaryPerson); + personMock.getFaceById.mockResolvedValue(faceStub.face1); + accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); + + await expect(sut.createPerson(authStub.admin)).resolves.toBe(personStub.primaryPerson); + }); + }); + + describe('handlePersonDelete', () => { + it('should stop if a person has not be found', async () => { + personMock.getById.mockResolvedValue(null); + + await expect(sut.handlePersonDelete({ id: 'person-1' })).resolves.toBe(false); + expect(personMock.update).not.toHaveBeenCalled(); + expect(storageMock.unlink).not.toHaveBeenCalled(); + }); + it('should delete a person', async () => { + personMock.getById.mockResolvedValue(personStub.primaryPerson); + + await expect(sut.handlePersonDelete({ id: 'person-1' })).resolves.toBe(true); + expect(personMock.delete).toHaveBeenCalledWith(personStub.primaryPerson); + expect(storageMock.unlink).toHaveBeenCalledWith(personStub.primaryPerson.thumbnailPath); + }); + }); + describe('handlePersonCleanup', () => { it('should delete people without faces', async () => { personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]); @@ -515,6 +648,7 @@ describe(PersonService.name, () => { searchMock.searchFaces.mockResolvedValue(faceSearch.oneRemoteMatch); personMock.create.mockResolvedValue(personStub.noName); assetMock.getByIds.mockResolvedValue([assetStub.image]); + personMock.createFace.mockResolvedValue(faceStub.primaryFace1); await sut.handleRecognizeFaces({ id: assetStub.image.id }); @@ -557,16 +691,16 @@ describe(PersonService.name, () => { expect(mediaMock.crop).not.toHaveBeenCalled(); }); - it('should skip an person with a face asset id not found', async () => { - personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId }); - personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); + it('should skip a person with a face asset id not found', async () => { + personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.id }); + personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1); await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); expect(mediaMock.crop).not.toHaveBeenCalled(); }); it('should skip a person with a face asset id without a thumbnail', async () => { personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId }); - personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); + personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1); assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); expect(mediaMock.crop).not.toHaveBeenCalled(); @@ -574,7 +708,7 @@ describe(PersonService.name, () => { it('should generate a thumbnail', async () => { personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId }); - personMock.getFacesByIds.mockResolvedValue([faceStub.middle]); + personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.middle); assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); @@ -601,7 +735,7 @@ describe(PersonService.name, () => { it('should generate a thumbnail without going negative', async () => { personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId }); - personMock.getFacesByIds.mockResolvedValue([faceStub.start]); + personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.start); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); @@ -622,7 +756,7 @@ describe(PersonService.name, () => { it('should generate a thumbnail without overflowing', async () => { personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId }); - personMock.getFacesByIds.mockResolvedValue([faceStub.end]); + personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.end); assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]); await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); @@ -646,15 +780,12 @@ describe(PersonService.name, () => { it('should require person.write and person.merge permission', async () => { personMock.getById.mockResolvedValueOnce(personStub.primaryPerson); personMock.getById.mockResolvedValueOnce(personStub.mergePerson); - personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, ); - expect(personMock.prepareReassignFaces).not.toHaveBeenCalled(); - expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); @@ -664,7 +795,6 @@ describe(PersonService.name, () => { it('should merge two people', async () => { personMock.getById.mockResolvedValueOnce(personStub.primaryPerson); personMock.getById.mockResolvedValueOnce(personStub.mergePerson); - personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); @@ -673,11 +803,6 @@ describe(PersonService.name, () => { { id: 'person-2', success: true }, ]); - expect(personMock.prepareReassignFaces).toHaveBeenCalledWith({ - newPersonId: personStub.primaryPerson.id, - oldPersonId: personStub.mergePerson.id, - }); - expect(personMock.reassignFaces).toHaveBeenCalledWith({ newPersonId: personStub.primaryPerson.id, oldPersonId: personStub.mergePerson.id, @@ -690,29 +815,6 @@ describe(PersonService.name, () => { expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); - it('should delete conflicting faces before merging', async () => { - personMock.getById.mockResolvedValue(personStub.primaryPerson); - personMock.getById.mockResolvedValue(personStub.mergePerson); - personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); - accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); - - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: true }, - ]); - - expect(personMock.prepareReassignFaces).toHaveBeenCalledWith({ - newPersonId: personStub.primaryPerson.id, - oldPersonId: personStub.mergePerson.id, - }); - - expect(jobMock.queue).toHaveBeenCalledWith({ - name: JobName.SEARCH_REMOVE_FACE, - data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id }, - }); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); - }); - it('should throw an error when the primary person is not found', async () => { personMock.getById.mockResolvedValue(null); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); @@ -735,7 +837,6 @@ describe(PersonService.name, () => { { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, ]); - expect(personMock.prepareReassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); @@ -744,7 +845,6 @@ describe(PersonService.name, () => { it('should handle an error reassigning faces', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getById.mockResolvedValue(personStub.mergePerson); - personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); personMock.reassignFaces.mockRejectedValue(new Error('update failed')); accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 3452807f6..79fdcbafe 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -28,6 +28,9 @@ import { import { StorageCore } from '../storage'; import { SystemConfigCore } from '../system-config'; import { + AssetFaceResponseDto, + AssetFaceUpdateDto, + FaceDto, MergePersonDto, PeopleResponseDto, PeopleUpdateDto, @@ -35,6 +38,7 @@ import { PersonSearchDto, PersonStatisticsResponseDto, PersonUpdateDto, + mapFaces, mapPerson, } from './person.dto'; @@ -80,6 +84,86 @@ export class PersonService { }; } + createPerson(authUser: AuthUserDto): Promise { + return this.repository.create({ ownerId: authUser.id }); + } + + async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise { + await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); + const person = await this.findOrFail(personId); + const result: PersonResponseDto[] = []; + const changeFeaturePhoto: string[] = []; + for (const data of dto.data) { + const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); + + for (const face of faces) { + await this.access.requirePermission(authUser, Permission.PERSON_CREATE, face.id); + if (person.faceAssetId === null) { + changeFeaturePhoto.push(person.id); + } + if (face.person && face.person.faceAssetId === face.id) { + changeFeaturePhoto.push(face.person.id); + } + + await this.repository.reassignFace(face.id, personId); + } + + result.push(person); + } + if (changeFeaturePhoto.length > 0) { + // Remove duplicates + await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto))); + } + return result; + } + + async reassignFacesById(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 getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise { + await this.access.requirePermission(authUser, Permission.ASSET_READ, dto.id); + const faces = await this.repository.getFaces(dto.id); + return faces.map((asset) => mapFaces(asset, authUser)); + } + + async createNewFeaturePhoto(changeFeaturePhoto: string[]) { + this.logger.debug( + `Changing feature photos for ${changeFeaturePhoto.length} ${changeFeaturePhoto.length > 1 ? 'people' : 'person'}`, + ); + for (const personId of changeFeaturePhoto) { + const assetFace = await this.repository.getRandomFace(personId); + + if (assetFace !== null) { + await this.repository.update({ + id: personId, + faceAssetId: assetFace.id, + }); + + await this.jobRepository.queue({ + name: JobName.GENERATE_PERSON_THUMBNAIL, + data: { + id: personId, + }, + }); + } + } + } + async getById(authUser: AuthUserDto, id: string): Promise { await this.access.requirePermission(authUser, Permission.PERSON_READ, id); return this.findOrFail(id).then(mapPerson); @@ -128,7 +212,7 @@ export class PersonService { throw new BadRequestException('Invalid assetId for feature face'); } - person = await this.repository.update({ id, faceAssetId: assetId }); + person = await this.repository.update({ id, faceAssetId: face.id }); await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }); } @@ -255,9 +339,9 @@ export class PersonService { personId = newPerson.id; } - const faceId: AssetFaceId = { assetId: asset.id, personId }; - await this.repository.createFace({ - ...faceId, + const face = await this.repository.createFace({ + assetId: asset.id, + personId, embedding, imageHeight: rest.imageHeight, imageWidth: rest.imageWidth, @@ -266,10 +350,11 @@ export class PersonService { boundingBoxY1: rest.boundingBox.y1, boundingBoxY2: rest.boundingBox.y2, }); + const faceId: AssetFaceId = { assetId: asset.id, personId }; await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACE, data: faceId }); if (newPerson) { - await this.repository.update({ id: personId, faceAssetId: asset.id }); + await this.repository.update({ id: personId, faceAssetId: face.id }); await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } }); } } @@ -304,14 +389,13 @@ export class PersonService { return false; } - const [face] = await this.repository.getFacesByIds([{ personId: person.id, assetId: person.faceAssetId }]); - if (!face) { + const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId); + if (face === null) { return false; } const { assetId, - personId, boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, @@ -324,8 +408,7 @@ export class PersonService { if (!asset?.resizePath) { return false; } - - this.logger.verbose(`Cropping face for person: ${personId}`); + this.logger.verbose(`Cropping face for person: ${person.id}`); const thumbnailPath = StorageCore.getPersonThumbnailPath(person); this.storageCore.ensureFolders(thumbnailPath); @@ -395,10 +478,6 @@ export class PersonService { const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id }; this.logger.log(`Merging ${mergeName} into ${primaryName}`); - const assetIds = await this.repository.prepareReassignFaces(mergeData); - for (const assetId of assetIds) { - await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId: mergeId } }); - } await this.repository.reassignFaces(mergeData); await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: mergePerson.id } }); diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index b6a71daf8..a501e4d6d 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -34,6 +34,7 @@ export interface IAccessRepository { }; person: { + hasFaceOwnerAccess(userId: string, assetFaceId: Set): Promise>; checkOwnerAccess(userId: string, personIds: Set): Promise>; }; diff --git a/server/src/domain/repositories/person.repository.ts b/server/src/domain/repositories/person.repository.ts index 2554a8a6f..4c6dcdc9c 100644 --- a/server/src/domain/repositories/person.repository.ts +++ b/server/src/domain/repositories/person.repository.ts @@ -34,7 +34,7 @@ export interface IPersonRepository { getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise; getAssets(personId: string): Promise; - prepareReassignFaces(data: UpdateFacesData): Promise; + reassignFaces(data: UpdateFacesData): Promise; create(entity: Partial): Promise; @@ -48,4 +48,8 @@ export interface IPersonRepository { getFacesByIds(ids: AssetFaceId[]): Promise; getRandomFace(personId: string): Promise; createFace(entity: Partial): Promise; + getFaces(assetId: string): Promise; + reassignFace(assetFaceId: string, newPersonId: string): Promise; + getFaceById(id: string): Promise; + getFaceByIdWithAssets(id: string): Promise; } diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index 7f9edfd42..cafaefb3f 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -19,6 +19,7 @@ import { AssetsController, AuditController, AuthController, + FaceController, JobController, LibraryController, OAuthController, @@ -50,6 +51,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; APIKeyController, AuditController, AuthController, + FaceController, JobController, LibraryController, OAuthController, diff --git a/server/src/immich/controllers/face.controller.ts b/server/src/immich/controllers/face.controller.ts new file mode 100644 index 000000000..5fd2dff27 --- /dev/null +++ b/server/src/immich/controllers/face.controller.ts @@ -0,0 +1,28 @@ +import { AssetFaceResponseDto, AuthUserDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain'; +import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthUser, Authenticated } from '../app.guard'; +import { UseValidation } from '../app.utils'; +import { UUIDParamDto } from './dto/uuid-param.dto'; + +@ApiTags('Face') +@Controller('face') +@Authenticated() +@UseValidation() +export class FaceController { + constructor(private service: PersonService) {} + + @Get() + getFaces(@AuthUser() authUser: AuthUserDto, @Query() dto: FaceDto): Promise { + return this.service.getFacesById(authUser, dto); + } + + @Put(':id') + reassignFacesById( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: FaceDto, + ): Promise { + return this.service.reassignFacesById(authUser, id, dto); + } +} diff --git a/server/src/immich/controllers/index.ts b/server/src/immich/controllers/index.ts index b54a63d86..c177144c3 100644 --- a/server/src/immich/controllers/index.ts +++ b/server/src/immich/controllers/index.ts @@ -5,6 +5,7 @@ export * from './app.controller'; export * from './asset.controller'; export * from './audit.controller'; export * from './auth.controller'; +export * from './face.controller'; export * from './job.controller'; export * from './library.controller'; export * from './oauth.controller'; diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index e581fddb1..51f222c7d 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -1,4 +1,5 @@ import { + AssetFaceUpdateDto, AssetResponseDto, AuthUserDto, BulkIdResponseDto, @@ -34,6 +35,20 @@ export class PersonController { return this.service.getAll(authUser, withHidden); } + @Post() + createPerson(@AuthUser() authUser: AuthUserDto): Promise { + return this.service.createPerson(authUser); + } + + @Put(':id/reassign') + reassignFaces( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetFaceUpdateDto, + ): Promise { + return this.service.reassignFaces(authUser, id, dto); + } + @Put() updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updatePeople(authUser, dto); diff --git a/server/src/infra/entities/person.entity.ts b/server/src/infra/entities/person.entity.ts index 2735204e6..3b4545e45 100644 --- a/server/src/infra/entities/person.entity.ts +++ b/server/src/infra/entities/person.entity.ts @@ -8,7 +8,6 @@ import { UpdateDateColumn, } from 'typeorm'; import { AssetFaceEntity } from './asset-face.entity'; -import { AssetEntity } from './asset.entity'; import { UserEntity } from './user.entity'; @Entity('person') @@ -40,8 +39,8 @@ export class PersonEntity { @Column({ nullable: true }) faceAssetId!: string | null; - @ManyToOne(() => AssetEntity, { onDelete: 'SET NULL', nullable: true }) - faceAsset!: AssetEntity | null; + @ManyToOne(() => AssetFaceEntity, { onDelete: 'SET NULL', nullable: true }) + faceAsset!: AssetFaceEntity | null; @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person) faces!: AssetFaceEntity[]; diff --git a/server/src/infra/migrations/1699727044012-EditFaceAssetForeignKey.ts b/server/src/infra/migrations/1699727044012-EditFaceAssetForeignKey.ts new file mode 100644 index 000000000..fdff7ea06 --- /dev/null +++ b/server/src/infra/migrations/1699727044012-EditFaceAssetForeignKey.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class EditFaceAssetForeignKey1699727044012 implements MigrationInterface { + name = 'EditFaceAssetForeignKey1699727044012' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`); + await queryRunner.query(`UPDATE person SET "faceAssetId" = asset_faces."id" FROM asset_faces WHERE person."faceAssetId" = asset_faces."assetId" AND person."id" = asset_faces."personId"`) + await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`); + await queryRunner.query(`UPDATE person SET "faceAssetId" = assets."id" FROM assets, asset_faces WHERE person."faceAssetId" = asset_faces."id" AND asset_faces."assetId" = assets."id"`); + await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } + +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index 208b7095c..5a3f9925b 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -5,6 +5,7 @@ import { ActivityEntity, AlbumEntity, AssetEntity, + AssetFaceEntity, LibraryEntity, PartnerEntity, PersonEntity, @@ -20,6 +21,7 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(LibraryEntity) private libraryRepository: Repository, @InjectRepository(PartnerEntity) private partnerRepository: Repository, @InjectRepository(PersonEntity) private personRepository: Repository, + @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository, @InjectRepository(UserTokenEntity) private tokenRepository: Repository, ) {} @@ -318,6 +320,22 @@ export class AccessRepository implements IAccessRepository { }) .then((persons) => new Set(persons.map((person) => person.id))); }, + hasFaceOwnerAccess: async (userId: string, assetFaceIds: Set): Promise> => { + if (assetFaceIds.size === 0) { + return new Set(); + } + return this.assetFaceRepository + .find({ + select: { id: true }, + where: { + id: In([...assetFaceIds]), + asset: { + ownerId: userId, + }, + }, + }) + .then((faces) => new Set(faces.map((face) => face.id))); + }, }; partner = { diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index e3c0ac26a..038b955a1 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -107,6 +107,48 @@ export class PersonRepository implements IPersonRepository { } @GenerateSql({ params: [DummyValue.UUID] }) + getFaces(assetId: string): Promise { + return this.assetFaceRepository.find({ + where: { assetId }, + relations: { + person: true, + }, + }); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + getFaceById(id: string): Promise { + return this.assetFaceRepository.findOneOrFail({ + where: { id }, + relations: { + person: true, + }, + }); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + getFaceByIdWithAssets(id: string): Promise { + return this.assetFaceRepository.findOne({ + where: { id }, + relations: { + person: true, + asset: true, + }, + }); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) + async reassignFace(assetFaceId: string, newPersonId: string): Promise { + const result = await this.assetFaceRepository + .createQueryBuilder() + .update() + .set({ personId: newPersonId }) + .where({ id: assetFaceId }) + .execute(); + + return result.affected ?? 0; + } + getById(personId: string): Promise { return this.personRepository.findOne({ where: { id: personId } }); } diff --git a/server/src/infra/sql/person.repository.sql b/server/src/infra/sql/person.repository.sql index 933283746..68d049ae0 100644 --- a/server/src/infra/sql/person.repository.sql +++ b/server/src/infra/sql/person.repository.sql @@ -133,24 +133,145 @@ GROUP BY HAVING COUNT("face"."assetId") = 0 --- PersonRepository.getById +-- PersonRepository.getFaces SELECT - "PersonEntity"."id" AS "PersonEntity_id", - "PersonEntity"."createdAt" AS "PersonEntity_createdAt", - "PersonEntity"."updatedAt" AS "PersonEntity_updatedAt", - "PersonEntity"."ownerId" AS "PersonEntity_ownerId", - "PersonEntity"."name" AS "PersonEntity_name", - "PersonEntity"."birthDate" AS "PersonEntity_birthDate", - "PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath", - "PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId", - "PersonEntity"."isHidden" AS "PersonEntity_isHidden" + "AssetFaceEntity"."id" AS "AssetFaceEntity_id", + "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId", + "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId", + "AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding", + "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth", + "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight", + "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", + "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", + "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", + "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", + "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", + "AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId", + "AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name", + "AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate", + "AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath", + "AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId", + "AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden" FROM - "person" "PersonEntity" + "asset_faces" "AssetFaceEntity" + LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId" WHERE - ("PersonEntity"."id" = $1) + ("AssetFaceEntity"."assetId" = $1) + +-- PersonRepository.getFaceById +SELECT DISTINCT + "distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id" +FROM + ( + SELECT + "AssetFaceEntity"."id" AS "AssetFaceEntity_id", + "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId", + "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId", + "AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding", + "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth", + "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight", + "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", + "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", + "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", + "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", + "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", + "AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId", + "AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name", + "AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate", + "AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath", + "AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId", + "AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden" + FROM + "asset_faces" "AssetFaceEntity" + LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId" + WHERE + ("AssetFaceEntity"."id" = $1) + ) "distinctAlias" +ORDER BY + "AssetFaceEntity_id" ASC LIMIT 1 +-- PersonRepository.getFaceByIdWithAssets +SELECT DISTINCT + "distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id" +FROM + ( + SELECT + "AssetFaceEntity"."id" AS "AssetFaceEntity_id", + "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId", + "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId", + "AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding", + "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth", + "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight", + "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", + "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", + "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", + "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", + "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", + "AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId", + "AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name", + "AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate", + "AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath", + "AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId", + "AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden", + "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id", + "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId", + "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId", + "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", + "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", + "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", + "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", + "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", + "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", + "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", + "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", + "AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime", + "AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite", + "AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived", + "AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal", + "AssetFaceEntity__AssetFaceEntity_asset"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly", + "AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline", + "AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum", + "AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration", + "AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible", + "AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId", + "AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName", + "AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath", + "AssetFaceEntity__AssetFaceEntity_asset"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId" + FROM + "asset_faces" "AssetFaceEntity" + LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId" + LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId" + AND ( + "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL + ) + WHERE + ("AssetFaceEntity"."id" = $1) + ) "distinctAlias" +ORDER BY + "AssetFaceEntity_id" ASC +LIMIT + 1 + +-- PersonRepository.reassignFace +UPDATE "asset_faces" +SET + "personId" = $1 +WHERE + "id" = $2 + -- PersonRepository.getByName SELECT "person"."id" AS "person_id", diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index d6e5b741d..76e2d14f7 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -1,5 +1,4 @@ import { PersonEntity } from '@app/infra/entities'; -import { assetStub } from '@test/fixtures/asset.stub'; import { userStub } from './user.stub'; export const personStub = { @@ -41,7 +40,7 @@ export const personStub = { birthDate: null, thumbnailPath: '/path/to/thumbnail.jpg', faces: [], - faceAssetId: null, + faceAssetId: 'assetFaceId', faceAsset: null, isHidden: false, }), @@ -97,8 +96,8 @@ export const personStub = { birthDate: null, thumbnailPath: '/new/path/to/thumbnail.jpg', faces: [], - faceAssetId: assetStub.image.id, - faceAsset: assetStub.image, + faceAssetId: 'asset-id', + faceAsset: null, isHidden: false, }), primaryPerson: Object.freeze({ diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 4c2a5ed8d..52fa21252 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -50,6 +50,7 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => }, person: { + hasFaceOwnerAccess: jest.fn(), checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 90a15221d..4b805fa65 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -20,8 +20,12 @@ export const newPersonRepositoryMock = (): jest.Mocked => { getAllFaces: jest.fn(), getFacesByIds: jest.fn(), getRandomFace: jest.fn(), - prepareReassignFaces: jest.fn(), + reassignFaces: jest.fn(), createFace: jest.fn(), + getFaces: jest.fn(), + reassignFace: jest.fn(), + getFaceById: jest.fn(), + getFaceByIdWithAssets: jest.fn(), }; }; diff --git a/web/src/api/api.ts b/web/src/api/api.ts index fc6f49f02..aecf28e12 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -21,6 +21,7 @@ import { UserApiFp, AuditApi, ActivityApi, + FaceApi, } from './open-api'; import { BASE_PATH } from './open-api/base'; import { DUMMY_BASE_URL, toPathString } from './open-api/common'; @@ -33,6 +34,7 @@ class ImmichApi { public assetApi: AssetApi; public auditApi: AuditApi; public authenticationApi: AuthenticationApi; + public faceApi: FaceApi; public jobApi: JobApi; public keyApi: APIKeyApi; public oauthApi: OAuthApi; @@ -60,6 +62,7 @@ class ImmichApi { this.libraryApi = new LibraryApi(this.config); this.assetApi = new AssetApi(this.config); this.authenticationApi = new AuthenticationApi(this.config); + this.faceApi = new FaceApi(this.config); this.jobApi = new JobApi(this.config); this.keyApi = new APIKeyApi(this.config); this.oauthApi = new OAuthApi(this.config); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 54cc92149..f8b8188ac 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -586,6 +586,142 @@ export const AssetBulkUploadCheckResultReasonEnum = { export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum]; +/** + * + * @export + * @interface AssetFaceResponseDto + */ +export interface AssetFaceResponseDto { + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxX1': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxX2': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxY1': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'boundingBoxY2': number; + /** + * + * @type {string} + * @memberof AssetFaceResponseDto + */ + 'id': string; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'imageHeight': number; + /** + * + * @type {number} + * @memberof AssetFaceResponseDto + */ + 'imageWidth': number; + /** + * + * @type {PersonResponseDto} + * @memberof AssetFaceResponseDto + */ + 'person': PersonResponseDto | null; +} +/** + * + * @export + * @interface AssetFaceUpdateDto + */ +export interface AssetFaceUpdateDto { + /** + * + * @type {Array} + * @memberof AssetFaceUpdateDto + */ + 'data': Array; +} +/** + * + * @export + * @interface AssetFaceUpdateItem + */ +export interface AssetFaceUpdateItem { + /** + * + * @type {string} + * @memberof AssetFaceUpdateItem + */ + 'assetId': string; + /** + * + * @type {string} + * @memberof AssetFaceUpdateItem + */ + 'personId': string; +} +/** + * + * @export + * @interface AssetFaceWithoutPersonResponseDto + */ +export interface AssetFaceWithoutPersonResponseDto { + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxX1': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxX2': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxY1': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'boundingBoxY2': number; + /** + * + * @type {string} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'id': string; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'imageHeight': number; + /** + * + * @type {number} + * @memberof AssetFaceWithoutPersonResponseDto + */ + 'imageWidth': number; +} /** * * @export @@ -842,10 +978,10 @@ export interface AssetResponseDto { 'ownerId': string; /** * - * @type {Array} + * @type {Array} * @memberof AssetResponseDto */ - 'people'?: Array; + 'people'?: Array; /** * * @type {boolean} @@ -1672,6 +1808,19 @@ export interface ExifResponseDto { */ 'timeZone'?: string | null; } +/** + * + * @export + * @interface FaceDto + */ +export interface FaceDto { + /** + * + * @type {string} + * @memberof FaceDto + */ + 'id': string; +} /** * * @export @@ -2564,6 +2713,49 @@ export interface PersonUpdateDto { */ 'name'?: string; } +/** + * + * @export + * @interface PersonWithFacesResponseDto + */ +export interface PersonWithFacesResponseDto { + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'birthDate': string | null; + /** + * + * @type {Array} + * @memberof PersonWithFacesResponseDto + */ + 'faces': Array; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'id': string; + /** + * + * @type {boolean} + * @memberof PersonWithFacesResponseDto + */ + 'isHidden': boolean; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'name': string; + /** + * + * @type {string} + * @memberof PersonWithFacesResponseDto + */ + 'thumbnailPath': string; +} /** * * @export @@ -11349,6 +11541,233 @@ export class AuthenticationApi extends BaseAPI { } +/** + * FaceApi - axios parameter creator + * @export + */ +export const FaceApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getFaces', 'id', id) + const localVarPath = `/face`; + // 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 (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {FaceDto} faceDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('reassignFacesById', 'id', id) + // verify required parameter 'faceDto' is not null or undefined + assertParamExists('reassignFacesById', 'faceDto', faceDto) + const localVarPath = `/face/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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: 'PUT', ...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); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * FaceApi - functional programming interface + * @export + */ +export const FaceApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration) + return { + /** + * + * @param {string} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} id + * @param {FaceDto} faceDto + * @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); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * FaceApi - factory interface + * @export + */ +export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = FaceApiFp(configuration) + return { + /** + * + * @param {FaceApiGetFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {FaceApiReassignFacesByIdRequest} 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)); + }, + }; +}; + +/** + * Request parameters for getFaces operation in FaceApi. + * @export + * @interface FaceApiGetFacesRequest + */ +export interface FaceApiGetFacesRequest { + /** + * + * @type {string} + * @memberof FaceApiGetFaces + */ + readonly id: string +} + +/** + * Request parameters for reassignFacesById operation in FaceApi. + * @export + * @interface FaceApiReassignFacesByIdRequest + */ +export interface FaceApiReassignFacesByIdRequest { + /** + * + * @type {string} + * @memberof FaceApiReassignFacesById + */ + readonly id: string + + /** + * + * @type {FaceDto} + * @memberof FaceApiReassignFacesById + */ + readonly faceDto: FaceDto +} + +/** + * FaceApi - object-oriented interface + * @export + * @class FaceApi + * @extends {BaseAPI} + */ +export class FaceApi extends BaseAPI { + /** + * + * @param {FaceApiGetFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FaceApi + */ + public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) { + return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {FaceApiReassignFacesByIdRequest} 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)); + } +} + + /** * JobApi - axios parameter creator * @export @@ -13180,6 +13599,44 @@ export class PartnerApi extends BaseAPI { */ export const PersonApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPerson: async (options: AxiosRequestConfig = {}): Promise => { + 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: 'POST', ...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) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {boolean} [withHidden] @@ -13439,6 +13896,54 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio options: localVarRequestOptions, }; }, + /** + * + * @param {string} id + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('reassignFaces', 'id', id) + // verify required parameter 'assetFaceUpdateDto' is not null or undefined + assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto) + const localVarPath = `/person/{id}/reassign` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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: 'PUT', ...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); + 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 {PeopleUpdateDto} peopleUpdateDto @@ -13541,6 +14046,15 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio export const PersonApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {boolean} [withHidden] @@ -13602,6 +14116,17 @@ export const PersonApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id + * @param {AssetFaceUpdateDto} assetFaceUpdateDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {PeopleUpdateDto} peopleUpdateDto @@ -13633,6 +14158,14 @@ export const PersonApiFp = function(configuration?: Configuration) { export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = PersonApiFp(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPerson(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.createPerson(options).then((request) => request(axios, basePath)); + }, /** * * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. @@ -13687,6 +14220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PersonApiReassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. @@ -13799,6 +14341,27 @@ export interface PersonApiMergePersonRequest { readonly mergePersonDto: MergePersonDto } +/** + * Request parameters for reassignFaces operation in PersonApi. + * @export + * @interface PersonApiReassignFacesRequest + */ +export interface PersonApiReassignFacesRequest { + /** + * + * @type {string} + * @memberof PersonApiReassignFaces + */ + readonly id: string + + /** + * + * @type {AssetFaceUpdateDto} + * @memberof PersonApiReassignFaces + */ + readonly assetFaceUpdateDto: AssetFaceUpdateDto +} + /** * Request parameters for updatePeople operation in PersonApi. * @export @@ -13841,6 +14404,16 @@ export interface PersonApiUpdatePersonRequest { * @extends {BaseAPI} */ export class PersonApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PersonApi + */ + public createPerson(options?: AxiosRequestConfig) { + return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. @@ -13907,6 +14480,17 @@ export class PersonApi extends BaseAPI { return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {PersonApiReassignFacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PersonApi + */ + public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) { + return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters. diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b93cc1379..f2ebd5c8d 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -560,7 +560,7 @@
{#if $slideshowState === SlideshowState.None} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 415aa2a3c..aa36f9195 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -15,16 +15,18 @@ mdiCalendar, mdiCameraIris, mdiClose, - mdiPencil, + mdiEye, + mdiEyeOff, mdiImageOutline, mdiMapMarkerOutline, mdiInformationOutline, - mdiEye, - mdiEyeOff, + mdiPencil, } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; + import PersonSidePanel from '../faces-page/person-side-panel.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import Map from '../shared-components/map/map.svelte'; + import { boundingBoxesArray } from '$lib/stores/people.store'; import { websocketStore } from '$lib/stores/websocket'; import { AppRoute } from '$lib/constants'; import ChangeLocation from '../shared-components/change-location.svelte'; @@ -35,8 +37,21 @@ export let albums: AlbumResponseDto[] = []; export let albumId: string | null = null; + let showAssetPath = false; let textarea: HTMLTextAreaElement; let description: string; + let showEditFaces = false; + let previousId: string; + + $: { + if (!previousId) { + previousId = asset.id; + } + if (asset.id !== previousId) { + showEditFaces = false; + previousId = asset.id; + } + } $: isOwner = $page?.data?.user?.id === asset.ownerId; @@ -84,6 +99,14 @@ return undefined; }; + const handleRefreshPeople = async () => { + await api.assetApi.getAssetById({ id: asset.id }).then((res) => { + people = res.data?.people || []; + textarea.value = res.data?.exifInfo?.description || ''; + }); + showEditFaces = false; + }; + const autoGrowHeight = (e: Event) => { const target = e.target as HTMLTextAreaElement; target.style.height = 'auto'; @@ -106,7 +129,6 @@ } }; - let showAssetPath = false; const toggleAssetPath = () => (showAssetPath = !showAssetPath); let isShowChangeDate = false; @@ -139,7 +161,7 @@ } -
+
@@ -589,3 +628,13 @@ {/each}
{/if} + +{#if showEditFaces} + { + showEditFaces = false; + }} + on:refresh={handleRefreshPeople} + /> +{/if} diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 9b3482332..68be9c12b 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -8,6 +8,9 @@ import { photoZoomState } from '$lib/stores/zoom-image.store'; import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; + import { photoViewer } from '$lib/stores/assets.store'; + import { getBoundingBox } from '$lib/utils/people-utils'; + import { boundingBoxesArray } from '$lib/stores/people.store'; export let asset: AssetResponseDto; export let element: HTMLDivElement | undefined = undefined; @@ -20,6 +23,13 @@ let copyImageToClipboard: (src: string) => Promise; let canCopyImagesToClipboard: () => boolean; + $: if (imgElement) { + createZoomImageWheel(imgElement, { + maxZoom: 10, + wheelZoomRatio: 0.2, + }); + } + onMount(async () => { // Import hack :( see https://github.com/vadimkorr/svelte-carousel/issues/27#issuecomment-851022295 // TODO: Move to regular import once the package correctly supports ESM. @@ -29,6 +39,7 @@ }); onDestroy(() => { + $boundingBoxesArray = []; abortController?.abort(); }); @@ -105,16 +116,10 @@ if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed) { hasZoomed = true; + loadAssetData({ loadOriginal: true }); } }); - - $: if (imgElement) { - createZoomImageWheel(imgElement, { - maxZoom: 10, - wheelZoomRatio: 0.2, - }); - } @@ -129,12 +134,19 @@ {:then}
{asset.id} + {#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewer) as boundingbox} +
+ {/each}
{/await}
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 5e57cb02e..1be7e8ad2 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -52,7 +52,7 @@ {#if hidden}
- +
{/if} diff --git a/web/src/lib/components/elements/icon.svelte b/web/src/lib/components/elements/icon.svelte index 94fe5ec93..b15043449 100644 --- a/web/src/lib/components/elements/icon.svelte +++ b/web/src/lib/components/elements/icon.svelte @@ -4,7 +4,7 @@ export let size: string | number = '1em'; export let color = 'currentColor'; export let path: string; - export let title = ''; + export let title: string | null = null; export let desc = ''; export let flipped = false; let className = ''; diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte new file mode 100644 index 000000000..9b391e938 --- /dev/null +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -0,0 +1,246 @@ + + +
+
+ {#if !searchFaces} +
+ +

Select face

+
+
+ + {#if !isShowLoadingNewPerson} + + {:else} +
+ +
+ {/if} +
+ {:else} + +
+ + {#if isShowLoadingSearch} +
+ +
+ {/if} +
+ + {/if} +
+
+

All people

+
+ {#if searchName == ''} + {#each allPeople as person (person.id)} + {#if person.id !== peopleWithFaces[editedPersonIndex].person?.id} +
+ +
+ {/if} + {/each} + {:else} + {#each searchedPeople as person (person.id)} + {#if person.id !== peopleWithFaces[editedPersonIndex].person?.id} +
+ +
+ {/if} + {/each} + {/if} +
+
+
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 a8a6a204c..d21f0de98 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -12,23 +12,18 @@ import { handleError } from '$lib/utils/handle-error'; import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; - import { mdiCallMerge, mdiClose, mdiMagnify, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; + import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; - import { cloneDeep } from 'lodash-es'; - import LoadingSpinner from '../shared-components/loading-spinner.svelte'; - import { searchNameLocal } from '$lib/utils/person'; + import PeopleList from './people-list.svelte'; import { page } from '$app/stores'; export let person: PersonResponseDto; let people: PersonResponseDto[] = []; - let peopleCopy: PersonResponseDto[] = []; let selectedPeople: PersonResponseDto[] = []; let screenHeight: number; let isShowConfirmation = false; - let name = ''; - let searchWord: string; - let isSearchingPeople = false; + let dispatch = createEventDispatcher(); $: hasSelection = selectedPeople.length > 0; @@ -39,44 +34,12 @@ onMount(async () => { const { data } = await api.personApi.getAllPeople({ withHidden: false }); people = data.people; - peopleCopy = cloneDeep(people); }); const onClose = () => { dispatch('go-back'); }; - const resetSearch = () => { - name = ''; - people = peopleCopy; - }; - - const searchPeople = async (force: boolean) => { - if (name === '') { - people = peopleCopy; - return; - } - if (!force) { - if (people.length < 20 && name.startsWith(searchWord)) { - people = searchNameLocal(name, peopleCopy, 10); - return; - } - } - - const timeout = setTimeout(() => (isSearchingPeople = true), 100); - try { - const { data } = await api.searchApi.searchPerson({ name }); - people = data; - searchWord = name; - } catch (error) { - handleError(error, "Can't search people"); - } finally { - clearTimeout(timeout); - } - - isSearchingPeople = false; - }; - const handleSwapPeople = () => { [person, selectedPeople[0]] = [selectedPeople[0], person]; $page.url.searchParams.set('action', 'merge'); @@ -113,7 +76,7 @@ }); dispatch('merge'); } catch (error) { - handleError(error, 'Cannot merge faces'); + handleError(error, 'Cannot merge people'); } finally { isShowConfirmation = false; } @@ -131,7 +94,7 @@ {#if hasSelection} Selected {selectedPeople.length} {:else} - Merge faces + Merge people {/if}
@@ -151,7 +114,7 @@
-

Choose matching faces to merge

+

Choose matching people to merge

{#each selectedPeople as person (person.id)} @@ -178,57 +141,25 @@
-
- - - searchPeople(false)} - /> - {#if name} - - {/if} - {#if isSearchingPeople} -
- -
- {/if} -
- -
-
- {#each unselectedPeople as person (person.id)} - onSelect(person)} circle border selectable /> - {/each} -
-
+ onSelect(detail)} + />
{#if isShowConfirmation} (isShowConfirmation = false)} > -

Are you sure you want merge these faces?
This action is irreversible.

-
+

Are you sure you want merge these people ?

{/if}
diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte index 4300708ae..cb5022d23 100644 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -36,7 +36,7 @@ >

- Merge faces - {title} + Merge People - {title}

dispatch('close')} /> @@ -108,7 +108,7 @@
-

Are these the same face?

+

Are these the same person?

They will be merged together

diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 92d339d02..a14630a46 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -14,12 +14,12 @@ export let person: PersonResponseDto; export let preload = false; - type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face'; + type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-people' | 'hide-person'; let dispatch = createEventDispatcher<{ 'change-name': void; 'set-birth-date': void; - 'merge-faces': void; - 'hide-face': void; + 'merge-people': void; + 'hide-person': void; }>(); let showVerticalDots = false; @@ -82,10 +82,10 @@ {#if showContextMenu} onMenuExit()}> - onMenuClick('hide-face')} text="Hide face" /> + onMenuClick('hide-person')} text="Hide Person" /> onMenuClick('change-name')} text="Change name" /> onMenuClick('set-birth-date')} text="Set date of birth" /> - onMenuClick('merge-faces')} text="Merge faces" /> + onMenuClick('merge-people')} text="Merge People" /> {/if} diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte new file mode 100644 index 000000000..adf59c40c --- /dev/null +++ b/web/src/lib/components/faces-page/people-list.svelte @@ -0,0 +1,106 @@ + + +
+ + + searchPeople(false)} + /> + {#if name} + + {/if} + {#if isSearchingPeople} +
+ +
+ {/if} +
+ +
+
+ {#each people as person (person.id)} + { + dispatch('select', person); + }} + circle + border + selectable + /> + {/each} +
+
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte new file mode 100644 index 000000000..ecd98fbfb --- /dev/null +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -0,0 +1,278 @@ + + +
+
+
+ +

Edit faces

+
+ {#if !isShowLoadingDone} + + {:else} + + {/if} +
+ +
+
+ {#if isShowLoadingPeople} +
+ +
+ {:else} + {#each peopleWithFaces as face, index} + {#if face.person} +
+
($boundingBoxesArray = [peopleWithFaces[index]])} + on:mouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])} + on:mouseleave={() => ($boundingBoxesArray = [])} + > +
+
+ {#if !selectedPersonToCreate[index]} +

+ {#if selectedPersonToReassign[index]?.id} + {selectedPersonToReassign[index]?.name} + {:else} + {face.person?.name} + {/if} +

+ {/if} + +
+ {#if selectedPersonToCreate[index] || selectedPersonToReassign[index]} + + {:else} + + {/if} +
+
+
+ {/if} + {/each} + {/if} +
+
+
+ +{#if showSeletecFaces} + (showSeletecFaces = false)} + on:createPerson={(event) => handleCreatePerson(event.detail)} + on:reassign={(event) => handleReassignFace(event.detail)} + /> +{/if} diff --git a/web/src/lib/components/faces-page/show-hide.svelte b/web/src/lib/components/faces-page/show-hide.svelte index 93fd26c76..8210c710b 100644 --- a/web/src/lib/components/faces-page/show-hide.svelte +++ b/web/src/lib/components/faces-page/show-hide.svelte @@ -22,12 +22,12 @@ >
dispatch('closeClick')} /> - +
dispatch('reset-visibility')} /> diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte new file mode 100644 index 000000000..8a7f49cdc --- /dev/null +++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte @@ -0,0 +1,190 @@ + + + + +
+ + + +
+ + +
+ + +
+
+ + +
+
+ {#if selectedPerson !== null} +
+

Choose matching faces to re assign

+ +
+ +
+
+ {/if} + handleSelectedPerson(detail)} + /> +
+
+
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 1134aa710..7b1ff8f7a 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -58,6 +58,8 @@ interface TrashAsset { value: string; } +export const photoViewer = writable(null); + type PendingChange = AddAsset | DeleteAsset | TrashAsset; export class AssetStore { diff --git a/web/src/lib/stores/people.store.ts b/web/src/lib/stores/people.store.ts new file mode 100644 index 000000000..4aeb044b1 --- /dev/null +++ b/web/src/lib/stores/people.store.ts @@ -0,0 +1,12 @@ +import { writable } from 'svelte/store'; + +export interface Faces { + imageHeight: number; + imageWidth: number; + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; +} + +export const boundingBoxesArray = writable([]); diff --git a/web/src/lib/utils/people-utils.ts b/web/src/lib/utils/people-utils.ts new file mode 100644 index 000000000..1d630c8c3 --- /dev/null +++ b/web/src/lib/utils/people-utils.ts @@ -0,0 +1,71 @@ +import type { Faces } from '$lib/stores/people.store'; +import type { ZoomImageWheelState } from '@zoom-image/core'; + +const getContainedSize = (img: HTMLImageElement): { width: number; height: number } => { + const ratio = img.naturalWidth / img.naturalHeight; + let width = img.height * ratio; + let height = img.height; + if (width > img.width) { + width = img.width; + height = img.width / ratio; + } + return { width, height }; +}; + +export interface boundingBox { + top: number; + left: number; + width: number; + height: number; +} + +export const getBoundingBox = ( + faces: Faces[], + zoom: ZoomImageWheelState, + photoViewer: HTMLImageElement | null, +): boundingBox[] => { + const boxes: boundingBox[] = []; + + if (photoViewer === null) { + return boxes; + } + const clientHeight = photoViewer.clientHeight; + const clientWidth = photoViewer.clientWidth; + + const { width, height } = getContainedSize(photoViewer); + + for (const face of faces) { + /* + * + * Create the coordinates of the box based on the displayed image. + * The coordinates must take into account margins due to the 'object-fit: contain;' css property of the photo-viewer. + * + */ + const coordinates = { + x1: + (width / face.imageWidth) * zoom.currentZoom * face.boundingBoxX1 + + ((clientWidth - width) / 2) * zoom.currentZoom + + zoom.currentPositionX, + x2: + (width / face.imageWidth) * zoom.currentZoom * face.boundingBoxX2 + + ((clientWidth - width) / 2) * zoom.currentZoom + + zoom.currentPositionX, + y1: + (height / face.imageHeight) * zoom.currentZoom * face.boundingBoxY1 + + ((clientHeight - height) / 2) * zoom.currentZoom + + zoom.currentPositionY, + y2: + (height / face.imageHeight) * zoom.currentZoom * face.boundingBoxY2 + + ((clientHeight - height) / 2) * zoom.currentZoom + + zoom.currentPositionY, + }; + + boxes.push({ + top: Math.round(coordinates.y1), + left: Math.round(coordinates.x1), + width: Math.round(coordinates.x2 - coordinates.x1), + height: Math.round(coordinates.y2 - coordinates.y1), + }); + } + return boxes; +}; diff --git a/web/src/lib/utils/person.ts b/web/src/lib/utils/person.ts index f769ccf37..e26d6c693 100644 --- a/web/src/lib/utils/person.ts +++ b/web/src/lib/utils/person.ts @@ -30,3 +30,7 @@ export const searchNameLocal = ( }) .slice(0, slice); }; + +export const getPersonNameWithHiddenValue = (name: string, isHidden: boolean) => { + return `${name ? name + (isHidden ? ' ' : '') : ''}${isHidden ? '(hidden)' : ''}`; +}; diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 6a2c70946..2738eeabb 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -79,7 +79,7 @@ // trigger reactivity people = people; - // Reset variables used on the "Show & hide faces" modal + // Reset variables used on the "Show & hide people" modal showLoadingSpinner = false; selectHidden = false; toggleVisibility = false; @@ -145,13 +145,13 @@ `Unable to change the visibility for ${changed.length} ${changed.length <= 1 ? 'person' : 'people'}`, ); } - // Reset variables used on the "Show & hide faces" modal + // Reset variables used on the "Show & hide people" modal showLoadingSpinner = false; selectHidden = false; toggleVisibility = false; }; - const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => { + const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => { const [personToMerge, personToBeMergedIn] = response; showMergeModal = false; @@ -167,7 +167,7 @@ people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); notificationController.show({ - message: 'Merge faces succesfully', + message: 'Merge people succesfully', type: NotificationType.Info, }); } catch (error) { @@ -213,7 +213,7 @@ edittingPerson = detail; }; - const handleHideFace = async (detail: PersonResponseDto) => { + const handleHidePerson = async (detail: PersonResponseDto) => { try { const { data: updatedPerson } = await api.personApi.updatePerson({ id: detail.id, @@ -244,7 +244,7 @@ } }; - const handleMergeFaces = (detail: PersonResponseDto) => { + const handleMergePeople = (detail: PersonResponseDto) => { goto(`${AppRoute.PEOPLE}/${detail.id}?action=merge&previousRoute=${AppRoute.PEOPLE}`); }; @@ -352,7 +352,7 @@ {potentialMergePeople} on:close={() => (showMergeModal = false)} on:reject={() => changeName()} - on:confirm={(event) => handleMergeSameFace(event.detail)} + on:confirm={(event) => handleMergeSamePerson(event.detail)} /> {/if} @@ -363,7 +363,7 @@ (selectHidden = !selectHidden)}>
-

Show & hide faces

+

Show & hide people

{/if} @@ -379,8 +379,8 @@ preload={idx < 20} on:change-name={() => handleChangeName(person)} on:set-birth-date={() => handleSetBirthDate(person)} - on:merge-faces={() => handleMergeFaces(person)} - on:hide-face={() => handleHideFace(person)} + on:merge-people={() => handleMergePeople(person)} + on:hide-person={() => handleHidePerson(person)} /> {/if} {/each} diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte index ca2ba9813..6eb3a0a3e 100644 --- a/web/src/routes/(user)/people/[personId]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/+page.svelte @@ -4,6 +4,7 @@ import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; + import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte'; import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; @@ -46,10 +47,11 @@ enum ViewMode { VIEW_ASSETS = 'view-assets', - SELECT_FACE = 'select-face', - MERGE_FACES = 'merge-faces', + SELECT_PERSON = 'select-person', + MERGE_PEOPLE = 'merge-people', SUGGEST_MERGE = 'suggest-merge', BIRTH_DATE = 'birth-date', + UNASSIGN_ASSETS = 'unassign-faces', } let assetStore = new AssetStore({ @@ -124,7 +126,7 @@ previousRoute = getPreviousRoute; } if (action == 'merge') { - viewMode = ViewMode.MERGE_FACES; + viewMode = ViewMode.MERGE_PEOPLE; } }); const handleEscape = () => { @@ -155,7 +157,17 @@ } }); - const toggleHideFace = async () => { + const handleUnmerge = () => { + $assetStore.removeAssets(Array.from($selectedAssets).map((a) => a.id)); + assetInteractionStore.clearMultiselect(); + viewMode = ViewMode.VIEW_ASSETS; + }; + + const handleReassignAssets = () => { + viewMode = ViewMode.UNASSIGN_ASSETS; + }; + + const toggleHidePerson = async () => { try { await api.personApi.updatePerson({ id: data.person.id, @@ -179,7 +191,7 @@ }; const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => { - if (viewMode !== ViewMode.SELECT_FACE) { + if (viewMode !== ViewMode.SELECT_PERSON) { return; } @@ -202,7 +214,7 @@ } }; - const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => { + const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => { const [personToMerge, personToBeMergedIn] = response; viewMode = ViewMode.VIEW_ASSETS; isEditingName = false; @@ -212,7 +224,7 @@ mergePersonDto: { ids: [personToMerge.id] }, }); notificationController.show({ - message: 'Merge faces succesfully', + message: 'Merge people succesfully', type: NotificationType.Info, }); people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); @@ -333,6 +345,15 @@ }; +{#if viewMode === ViewMode.UNASSIGN_ASSETS} + a.id)} + personAssets={data.person} + on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} + on:confirm={handleUnmerge} + /> +{/if} + {#if viewMode === ViewMode.SUGGEST_MERGE} (viewMode = ViewMode.VIEW_ASSETS)} on:reject={() => changeName()} - on:confirm={(event) => handleMergeSameFace(event.detail)} + on:confirm={(event) => handleMergeSamePerson(event.detail)} /> {/if} @@ -352,7 +373,7 @@ /> {/if} -{#if viewMode === ViewMode.MERGE_FACES} +{#if viewMode === ViewMode.MERGE_PEOPLE} {/if} @@ -370,6 +391,7 @@ $assetStore.removeAssets(ids)} /> + @@ -379,16 +401,19 @@ goto(previousRoute)}> - (viewMode = ViewMode.SELECT_FACE)} /> + (viewMode = ViewMode.SELECT_PERSON)} /> (viewMode = ViewMode.BIRTH_DATE)} /> - (viewMode = ViewMode.MERGE_FACES)} /> - toggleHideFace()} /> + (viewMode = ViewMode.MERGE_PEOPLE)} /> + toggleHidePerson()} + /> {/if} - {#if viewMode === ViewMode.SELECT_FACE} + {#if viewMode === ViewMode.SELECT_PERSON} (viewMode = ViewMode.VIEW_ASSETS)}> Select feature photo @@ -401,13 +426,13 @@ handleSelectFeaturePhoto(asset)} on:escape={handleEscape} > {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} - +