diff --git a/src/backend/middlewares/PersonMWs.ts b/src/backend/middlewares/PersonMWs.ts index 61a70317..0c38811d 100644 --- a/src/backend/middlewares/PersonMWs.ts +++ b/src/backend/middlewares/PersonMWs.ts @@ -45,10 +45,10 @@ export class PersonMWs { } try { const persons = (req.resultPipe as PersonWithPhoto[]); - for (let i = 0; i < persons.length; i++) { - persons[i].samplePhoto = await ObjectManagers.getInstance() - .PersonManager.getSamplePhoto(persons[i].name); - } + const photoMap = await ObjectManagers.getInstance() + .PersonManager.getSamplePhotos(persons.map(p => p.name)); + persons.forEach(p => p.samplePhoto = photoMap[p.name]); + req.resultPipe = persons; return next(); diff --git a/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts b/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts index ef694f25..dd089972 100644 --- a/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts +++ b/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts @@ -10,6 +10,7 @@ import {ThumbnailSourceType} from '../../model/threading/PhotoWorker'; import {MediaDTO} from '../../../common/entities/MediaDTO'; import {PersonWithPhoto} from '../PersonMWs'; import {PhotoProcessing} from '../../model/fileprocessing/PhotoProcessing'; +import {PhotoDTO} from '../../../common/entities/PhotoDTO'; export class ThumbnailGeneratorMWs { @@ -75,12 +76,14 @@ export class ThumbnailGeneratorMWs { if (!req.resultPipe) { return next(); } + const photo: PhotoDTO = req.resultPipe; try { - req.resultPipe = await PhotoProcessing.generatePersonThumbnail(req.resultPipe); + req.resultPipe = await PhotoProcessing.generatePersonThumbnail(photo); return next(); } catch (error) { + console.error(error); return next(new ErrorDTO(ErrorCodes.THUMBNAIL_GENERATION_ERROR, - 'Error during generating face thumbnail: ' + req.resultPipe, error.toString())); + 'Error during generating face thumbnail: ' + photo.name, error.toString())); } } diff --git a/src/backend/model/database/interfaces/IPersonManager.ts b/src/backend/model/database/interfaces/IPersonManager.ts index 28fed32c..6feda8ec 100644 --- a/src/backend/model/database/interfaces/IPersonManager.ts +++ b/src/backend/model/database/interfaces/IPersonManager.ts @@ -7,6 +7,8 @@ export interface IPersonManager { getSamplePhoto(name: string): Promise; + getSamplePhotos(names: string[]): Promise<{ [key: string]: PhotoDTO }>; + get(name: string): Promise; saveAll(names: string[]): Promise; diff --git a/src/backend/model/database/sql/PersonManager.ts b/src/backend/model/database/sql/PersonManager.ts index f929c146..c040f7a6 100644 --- a/src/backend/model/database/sql/PersonManager.ts +++ b/src/backend/model/database/sql/PersonManager.ts @@ -6,6 +6,7 @@ import {MediaEntity} from './enitites/MediaEntity'; import {FaceRegionEntry} from './enitites/FaceRegionEntry'; import {PersonDTO} from '../../../../common/entities/PersonDTO'; import {Utils} from '../../../../common/Utils'; +import {SelectQueryBuilder} from 'typeorm'; const LOG_TAG = '[PersonManager]'; @@ -35,28 +36,39 @@ export class PersonManager implements IPersonManager { } async getSamplePhoto(name: string): Promise { - if (!this.samplePhotos[name]) { + return (await this.getSamplePhotos([name]))[name]; + } + + + async getSamplePhotos(names: string[]): Promise<{ [key: string]: PhotoDTO }> { + const hasAll = names.reduce((prev, name) => prev && !!this.samplePhotos[name], true); + if (!hasAll) { const connection = await SQLConnection.getConnection(); - const rawAndEntities = await connection.getRepository(MediaEntity).createQueryBuilder('media') - .limit(1) - .leftJoinAndSelect('media.directory', 'directory') + const rawAndEntities = await (connection + .getRepository(MediaEntity) + .createQueryBuilder('media') as SelectQueryBuilder) + .select(['media.name', 'media.id', 'person.name', 'directory.name', + 'directory.path', 'media.metadata.size.width', 'media.metadata.size.height']) + .leftJoin('media.directory', 'directory') .leftJoinAndSelect('media.metadata.faces', 'faces') .leftJoin('faces.person', 'person') - .where('person.name LIKE :name COLLATE utf8_general_ci', {name: name}).getRawAndEntities(); + .groupBy('person.name') + .orWhere(`person.name IN (:...names) COLLATE utf8_general_ci`, {names: names}).getRawAndEntities(); - if (rawAndEntities.entities.length === 0) { - return null; + + for (let i = 0; i < rawAndEntities.raw.length; ++i) { + this.samplePhotos[rawAndEntities.raw[i].person_name] = + Utils.clone(rawAndEntities.entities.find(m => m.name === rawAndEntities.raw[i].media_name)); + this.samplePhotos[rawAndEntities.raw[i].person_name].metadata.faces = [FaceRegionEntry.fromRawToDTO(rawAndEntities.raw[i])]; } - const media: PhotoDTO = Utils.clone(rawAndEntities.entities[0]); - - media.metadata.faces = [FaceRegionEntry.fromRawToDTO(rawAndEntities.raw[0])]; - - this.samplePhotos[name] = media; } - return this.samplePhotos[name]; + const photoMap: { [key: string]: PhotoDTO } = {}; + names.forEach(n => photoMap[n] = this.samplePhotos[n]); + return photoMap; } + async loadAll(): Promise { const connection = await SQLConnection.getConnection(); const personRepository = connection.getRepository(PersonEntry); diff --git a/src/backend/model/database/sql/enitites/PersonEntry.ts b/src/backend/model/database/sql/enitites/PersonEntry.ts index 49ba5bb3..054f188d 100644 --- a/src/backend/model/database/sql/enitites/PersonEntry.ts +++ b/src/backend/model/database/sql/enitites/PersonEntry.ts @@ -6,6 +6,7 @@ import {PersonDTO} from '../../../../../common/entities/PersonDTO'; @Entity() @Unique(['name']) export class PersonEntry implements PersonDTO { + @Index() @PrimaryGeneratedColumn({unsigned: true}) id: number; diff --git a/test/backend/unit/model/sql/PersonManager.ts b/test/backend/unit/model/sql/PersonManager.ts index a9178618..23af543b 100644 --- a/test/backend/unit/model/sql/PersonManager.ts +++ b/test/backend/unit/model/sql/PersonManager.ts @@ -1,4 +1,14 @@ +import {expect} from 'chai'; import {PersonManager} from '../../../../../src/backend/model/database/sql/PersonManager'; +import {SQLTestHelper} from '../../../SQLTestHelper'; +import {TestHelper} from './TestHelper'; +import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO'; +import {PersonEntry} from '../../../../../src/backend/model/database/sql/enitites/PersonEntry'; +import {FaceRegionEntry} from '../../../../../src/backend/model/database/sql/enitites/FaceRegionEntry'; +import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; +import {PhotoEntity} from '../../../../../src/backend/model/database/sql/enitites/PhotoEntity'; +import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity'; +import {VideoEntity} from '../../../../../src/backend/model/database/sql/enitites/VideoEntity'; // to help WebStorm to handle the test cases @@ -7,7 +17,78 @@ declare const after: any; declare const it: any; -describe('PersonManager', () => { +describe = SQLTestHelper.describe; +describe('PersonManager', (sqlHelper: SQLTestHelper) => { + + + const dir = TestHelper.getDirectoryEntry(); + const p = TestHelper.getPhotoEntry1(dir); + const p2 = TestHelper.getPhotoEntry2(dir); + const p_faceLess = TestHelper.getPhotoEntry2(dir); + delete p_faceLess.metadata.faces; + p_faceLess.name = 'fl'; + const v = TestHelper.getVideoEntry1(dir); + + const setUpSqlDB = async () => { + await sqlHelper.initDB(); + + const savePhoto = async (photo: PhotoDTO) => { + const savedPhoto = await pr.save(photo); + if (!photo.metadata.faces) { + return; + } + for (let i = 0; i < photo.metadata.faces.length; i++) { + const face = photo.metadata.faces[i]; + const person = await conn.getRepository(PersonEntry).save({name: face.name}); + await conn.getRepository(FaceRegionEntry).save({box: face.box, person: person, media: savedPhoto}); + } + }; + const conn = await SQLConnection.getConnection(); + + const pr = conn.getRepository(PhotoEntity); + + await conn.getRepository(DirectoryEntity).save(p.directory); + await savePhoto(p); + await savePhoto(p2); + await savePhoto(p_faceLess); + + await conn.getRepository(VideoEntity).save(v); + + await SQLConnection.close(); + }; + + + beforeEach(async () => { + await setUpSqlDB(); + }); + + after(async () => { + await sqlHelper.clearDB(); + }); + + + it('should get sample photos', async () => { + const pm = new PersonManager(); + const map: { [key: string]: PhotoDTO } = {}; + p.metadata.faces.forEach(face => { + map[face.name] = { + id: p.id, + name: p.name, + directory: { + path: p.directory.path, + name: p.directory.name, + }, + metadata: { + size: p.metadata.size, + faces: [p.metadata.faces.find(f => f.name === face.name)] + }, + readyIcon: false, + readyThumbnails: [] + }; + + }); + expect(await pm.getSamplePhotos(p.metadata.faces.map(f => f.name))).to.deep.equal(map); + }); });