2023-09-27 22:46:46 +02:00
|
|
|
import { PersonEntity } from '@app/infra/entities';
|
2023-10-11 04:14:44 +02:00
|
|
|
import { PersonPathType } from '@app/infra/entities/move.entity';
|
2023-05-17 19:07:17 +02:00
|
|
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
2023-10-09 16:25:03 +02:00
|
|
|
import { AccessCore, Permission } from '../access';
|
|
|
|
import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset';
|
2023-05-17 19:07:17 +02:00
|
|
|
import { AuthUserDto } from '../auth';
|
2023-07-10 19:56:45 +02:00
|
|
|
import { mimeTypes } from '../domain.constant';
|
2023-09-27 22:46:46 +02:00
|
|
|
import { usePagination } from '../domain.util';
|
2023-10-09 16:25:03 +02:00
|
|
|
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
|
|
|
import { FACE_THUMBNAIL_SIZE } from '../media';
|
|
|
|
import {
|
|
|
|
AssetFaceId,
|
|
|
|
CropOptions,
|
|
|
|
IAccessRepository,
|
|
|
|
IAssetRepository,
|
|
|
|
IJobRepository,
|
|
|
|
IMachineLearningRepository,
|
|
|
|
IMediaRepository,
|
2023-10-11 04:14:44 +02:00
|
|
|
IMoveRepository,
|
2023-10-09 16:25:03 +02:00
|
|
|
IPersonRepository,
|
|
|
|
ISearchRepository,
|
|
|
|
IStorageRepository,
|
|
|
|
ISystemConfigRepository,
|
|
|
|
ImmichReadStream,
|
|
|
|
UpdateFacesData,
|
|
|
|
WithoutProperty,
|
|
|
|
} from '../repositories';
|
2023-10-11 04:14:44 +02:00
|
|
|
import { StorageCore } from '../storage';
|
2023-10-09 16:25:03 +02:00
|
|
|
import { SystemConfigCore } from '../system-config';
|
2023-07-18 20:09:43 +02:00
|
|
|
import {
|
|
|
|
MergePersonDto,
|
|
|
|
PeopleResponseDto,
|
2023-07-23 05:00:43 +02:00
|
|
|
PeopleUpdateDto,
|
2023-07-18 20:09:43 +02:00
|
|
|
PersonResponseDto,
|
|
|
|
PersonSearchDto,
|
|
|
|
PersonUpdateDto,
|
2023-09-04 21:45:59 +02:00
|
|
|
mapPerson,
|
2023-07-18 20:09:43 +02:00
|
|
|
} from './person.dto';
|
2023-05-17 19:07:17 +02:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class PersonService {
|
2023-09-18 23:22:44 +02:00
|
|
|
private access: AccessCore;
|
2023-09-18 06:05:35 +02:00
|
|
|
private configCore: SystemConfigCore;
|
2023-09-27 22:46:46 +02:00
|
|
|
private storageCore: StorageCore;
|
2023-05-17 19:07:17 +02:00
|
|
|
readonly logger = new Logger(PersonService.name);
|
|
|
|
|
|
|
|
constructor(
|
2023-09-27 22:46:46 +02:00
|
|
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
|
|
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
|
|
|
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
|
2023-10-11 04:14:44 +02:00
|
|
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
2023-09-27 22:46:46 +02:00
|
|
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
2023-05-17 19:07:17 +02:00
|
|
|
@Inject(IPersonRepository) private repository: IPersonRepository,
|
2023-09-27 22:46:46 +02:00
|
|
|
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
2023-09-18 06:05:35 +02:00
|
|
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
2023-05-17 19:07:17 +02:00
|
|
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
|
|
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
2023-09-18 06:05:35 +02:00
|
|
|
) {
|
2023-09-18 23:22:44 +02:00
|
|
|
this.access = new AccessCore(accessRepository);
|
2023-10-09 02:51:03 +02:00
|
|
|
this.configCore = SystemConfigCore.create(configRepository);
|
2023-10-11 04:14:44 +02:00
|
|
|
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, repository);
|
2023-09-18 06:05:35 +02:00
|
|
|
}
|
2023-05-17 19:07:17 +02:00
|
|
|
|
2023-07-18 20:09:43 +02:00
|
|
|
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
2023-09-18 06:05:35 +02:00
|
|
|
const { machineLearning } = await this.configCore.getConfig();
|
2023-09-08 08:49:43 +02:00
|
|
|
const people = await this.repository.getAllForUser(authUser.id, {
|
2023-09-18 06:05:35 +02:00
|
|
|
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
2023-08-16 02:06:49 +02:00
|
|
|
withHidden: dto.withHidden || false,
|
|
|
|
});
|
2023-08-14 18:09:26 +02:00
|
|
|
const persons: PersonResponseDto[] = people
|
2023-07-18 20:09:43 +02:00
|
|
|
// with thumbnails
|
|
|
|
.filter((person) => !!person.thumbnailPath)
|
|
|
|
.map((person) => mapPerson(person));
|
|
|
|
|
|
|
|
return {
|
|
|
|
people: persons.filter((person) => dto.withHidden || !person.isHidden),
|
|
|
|
total: persons.length,
|
|
|
|
visible: persons.filter((person: PersonResponseDto) => !person.isHidden).length,
|
|
|
|
};
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|
|
|
|
|
2023-09-18 23:22:44 +02:00
|
|
|
async getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> {
|
|
|
|
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
|
|
|
return this.findOrFail(id).then(mapPerson);
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
async getThumbnail(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
|
2023-09-18 23:22:44 +02:00
|
|
|
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
|
|
|
const person = await this.repository.getById(id);
|
2023-05-17 19:07:17 +02:00
|
|
|
if (!person || !person.thumbnailPath) {
|
|
|
|
throw new NotFoundException();
|
|
|
|
}
|
|
|
|
|
2023-07-10 19:56:45 +02:00
|
|
|
return this.storageRepository.createReadStream(person.thumbnailPath, mimeTypes.lookup(person.thumbnailPath));
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
|
2023-09-18 23:22:44 +02:00
|
|
|
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
|
|
|
const assets = await this.repository.getAssets(id);
|
2023-05-17 19:07:17 +02:00
|
|
|
return assets.map(mapAsset);
|
|
|
|
}
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
2023-09-18 23:22:44 +02:00
|
|
|
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
|
|
|
|
let person = await this.findOrFail(id);
|
2023-05-17 19:07:17 +02:00
|
|
|
|
2023-09-26 09:03:22 +02:00
|
|
|
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
|
|
|
|
|
|
|
if (name !== undefined || birthDate !== undefined || isHidden !== undefined) {
|
|
|
|
person = await this.repository.update({ id, name, birthDate, isHidden });
|
2023-08-18 22:10:29 +02:00
|
|
|
if (this.needsSearchIndexUpdate(dto)) {
|
2023-09-18 23:22:44 +02:00
|
|
|
const assets = await this.repository.getAssets(id);
|
2023-08-18 22:10:29 +02:00
|
|
|
const ids = assets.map((asset) => asset.id);
|
|
|
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
|
|
|
}
|
2023-07-03 00:46:20 +02:00
|
|
|
}
|
|
|
|
|
2023-09-26 09:03:22 +02:00
|
|
|
if (assetId) {
|
|
|
|
await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
|
2023-09-27 22:46:46 +02:00
|
|
|
const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
|
2023-07-11 23:52:41 +02:00
|
|
|
if (!face) {
|
|
|
|
throw new BadRequestException('Invalid assetId for feature face');
|
|
|
|
}
|
2023-07-03 00:46:20 +02:00
|
|
|
|
2023-09-26 09:03:22 +02:00
|
|
|
person = await this.repository.update({ id, faceAssetId: assetId });
|
|
|
|
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
2023-07-03 00:46:20 +02:00
|
|
|
}
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
return mapPerson(person);
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|
|
|
|
|
2023-07-23 05:00:43 +02:00
|
|
|
async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
|
|
|
const results: BulkIdResponseDto[] = [];
|
|
|
|
for (const person of dto.people) {
|
|
|
|
try {
|
|
|
|
await this.update(authUser, person.id, {
|
|
|
|
isHidden: person.isHidden,
|
|
|
|
name: person.name,
|
2023-08-18 22:10:29 +02:00
|
|
|
birthDate: person.birthDate,
|
2023-07-23 05:00:43 +02:00
|
|
|
featureFaceAssetId: person.featureFaceAssetId,
|
|
|
|
}),
|
|
|
|
results.push({ id: person.id, success: true });
|
|
|
|
} catch (error: Error | any) {
|
|
|
|
this.logger.error(`Unable to update ${person.id} : ${error}`, error?.stack);
|
|
|
|
results.push({ id: person.id, success: false, error: BulkIdErrorReason.UNKNOWN });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2023-10-03 03:15:11 +02:00
|
|
|
async handlePersonDelete({ id }: IEntityJob) {
|
|
|
|
const person = await this.repository.getById(id);
|
|
|
|
if (!person) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.repository.delete(person);
|
|
|
|
await this.storageRepository.unlink(person.thumbnailPath);
|
|
|
|
} catch (error: Error | any) {
|
|
|
|
this.logger.error(`Unable to delete person: ${error}`, error?.stack);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-05-26 21:43:24 +02:00
|
|
|
async handlePersonCleanup() {
|
2023-05-17 19:07:17 +02:00
|
|
|
const people = await this.repository.getAllWithoutFaces();
|
|
|
|
for (const person of people) {
|
|
|
|
this.logger.debug(`Person ${person.name || person.id} no longer has any faces, deleting.`);
|
2023-10-03 03:15:11 +02:00
|
|
|
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: person.id } });
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|
2023-05-26 21:43:24 +02:00
|
|
|
|
|
|
|
return true;
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|
2023-07-11 23:52:41 +02:00
|
|
|
|
2023-09-27 22:46:46 +02:00
|
|
|
async handleQueueRecognizeFaces({ force }: IBaseJob) {
|
|
|
|
const { machineLearning } = await this.configCore.getConfig();
|
|
|
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
|
|
|
return force
|
|
|
|
? this.assetRepository.getAll(pagination, { order: 'DESC' })
|
|
|
|
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (force) {
|
2023-10-03 03:15:11 +02:00
|
|
|
const people = await this.repository.getAll();
|
|
|
|
for (const person of people) {
|
|
|
|
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: person.id } });
|
|
|
|
}
|
2023-09-27 22:46:46 +02:00
|
|
|
const faces = await this.searchRepository.deleteAllFaces();
|
|
|
|
this.logger.debug(`Deleted ${people} people and ${faces} faces`);
|
|
|
|
}
|
|
|
|
|
|
|
|
for await (const assets of assetPagination) {
|
|
|
|
for (const asset of assets) {
|
|
|
|
await this.jobRepository.queue({ name: JobName.RECOGNIZE_FACES, data: { id: asset.id } });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleRecognizeFaces({ id }: IEntityJob) {
|
|
|
|
const { machineLearning } = await this.configCore.getConfig();
|
|
|
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const [asset] = await this.assetRepository.getByIds([id]);
|
|
|
|
if (!asset || !asset.resizePath) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const faces = await this.machineLearningRepository.detectFaces(
|
|
|
|
machineLearning.url,
|
|
|
|
{ imagePath: asset.resizePath },
|
|
|
|
machineLearning.facialRecognition,
|
|
|
|
);
|
|
|
|
|
|
|
|
this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`);
|
|
|
|
this.logger.verbose(faces.map((face) => ({ ...face, embedding: `float[${face.embedding.length}]` })));
|
|
|
|
|
|
|
|
for (const { embedding, ...rest } of faces) {
|
|
|
|
const faceSearchResult = await this.searchRepository.searchFaces(embedding, { ownerId: asset.ownerId });
|
|
|
|
|
|
|
|
let personId: string | null = null;
|
|
|
|
|
|
|
|
// try to find a matching face and link to the associated person
|
|
|
|
// The closer to 0, the better the match. Range is from 0 to 2
|
|
|
|
if (faceSearchResult.total && faceSearchResult.distances[0] <= machineLearning.facialRecognition.maxDistance) {
|
|
|
|
this.logger.verbose(`Match face with distance ${faceSearchResult.distances[0]}`);
|
|
|
|
personId = faceSearchResult.items[0].personId;
|
|
|
|
}
|
|
|
|
|
|
|
|
let newPerson: PersonEntity | null = null;
|
|
|
|
if (!personId) {
|
|
|
|
this.logger.debug('No matches, creating a new person.');
|
|
|
|
newPerson = await this.repository.create({ ownerId: asset.ownerId });
|
|
|
|
personId = newPerson.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
const faceId: AssetFaceId = { assetId: asset.id, personId };
|
|
|
|
await this.repository.createFace({
|
|
|
|
...faceId,
|
|
|
|
embedding,
|
|
|
|
imageHeight: rest.imageHeight,
|
|
|
|
imageWidth: rest.imageWidth,
|
|
|
|
boundingBoxX1: rest.boundingBox.x1,
|
|
|
|
boundingBoxX2: rest.boundingBox.x2,
|
|
|
|
boundingBoxY1: rest.boundingBox.y1,
|
|
|
|
boundingBoxY2: rest.boundingBox.y2,
|
|
|
|
});
|
|
|
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACE, data: faceId });
|
|
|
|
|
|
|
|
if (newPerson) {
|
|
|
|
await this.repository.update({ id: personId, faceAssetId: asset.id });
|
|
|
|
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async handlePersonMigration({ id }: IEntityJob) {
|
|
|
|
const person = await this.repository.getById(id);
|
|
|
|
if (!person) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-10-11 04:14:44 +02:00
|
|
|
await this.storageCore.movePersonFile(person, PersonPathType.FACE);
|
2023-09-27 22:46:46 +02:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleGeneratePersonThumbnail(data: IEntityJob) {
|
|
|
|
const { machineLearning, thumbnail } = await this.configCore.getConfig();
|
|
|
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const person = await this.repository.getById(data.id);
|
|
|
|
if (!person?.faceAssetId) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const [face] = await this.repository.getFacesByIds([{ personId: person.id, assetId: person.faceAssetId }]);
|
|
|
|
if (!face) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
|
|
assetId,
|
|
|
|
personId,
|
|
|
|
boundingBoxX1: x1,
|
|
|
|
boundingBoxX2: x2,
|
|
|
|
boundingBoxY1: y1,
|
|
|
|
boundingBoxY2: y2,
|
|
|
|
imageWidth,
|
|
|
|
imageHeight,
|
|
|
|
} = face;
|
|
|
|
|
|
|
|
const [asset] = await this.assetRepository.getByIds([assetId]);
|
|
|
|
if (!asset?.resizePath) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.verbose(`Cropping face for person: ${personId}`);
|
2023-10-11 04:14:44 +02:00
|
|
|
const thumbnailPath = this.storageCore.getPersonThumbnailPath(person);
|
|
|
|
this.storageCore.ensureFolders(thumbnailPath);
|
2023-09-27 22:46:46 +02:00
|
|
|
|
|
|
|
const halfWidth = (x2 - x1) / 2;
|
|
|
|
const halfHeight = (y2 - y1) / 2;
|
|
|
|
|
|
|
|
const middleX = Math.round(x1 + halfWidth);
|
|
|
|
const middleY = Math.round(y1 + halfHeight);
|
|
|
|
|
|
|
|
// zoom out 10%
|
|
|
|
const targetHalfSize = Math.floor(Math.max(halfWidth, halfHeight) * 1.1);
|
|
|
|
|
|
|
|
// get the longest distance from the center of the image without overflowing
|
|
|
|
const newHalfSize = Math.min(
|
|
|
|
middleX - Math.max(0, middleX - targetHalfSize),
|
|
|
|
middleY - Math.max(0, middleY - targetHalfSize),
|
|
|
|
Math.min(imageWidth - 1, middleX + targetHalfSize) - middleX,
|
|
|
|
Math.min(imageHeight - 1, middleY + targetHalfSize) - middleY,
|
|
|
|
);
|
|
|
|
|
|
|
|
const cropOptions: CropOptions = {
|
|
|
|
left: middleX - newHalfSize,
|
|
|
|
top: middleY - newHalfSize,
|
|
|
|
width: newHalfSize * 2,
|
|
|
|
height: newHalfSize * 2,
|
|
|
|
};
|
|
|
|
|
|
|
|
const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions);
|
|
|
|
const thumbnailOptions = {
|
|
|
|
format: 'jpeg',
|
|
|
|
size: FACE_THUMBNAIL_SIZE,
|
|
|
|
colorspace: thumbnail.colorspace,
|
|
|
|
quality: thumbnail.quality,
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
|
|
|
|
await this.repository.update({ id: personId, thumbnailPath });
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
async mergePerson(authUser: AuthUserDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
|
|
|
const mergeIds = dto.ids;
|
2023-09-18 23:22:44 +02:00
|
|
|
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
|
|
|
|
const primaryPerson = await this.findOrFail(id);
|
2023-07-11 23:52:41 +02:00
|
|
|
const primaryName = primaryPerson.name || primaryPerson.id;
|
|
|
|
|
|
|
|
const results: BulkIdResponseDto[] = [];
|
|
|
|
|
|
|
|
for (const mergeId of mergeIds) {
|
2023-09-18 23:22:44 +02:00
|
|
|
const hasPermission = await this.access.hasPermission(authUser, Permission.PERSON_MERGE, mergeId);
|
|
|
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
try {
|
2023-09-18 23:22:44 +02:00
|
|
|
const mergePerson = await this.repository.getById(mergeId);
|
2023-07-11 23:52:41 +02:00
|
|
|
if (!mergePerson) {
|
|
|
|
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const mergeName = mergePerson.name || mergePerson.id;
|
|
|
|
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);
|
2023-10-03 03:15:11 +02:00
|
|
|
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: mergePerson.id } });
|
2023-07-11 23:52:41 +02:00
|
|
|
|
|
|
|
this.logger.log(`Merged ${mergeName} into ${primaryName}`);
|
|
|
|
results.push({ id: mergeId, success: true });
|
|
|
|
} catch (error: Error | any) {
|
|
|
|
this.logger.error(`Unable to merge ${mergeId} into ${id}: ${error}`, error?.stack);
|
|
|
|
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.UNKNOWN });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-22 21:42:12 +02:00
|
|
|
// Re-index all faces in typesense for up-to-date search results
|
|
|
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES });
|
|
|
|
|
2023-07-11 23:52:41 +02:00
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:10:29 +02:00
|
|
|
/**
|
|
|
|
* Returns true if the given person update is going to require an update of the search index.
|
|
|
|
* @param dto the Person going to be updated
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
private needsSearchIndexUpdate(dto: PersonUpdateDto): boolean {
|
|
|
|
return dto.name !== undefined || dto.isHidden !== undefined;
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:22:44 +02:00
|
|
|
private async findOrFail(id: string) {
|
|
|
|
const person = await this.repository.getById(id);
|
2023-07-11 23:52:41 +02:00
|
|
|
if (!person) {
|
|
|
|
throw new BadRequestException('Person not found');
|
|
|
|
}
|
|
|
|
return person;
|
|
|
|
}
|
2023-05-17 19:07:17 +02:00
|
|
|
}
|