mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-01-19 23:56:57 +00:00
feat(web/server): Face thumbnail selection (#3081)
* add migration * verify running migration populate new value * implemented service * generate api * FE works * FR Works * fix test * fix test fixture * fix test * fix test * consolidate api * fix test * added test * pr feedback * refactor * click ont humbnail to show feature selection as well
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class PersonUpdateDto {
|
||||
@IsNotEmpty()
|
||||
/**
|
||||
* Person name.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name!: string;
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Asset is used to get the feature face thumbnail.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
featureFaceAssetId?: string;
|
||||
}
|
||||
|
||||
export class PersonResponseDto {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AssetEntity, PersonEntity } from '@app/infra/entities';
|
||||
|
||||
import { AssetFaceId } from '@app/domain';
|
||||
import { AssetEntity, AssetFaceEntity, PersonEntity } from '@app/infra/entities';
|
||||
export const IPersonRepository = 'IPersonRepository';
|
||||
|
||||
export interface PersonSearchOptions {
|
||||
@@ -16,4 +16,6 @@ export interface IPersonRepository {
|
||||
update(entity: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
delete(entity: PersonEntity): Promise<PersonEntity | null>;
|
||||
deleteAll(): Promise<number>;
|
||||
|
||||
getFaceById(payload: AssetFaceId): Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
assetEntityStub,
|
||||
authStub,
|
||||
faceStub,
|
||||
newJobRepositoryMock,
|
||||
newPersonRepositoryMock,
|
||||
newStorageRepositoryMock,
|
||||
@@ -108,6 +109,36 @@ describe(PersonService.name, () => {
|
||||
data: { ids: [assetEntityStub.image.id] },
|
||||
});
|
||||
});
|
||||
|
||||
it("should update a person's thumbnailPath", async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.withName);
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
|
||||
).resolves.toEqual(responseDto);
|
||||
|
||||
expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1');
|
||||
expect(personMock.getFaceById).toHaveBeenCalledWith({
|
||||
assetId: faceStub.face1.assetId,
|
||||
personId: 'person-1',
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.GENERATE_FACE_THUMBNAIL,
|
||||
data: {
|
||||
assetId: faceStub.face1.assetId,
|
||||
personId: 'person-1',
|
||||
boundingBox: {
|
||||
x1: faceStub.face1.boundingBoxX1,
|
||||
x2: faceStub.face1.boundingBoxX2,
|
||||
y1: faceStub.face1.boundingBoxY1,
|
||||
y2: faceStub.face1.boundingBoxY2,
|
||||
},
|
||||
imageHeight: faceStub.face1.imageHeight,
|
||||
imageWidth: faceStub.face1.imageWidth,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonCleanup', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PersonEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { AssetResponseDto, mapAsset } from '../asset';
|
||||
import { AuthUserDto } from '../auth';
|
||||
@@ -52,18 +53,54 @@ export class PersonService {
|
||||
}
|
||||
|
||||
async update(authUser: AuthUserDto, personId: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
const exists = await this.repository.getById(authUser.id, personId);
|
||||
if (!exists) {
|
||||
let person = await this.repository.getById(authUser.id, personId);
|
||||
if (!person) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
const person = await this.repository.update({ id: personId, name: dto.name });
|
||||
if (dto.name) {
|
||||
person = await this.updateName(authUser, personId, dto.name);
|
||||
}
|
||||
|
||||
if (dto.featureFaceAssetId) {
|
||||
await this.updateFaceThumbnail(personId, dto.featureFaceAssetId);
|
||||
}
|
||||
|
||||
return mapPerson(person);
|
||||
}
|
||||
|
||||
private async updateName(authUser: AuthUserDto, personId: string, name: string): Promise<PersonEntity> {
|
||||
const person = await this.repository.update({ id: personId, name });
|
||||
|
||||
const relatedAsset = await this.getAssets(authUser, personId);
|
||||
const assetIds = relatedAsset.map((asset) => asset.id);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: assetIds } });
|
||||
|
||||
return mapPerson(person);
|
||||
return person;
|
||||
}
|
||||
|
||||
private async updateFaceThumbnail(personId: string, assetId: string): Promise<void> {
|
||||
const face = await this.repository.getFaceById({ assetId, personId });
|
||||
|
||||
if (!face) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
return await this.jobRepository.queue({
|
||||
name: JobName.GENERATE_FACE_THUMBNAIL,
|
||||
data: {
|
||||
assetId: assetId,
|
||||
personId,
|
||||
boundingBox: {
|
||||
x1: face.boundingBoxX1,
|
||||
x2: face.boundingBoxX2,
|
||||
y1: face.boundingBoxY1,
|
||||
y2: face.boundingBoxY2,
|
||||
},
|
||||
imageHeight: face.imageHeight,
|
||||
imageWidth: face.imageWidth,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async handlePersonCleanup() {
|
||||
|
||||
Reference in New Issue
Block a user