fix: suggest people (#4566)

* fix: suggest people

* feat: remove hidden people

* add hidden people when merging faces

* pr feedback

* fix: don't use reactive statement

* fixed section height

* improve merging

* fix: migration

* fix migration

* feat: add asset count

* fix: test

* rename endpoint

* add server test

* improve responsive design

* fix: remove videos from live photos in the asset count

* pr feedback

* fix: rename asset count endpoint

* fix: return firstname and lastname

* fix: reset people only on error

* fix: search

* fix: responsive design & div flickering

* fix: cleanup

* chore: open api

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
martin
2023-10-24 17:53:49 +02:00
committed by GitHub
parent 1aae29a0b8
commit 3e3598fd92
29 changed files with 736 additions and 80 deletions

View File

@@ -73,6 +73,11 @@ export class PersonResponseDto {
isHidden!: boolean;
}
export class PersonStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
assets!: number;
}
export class PeopleResponseDto {
@ApiProperty({ type: 'integer' })
total!: number;

View File

@@ -42,6 +42,8 @@ const responseDto: PersonResponseDto = {
isHidden: false,
};
const statistics = { assets: 3 };
const croppedFace = Buffer.from('Cropped Face');
const detectFaceMock = {
@@ -731,4 +733,21 @@ describe(PersonService.name, () => {
expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
});
});
describe('getStatistics', () => {
it('should get correct number of person', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson);
personMock.getStatistics.mockResolvedValue(statistics);
accessMock.person.hasOwnerAccess.mockResolvedValue(true);
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 });
expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
});
it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson);
accessMock.person.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
});
});
});

View File

@@ -33,6 +33,7 @@ import {
PeopleUpdateDto,
PersonResponseDto,
PersonSearchDto,
PersonStatisticsResponseDto,
PersonUpdateDto,
mapPerson,
} from './person.dto';
@@ -84,6 +85,11 @@ export class PersonService {
return this.findOrFail(id).then(mapPerson);
}
async getStatistics(authUser: AuthUserDto, id: string): Promise<PersonStatisticsResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
return this.repository.getStatistics(id);
}
async getThumbnail(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
const person = await this.repository.getById(id);

View File

@@ -1,4 +1,5 @@
import { AssetEntity, AssetFaceEntity, PersonEntity } from '@app/infra/entities';
export const IPersonRepository = 'IPersonRepository';
export interface PersonSearchOptions {
@@ -6,6 +7,10 @@ export interface PersonSearchOptions {
withHidden: boolean;
}
export interface PersonNameSearchOptions {
withHidden?: boolean;
}
export interface AssetFaceId {
assetId: string;
personId: string;
@@ -16,13 +21,17 @@ export interface UpdateFacesData {
newPersonId: string;
}
export interface PersonStatistics {
assets: number;
}
export interface IPersonRepository {
getAll(): Promise<PersonEntity[]>;
getAllWithoutThumbnail(): Promise<PersonEntity[]>;
getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
getAllWithoutFaces(): Promise<PersonEntity[]>;
getById(personId: string): Promise<PersonEntity | null>;
getByName(userId: string, personName: string): Promise<PersonEntity[]>;
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
getAssets(personId: string): Promise<AssetEntity[]>;
prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
@@ -33,6 +42,8 @@ export interface IPersonRepository {
delete(entity: PersonEntity): Promise<PersonEntity | null>;
deleteAll(): Promise<number>;
getStatistics(personId: string): Promise<PersonStatistics>;
getAllFaces(): Promise<AssetFaceEntity[]>;
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;

View File

@@ -90,4 +90,9 @@ export class SearchPeopleDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsBoolean()
@Transform(toBoolean)
@Optional()
withHidden?: boolean;
}

View File

@@ -159,8 +159,8 @@ export class SearchService {
};
}
async searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return await this.personRepository.getByName(authUser.id, dto.name);
searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(authUser.id, dto.name, { withHidden: dto.withHidden });
}
async handleIndexAlbums() {