From 9a923aa8ab167e8a7f63536609fec08f2fb34796 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sat, 16 Jan 2021 16:59:59 +0100 Subject: [PATCH] Implementing model for advanced searching. #58 --- package-lock.json | 33 + package.json | 1 + src/backend/model/ObjectManagers.ts | 12 + src/backend/model/database/LocationManager.ts | 14 + .../model/database/sql/GalleryManager.ts | 16 +- .../model/database/sql/SearchManager.ts | 296 +++- src/common/entities/DirectoryDTO.ts | 7 + src/common/entities/RandomQueryDTO.ts | 1 + src/common/entities/SearchQueryDTO.ts | 106 ++ src/common/entities/SearchResultDTO.ts | 4 +- test/backend/SQLTestHelper.ts | 4 +- test/backend/unit/model/sql/SearchManager.ts | 1262 ++++++++++++++--- test/backend/unit/model/sql/TestHelper.ts | 119 +- 13 files changed, 1607 insertions(+), 268 deletions(-) create mode 100644 src/backend/model/database/LocationManager.ts create mode 100644 src/common/entities/SearchQueryDTO.ts diff --git a/package-lock.json b/package-lock.json index 5708a0ef..c78f0d6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6729,6 +6729,16 @@ } } }, + "deep-equal-in-any-order": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.28.tgz", + "integrity": "sha512-qq3jffpGmAG9kGpZGKusjRwoGxmFgIqNW076HQmV9rNdrFsgTcpuCyp6dBhzdVCWgQDkgRmvZLYAilV4u2BsfQ==", + "dev": true, + "requires": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^1.1.21" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -12245,6 +12255,12 @@ "lodash.isarray": "^3.0.0" } }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=", + "dev": true + }, "lodash.restparam": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", @@ -17448,6 +17464,23 @@ "socks": "~2.2.0" } }, + "sort-any": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-1.1.23.tgz", + "integrity": "sha512-aY92w1RkjIyJd1l+O4btCwfAIfZm2r+zA6+cfKbKUO5D5MEZlqY27B7QyHHIsEShBsvx+Ur1Oq3v/gfR6wxD/w==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } + } + }, "sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", diff --git a/package.json b/package.json index c82ae4cf..a743ee51 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "codelyzer": "6.0.1", "core-js": "3.8.2", "coveralls": "3.1.0", + "deep-equal-in-any-order": "^1.0.28", "ejs-loader": "0.5.0", "gulp": "4.0.2", "gulp-json-editor": "2.5.4", diff --git a/src/backend/model/ObjectManagers.ts b/src/backend/model/ObjectManagers.ts index 8608ab5f..689b9bf7 100644 --- a/src/backend/model/ObjectManagers.ts +++ b/src/backend/model/ObjectManagers.ts @@ -8,6 +8,7 @@ import {IIndexingManager} from './database/interfaces/IIndexingManager'; import {IPersonManager} from './database/interfaces/IPersonManager'; import {IVersionManager} from './database/interfaces/IVersionManager'; import {IJobManager} from './database/interfaces/IJobManager'; +import {LocationManager} from './database/LocationManager'; export class ObjectManagers { @@ -21,6 +22,7 @@ export class ObjectManagers { private _personManager: IPersonManager; private _versionManager: IVersionManager; private _jobManager: IJobManager; + private _locationManager: LocationManager; get VersionManager(): IVersionManager { @@ -31,6 +33,14 @@ export class ObjectManagers { this._versionManager = value; } + get LocationManager(): LocationManager { + return this._locationManager; + } + + set LocationManager(value: LocationManager) { + this._locationManager = value; + } + get PersonManager(): IPersonManager { return this._personManager; } @@ -129,6 +139,7 @@ export class ObjectManagers { ObjectManagers.getInstance().IndexingManager = new IndexingManager(); ObjectManagers.getInstance().PersonManager = new PersonManager(); ObjectManagers.getInstance().VersionManager = new VersionManager(); + ObjectManagers.getInstance().LocationManager = new LocationManager(); this.InitCommonManagers(); } @@ -149,6 +160,7 @@ export class ObjectManagers { ObjectManagers.getInstance().IndexingManager = new IndexingManager(); ObjectManagers.getInstance().PersonManager = new PersonManager(); ObjectManagers.getInstance().VersionManager = new VersionManager(); + ObjectManagers.getInstance().LocationManager = new LocationManager(); this.InitCommonManagers(); Logger.debug('SQL DB inited'); } diff --git a/src/backend/model/database/LocationManager.ts b/src/backend/model/database/LocationManager.ts new file mode 100644 index 00000000..21899e79 --- /dev/null +++ b/src/backend/model/database/LocationManager.ts @@ -0,0 +1,14 @@ +import {GPSMetadata} from '../../../common/entities/PhotoDTO'; + + +export class LocationManager { + + async getGPSData(text: string): Promise { + return { + longitude: 0, + latitude: 0, + altitude: 0 + }; + } + +} diff --git a/src/backend/model/database/sql/GalleryManager.ts b/src/backend/model/database/sql/GalleryManager.ts index 6d66fcda..79686654 100644 --- a/src/backend/model/database/sql/GalleryManager.ts +++ b/src/backend/model/database/sql/GalleryManager.ts @@ -24,20 +24,24 @@ const LOG_TAG = '[GalleryManager]'; export class GalleryManager implements IGalleryManager, ISQLGalleryManager { + public static parseRelativeDirePath(relativeDirectoryName: string): { name: string, parent: string } { + + relativeDirectoryName = DiskMangerWorker.normalizeDirPath(relativeDirectoryName); + return { + name: path.basename(relativeDirectoryName), + parent: path.join(path.dirname(relativeDirectoryName), path.sep), + }; + } public async listDirectory(relativeDirectoryName: string, knownLastModified?: number, knownLastScanned?: number): Promise { - - relativeDirectoryName = DiskMangerWorker.normalizeDirPath(relativeDirectoryName); - const directoryName = path.basename(relativeDirectoryName); - const directoryParent = path.join(path.dirname(relativeDirectoryName), path.sep); + const directoryPath = GalleryManager.parseRelativeDirePath(relativeDirectoryName); const connection = await SQLConnection.getConnection(); const stat = fs.statSync(path.join(ProjectPath.ImageFolder, relativeDirectoryName)); const lastModified = DiskMangerWorker.calcLastModified(stat); - - const dir = await this.selectParentDir(connection, directoryName, directoryParent); + const dir = await this.selectParentDir(connection, directoryPath.name, directoryPath.parent); if (dir && dir.lastScanned != null) { // If it seems that the content did not changed, do not work on it if (knownLastModified && knownLastScanned diff --git a/src/backend/model/database/sql/SearchManager.ts b/src/backend/model/database/sql/SearchManager.ts index 4bc182f3..c1d87afc 100644 --- a/src/backend/model/database/sql/SearchManager.ts +++ b/src/backend/model/database/sql/SearchManager.ts @@ -8,8 +8,24 @@ import {MediaEntity} from './enitites/MediaEntity'; import {VideoEntity} from './enitites/VideoEntity'; import {PersonEntry} from './enitites/PersonEntry'; import {FaceRegionEntry} from './enitites/FaceRegionEntry'; -import {SelectQueryBuilder} from 'typeorm'; +import {Brackets, SelectQueryBuilder, WhereExpression} from 'typeorm'; import {Config} from '../../../../common/config/private/Config'; +import { + ANDSearchQuery, + DateSearch, + DistanceSearch, + OrientationSearch, + OrientationSearchTypes, + ORSearchQuery, + RatingSearch, + ResolutionSearch, + SearchQueryDTO, + SearchQueryTypes, + TextSearch, + TextSearchQueryTypes +} from '../../../../common/entities/SearchQueryDTO'; +import {GalleryManager} from './GalleryManager'; +import {ObjectManagers} from '../../ObjectManagers'; export class SearchManager implements ISearchManager { @@ -113,6 +129,75 @@ export class SearchManager implements ISearchManager { return SearchManager.autoCompleteItemsUnique(result); } + async getGPSData(query: SearchQueryDTO) { + if ((query as ANDSearchQuery | ORSearchQuery).list) { + for (let i = 0; i < (query as ANDSearchQuery | ORSearchQuery).list.length; ++i) { + (query as ANDSearchQuery | ORSearchQuery).list[i] = + await this.getGPSData((query as ANDSearchQuery | ORSearchQuery).list[i]); + } + } + if (query.type === SearchQueryTypes.distance && (query).from.text) { + (query).from.GPSData = + await ObjectManagers.getInstance().LocationManager.getGPSData((query).from.text); + } + return query; + } + + async aSearch(query: SearchQueryDTO) { + query = this.flattenSameOfQueries(query); + query = await this.getGPSData(query); + const connection = await SQLConnection.getConnection(); + + const result: SearchResultDTO = { + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + }; + + + const sqlQuery = await connection.getRepository(MediaEntity).createQueryBuilder('media') + .innerJoin(q => { + const subQuery = q.from(MediaEntity, 'media') + .select('distinct media.id') + .limit(Config.Client.Search.maxMediaResult + 1); + + subQuery.leftJoin('media.directory', 'directory') + .leftJoin('media.metadata.faces', 'faces') + .leftJoin('faces.person', 'person') + .where(this.buildWhereQuery(query)); + + return subQuery; + }, + 'innerMedia', + 'media.id=innerMedia.id') + .leftJoinAndSelect('media.directory', 'directory') + .leftJoinAndSelect('media.metadata.faces', 'faces') + .leftJoinAndSelect('faces.person', 'person'); + + + result.media = await this.loadMediaWithFaces(sqlQuery); + + if (result.media.length > Config.Client.Search.maxMediaResult) { + result.resultOverflow = true; + } + + /* result.directories = await connection + .getRepository(DirectoryEntity) + .createQueryBuilder('dir') + .where('dir.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .limit(Config.Client.Search.maxMediaResult + 1) + .getMany(); + + if (result.directories.length > Config.Client.Search.maxDirectoryResult) { + result.resultOverflow = true; + }*/ + + return result; + } + async search(text: string, searchType: SearchTypes): Promise { const connection = await SQLConnection.getConnection(); @@ -245,6 +330,195 @@ export class SearchManager implements ISearchManager { return result; } + private buildWhereQuery(query: SearchQueryDTO, paramCounter = {value: 0}): Brackets { + switch (query.type) { + case SearchQueryTypes.AND: + return new Brackets(q => { + (query).list.forEach(sq => q.andWhere(this.buildWhereQuery(sq, paramCounter))); + return q; + }); + case SearchQueryTypes.OR: + return new Brackets(q => { + (query).list.forEach(sq => q.orWhere(this.buildWhereQuery(sq, paramCounter))); + return q; + }); + + + case SearchQueryTypes.distance: + /** + * This is a best effort calculation, not fully accurate in order to have higher performance. + * see: https://stackoverflow.com/a/50506609 + */ + const earth = 6378.137, // radius of the earth in kilometer + latDelta = (1 / ((2 * Math.PI / 360) * earth)), // 1 km in degree + lonDelta = (1 / ((2 * Math.PI / 360) * earth)); // 1 km in degree + + const minLat = (query).from.GPSData.latitude - ((query).distance * latDelta), + maxLat = (query).from.GPSData.latitude + ((query).distance * latDelta), + minLon = (query).from.GPSData.latitude - + ((query).distance * lonDelta) / Math.cos(minLat * (Math.PI / 180)), + maxLon = (query).from.GPSData.latitude + + ((query).distance * lonDelta) / Math.cos(maxLat * (Math.PI / 180)); + + return new Brackets(q => { + const textParam: any = {}; + paramCounter.value++; + textParam['maxLat' + paramCounter.value] = maxLat; + textParam['minLat' + paramCounter.value] = minLat; + textParam['maxLon' + paramCounter.value] = maxLon; + textParam['minLon' + paramCounter.value] = minLon; + q.where(`media.metadata.positionData.GPSData.latitude < :maxLat${paramCounter.value}`, textParam); + q.andWhere(`media.metadata.positionData.GPSData.latitude > :minLat${paramCounter.value}`, textParam); + q.andWhere(`media.metadata.positionData.GPSData.longitude < :maxLon${paramCounter.value}`, textParam); + q.andWhere(`media.metadata.positionData.GPSData.longitude > :minLon${paramCounter.value}`, textParam); + return q; + }); + + case SearchQueryTypes.date: + return new Brackets(q => { + if (typeof (query).before === 'undefined' && typeof (query).after === 'undefined') { + throw new Error('Invalid search query: Date Query should contain before or after value'); + } + if (typeof (query).before !== 'undefined') { + const textParam: any = {}; + textParam['before' + paramCounter.value] = (query).before; + q.where(`media.metadata.creationDate <= :before${paramCounter.value}`, textParam); + } + + if (typeof (query).after !== 'undefined') { + const textParam: any = {}; + textParam['after' + paramCounter.value] = (query).after; + q.andWhere(`media.metadata.creationDate >= :after${paramCounter.value}`, textParam); + } + paramCounter.value++; + return q; + }); + + case SearchQueryTypes.rating: + return new Brackets(q => { + if (typeof (query).min === 'undefined' && typeof (query).max === 'undefined') { + throw new Error('Invalid search query: Rating Query should contain min or max value'); + } + if (typeof (query).min !== 'undefined') { + const textParam: any = {}; + textParam['min' + paramCounter.value] = (query).min; + q.where(`media.metadata.rating >= :min${paramCounter.value}`, textParam); + } + + if (typeof (query).max !== 'undefined') { + const textParam: any = {}; + textParam['max' + paramCounter.value] = (query).max; + q.andWhere(`media.metadata.rating <= :max${paramCounter.value}`, textParam); + } + paramCounter.value++; + return q; + }); + + case SearchQueryTypes.resolution: + return new Brackets(q => { + if (typeof (query).min === 'undefined' && typeof (query).max === 'undefined') { + throw new Error('Invalid search query: Rating Query should contain min or max value'); + } + if (typeof (query).min !== 'undefined') { + const textParam: any = {}; + textParam['min' + paramCounter.value] = (query).min * 1000 * 1000; + q.where(`media.metadata.size.width * media.metadata.size.height >= :min${paramCounter.value}`, textParam); + } + + if (typeof (query).max !== 'undefined') { + const textParam: any = {}; + textParam['max' + paramCounter.value] = (query).max * 1000 * 1000; + q.andWhere(`media.metadata.size.width * media.metadata.size.height <= :max${paramCounter.value}`, textParam); + } + paramCounter.value++; + return q; + }); + + case SearchQueryTypes.orientation: + return new Brackets(q => { + if ((query).orientation === OrientationSearchTypes.landscape) { + q.where('media.metadata.size.width >= media.metadata.size.height'); + } + if ((query).orientation === OrientationSearchTypes.portrait) { + q.andWhere('media.metadata.size.width <= media.metadata.size.height'); + } + paramCounter.value++; + return q; + }); + + + case SearchQueryTypes.SOME_OF: + throw new Error('Some of not supported'); + + } + + return new Brackets((q: WhereExpression) => { + + const createMatchString = (str: string) => { + return (query).matchType === TextSearchQueryTypes.exact_match ? str : `%${str}%`; + }; + + const textParam: any = {}; + paramCounter.value++; + textParam['text' + paramCounter.value] = createMatchString((query).text); + + if (query.type === SearchQueryTypes.any_text || + query.type === SearchQueryTypes.directory) { + const dirPathStr = ((query).text).replace(new RegExp('\\\\', 'g'), '/'); + + + textParam['fullPath' + paramCounter.value] = createMatchString(dirPathStr); + q.orWhere(`directory.path LIKE :fullPath${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + + const directoryPath = GalleryManager.parseRelativeDirePath(dirPathStr); + q.orWhere(new Brackets(dq => { + textParam['dirName' + paramCounter.value] = createMatchString(directoryPath.name); + dq.where(`directory.name LIKE :dirName${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + if (dirPathStr.includes('/')) { + textParam['parentName' + paramCounter.value] = createMatchString(directoryPath.parent); + dq.andWhere(`directory.path LIKE :parentName${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } + return dq; + })); + } + + if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.file_name) { + q.orWhere(`media.name LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } + + if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.caption) { + q.orWhere(`media.metadata.caption LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } + if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.person) { + q.orWhere(`person.name LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } + + if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.position) { + q.orWhere(`media.metadata.positionData.country LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam) + .orWhere(`media.metadata.positionData.state LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam) + .orWhere(`media.metadata.positionData.city LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } + if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.keyword) { + q.orWhere(`media.metadata.keywords LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } + return q; + }); + } + + private flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO { + return query; + } + private encapsulateAutoComplete(values: string[], type: SearchTypes): Array { const res: AutoCompleteItem[] = []; values.forEach((value) => { @@ -259,11 +533,27 @@ export class SearchManager implements ISearchManager { let rawIndex = 0; for (let i = 0; i < media.length; i++) { - if (rawAndEntities.raw[rawIndex].faces_id === null || - rawAndEntities.raw[rawIndex].media_id !== media[i].id) { + + if (rawAndEntities.raw[rawIndex].media_id !== media[i].id) { + throw new Error('index mismatch'); + } + + // media without a face + if (rawAndEntities.raw[rawIndex].faces_id === null) { delete media[i].metadata.faces; + rawIndex++; continue; } + + + /* + if (rawAndEntities.raw[rawIndex].faces_id === null || + rawAndEntities.raw[rawIndex].media_id !== media[i].id) { + delete media[i].metadata.faces; + continue; + }*/ + + // process all faces for one media media[i].metadata.faces = []; while (rawAndEntities.raw[rawIndex].media_id === media[i].id) { diff --git a/src/common/entities/DirectoryDTO.ts b/src/common/entities/DirectoryDTO.ts index af4246de..483bae8d 100644 --- a/src/common/entities/DirectoryDTO.ts +++ b/src/common/entities/DirectoryDTO.ts @@ -1,5 +1,6 @@ import {MediaDTO} from './MediaDTO'; import {FileDTO} from './FileDTO'; +import {PhotoDTO} from './PhotoDTO'; export interface DirectoryDTO { id: number; @@ -54,4 +55,10 @@ export module DirectoryDTO { } }; + export const filterPhotos = (dir: DirectoryDTO): PhotoDTO[] => { + return dir.media.filter(m => MediaDTO.isPhoto(m)); + }; + export const filterVideos = (dir: DirectoryDTO): PhotoDTO[] => { + return dir.media.filter(m => MediaDTO.isPhoto(m)); + }; } diff --git a/src/common/entities/RandomQueryDTO.ts b/src/common/entities/RandomQueryDTO.ts index 9e4ae641..293aac3e 100644 --- a/src/common/entities/RandomQueryDTO.ts +++ b/src/common/entities/RandomQueryDTO.ts @@ -2,6 +2,7 @@ export enum OrientationType { any = 0, portrait = 1, landscape = 2 } +// TODO replace it with advanced search export interface RandomQueryDTO { directory?: string; recursive?: boolean; diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts new file mode 100644 index 00000000..6c8290ca --- /dev/null +++ b/src/common/entities/SearchQueryDTO.ts @@ -0,0 +1,106 @@ +import {GPSMetadata} from './PhotoDTO'; + +export enum SearchQueryTypes { + AND = 1, OR, SOME_OF, + + // non-text metadata + date = 10, + rating, + distance, + resolution, + orientation, + + // TEXT search types + any_text = 100, + person, + keyword, + position, + caption, + file_name, + directory, +} + +export enum TextSearchQueryTypes { + exact_match = 1, like = 2 +} + +export enum OrientationSearchTypes { + portrait = 1, landscape = 2 +} + +export interface SearchQueryDTO { + type: SearchQueryTypes; +} + +export interface ANDSearchQuery extends SearchQueryDTO { + type: SearchQueryTypes.AND; + list: SearchQueryDTO[]; +} + +export interface ORSearchQuery extends SearchQueryDTO { + type: SearchQueryTypes.OR; + list: SearchQueryDTO[]; +} + +export interface SomeOfSearchQuery extends SearchQueryDTO, RangeSearchQuery { + type: SearchQueryTypes.SOME_OF; + list: NegatableSearchQuery[]; + min?: number; // at least this amount of items + max?: number; // maximum this amount of items +} + +export interface NegatableSearchQuery extends SearchQueryDTO { + negate?: boolean; // if true negates the expression +} + +export interface RangeSearchQuery extends SearchQueryDTO { + min?: number; + max?: number; +} + + +export interface TextSearch extends NegatableSearchQuery { + type: SearchQueryTypes.any_text | + SearchQueryTypes.person | + SearchQueryTypes.keyword | + SearchQueryTypes.position | + SearchQueryTypes.caption | + SearchQueryTypes.file_name | + SearchQueryTypes.directory; + matchType: TextSearchQueryTypes; + text: string; +} + +export interface DistanceSearch extends NegatableSearchQuery { + type: SearchQueryTypes.distance; + from: { + text?: string; + GPSData?: GPSMetadata; + }; + distance: number; // in kms +} + + +export interface DateSearch extends NegatableSearchQuery { + type: SearchQueryTypes.date; + after?: number; + before?: number; +} + +export interface RatingSearch extends NegatableSearchQuery, RangeSearchQuery { + type: SearchQueryTypes.rating; + min?: number; + max?: number; +} + +export interface ResolutionSearch extends NegatableSearchQuery, RangeSearchQuery { + type: SearchQueryTypes.resolution; + min?: number; // in megapixels + max?: number; // in megapixels +} + +export interface OrientationSearch extends NegatableSearchQuery { + type: SearchQueryTypes.orientation; + orientation: OrientationSearchTypes; +} + diff --git a/src/common/entities/SearchResultDTO.ts b/src/common/entities/SearchResultDTO.ts index 60aa8b2d..c7bef2a7 100644 --- a/src/common/entities/SearchResultDTO.ts +++ b/src/common/entities/SearchResultDTO.ts @@ -1,13 +1,13 @@ import {DirectoryDTO} from './DirectoryDTO'; -import {PhotoDTO} from './PhotoDTO'; import {SearchTypes} from './AutoCompleteItem'; import {FileDTO} from './FileDTO'; +import {MediaDTO} from './MediaDTO'; export interface SearchResultDTO { searchText: string; searchType?: SearchTypes; directories: DirectoryDTO[]; - media: PhotoDTO[]; + media: MediaDTO[]; metaFile: FileDTO[]; resultOverflow: boolean; } diff --git a/test/backend/SQLTestHelper.ts b/test/backend/SQLTestHelper.ts index 1e283823..51df8e85 100644 --- a/test/backend/SQLTestHelper.ts +++ b/test/backend/SQLTestHelper.ts @@ -8,11 +8,12 @@ import {ProjectPath} from '../../src/backend/ProjectPath'; declare let describe: any; const savedDescribe = describe; + export class SQLTestHelper { static enable = { sqlite: true, - mysql: true + mysql: process.env.TEST_MYSQL !== 'false' }; public static readonly savedDescribe = savedDescribe; tempDir: string; @@ -40,6 +41,7 @@ export class SQLTestHelper { }); } + public async initDB() { if (this.dbType === ServerConfig.DatabaseType.sqlite) { await this.initSQLite(); diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index 5db86bb7..4139986c 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -1,60 +1,164 @@ -import {expect} from 'chai'; -import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; -import {PhotoEntity} from '../../../../../src/backend/model/database/sql/enitites/PhotoEntity'; +import {LocationManager} from '../../../../../src/backend/model/database/LocationManager'; import {SearchManager} from '../../../../../src/backend/model/database/sql/SearchManager'; -import {AutoCompleteItem, SearchTypes} from '../../../../../src/common/entities/AutoCompleteItem'; import {SearchResultDTO} from '../../../../../src/common/entities/SearchResultDTO'; -import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity'; import {Utils} from '../../../../../src/common/Utils'; -import {TestHelper} from './TestHelper'; -import {VideoEntity} from '../../../../../src/backend/model/database/sql/enitites/VideoEntity'; -import {PersonEntry} from '../../../../../src/backend/model/database/sql/enitites/PersonEntry'; -import {FaceRegionEntry} from '../../../../../src/backend/model/database/sql/enitites/FaceRegionEntry'; -import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO'; import {SQLTestHelper} from '../../../SQLTestHelper'; -import {Config} from '../../../../../src/common/config/private/Config'; +import * as path from 'path'; +import { + ANDSearchQuery, + DateSearch, + DistanceSearch, + OrientationSearch, + OrientationSearchTypes, + ORSearchQuery, + RatingSearch, + ResolutionSearch, + SearchQueryDTO, + SearchQueryTypes, + TextSearch, + TextSearchQueryTypes +} from '../../../../../src/common/entities/SearchQueryDTO'; +import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager'; +import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; +import {TestHelper} from './TestHelper'; +import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; +import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; +import {DiskMangerWorker} from '../../../../../src/backend/model/threading/DiskMangerWorker'; +import {GalleryManager} from '../../../../../src/backend/model/database/sql/GalleryManager'; +import {Connection} from 'typeorm'; +import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity'; +import {GPSMetadata, PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO'; +import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; +import {MediaDTO} from '../../../../../src/common/entities/MediaDTO'; + +const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +const chai = require('chai'); + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; // to help WebStorm to handle the test cases declare let describe: any; declare const after: any; -describe = SQLTestHelper.describe; +const tmpDescribe = describe; +describe = SQLTestHelper.describe; // fake it os IDE plays nicely (recognize the test) + + +class IndexingManagerTest extends IndexingManager { + + public async saveToDB(scannedDirectory: DirectoryDTO): Promise { + return super.saveToDB(scannedDirectory); + } +} + +class GalleryManagerTest extends GalleryManager { + + public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise { + return super.selectParentDir(connection, directoryName, directoryParent); + } + + public async fillParentDir(connection: Connection, dir: DirectoryEntity): Promise { + return super.fillParentDir(connection, dir); + } +} describe('SearchManager', (sqlHelper: SQLTestHelper) => { + describe = tmpDescribe; + let dir: DirectoryDTO; + /** + * dir + * |- v + * |- p + * |- p2 + * |-> subDir + * |- p_faceLess + * |-> subDir2 + * |- p4 + */ - 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); + let v: VideoDTO; + let p: PhotoDTO; + let p2: PhotoDTO; + let p_faceLess: PhotoDTO; + let p4: PhotoDTO; + + + const setUpTestGallery = async (): Promise => { + let directory: DirectoryDTO = TestHelper.getDirectoryEntry(); + const subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace'); + const subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi'); + TestHelper.getPhotoEntry1(directory); + TestHelper.getPhotoEntry2(directory); + TestHelper.getPhotoEntry4(subDir2); + const pFaceLess = TestHelper.getPhotoEntry3(subDir); + delete pFaceLess.metadata.faces; + TestHelper.getVideoEntry1(directory); + + await ObjectManagers.InitSQLManagers(); + const connection = await SQLConnection.getConnection(); + ObjectManagers.getInstance().IndexingManager.indexDirectory = () => Promise.resolve(null); + + const im = new IndexingManagerTest(); + await im.saveToDB(directory); + await im.saveToDB(subDir); + await im.saveToDB(subDir2); + + if (ObjectManagers.getInstance().IndexingManager && + ObjectManagers.getInstance().IndexingManager.IsSavingInProgress) { + await ObjectManagers.getInstance().IndexingManager.SavingReady; + } + + const gm = new GalleryManagerTest(); + directory = await gm.selectParentDir(connection, directory.name, path.join(path.dirname('.'), path.sep)); + await gm.fillParentDir(connection, directory); + + const populateDir = async (d: DirectoryDTO) => { + for (let i = 0; i < d.directories.length; i++) { + d.directories[i] = await gm.selectParentDir(connection, d.directories[i].name, + path.join(DiskMangerWorker.pathFromParent(d), path.sep)); + await gm.fillParentDir(connection, d.directories[i]); + await populateDir(d.directories[i]); + } + }; + await populateDir(directory); + + await ObjectManagers.reset(); + + dir = directory; + p = dir.media[0]; + p2 = dir.media[1]; + v = dir.media[2]; + p4 = dir.directories[1].media[0]; + p_faceLess = dir.directories[0].media[0]; + }; const setUpSqlDB = async () => { await sqlHelper.initDB(); + await setUpTestGallery(); + /* + 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 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); - 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(DirectoryEntity).save(p.directory); - await savePhoto(p); - await savePhoto(p2); - await savePhoto(p_faceLess); + await conn.getRepository(VideoEntity).save(v);*/ - await conn.getRepository(VideoEntity).save(v); - - await SQLConnection.close(); + // await SQLConnection.close(); }; @@ -66,215 +170,903 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { after(async () => { await sqlHelper.clearDB(); }); + /* + it('should get autocomplete', async () => { + const sm = new SearchManager(); - it('should get autocomplete', async () => { - const sm = new SearchManager(); + const cmp = (a: AutoCompleteItem, b: AutoCompleteItem) => { + if (a.text === b.text) { + return a.type - b.type; + } + return a.text.localeCompare(b.text); + }; - const cmp = (a: AutoCompleteItem, b: AutoCompleteItem) => { - if (a.text === b.text) { - return a.type - b.type; - } - return a.text.localeCompare(b.text); - }; + expect((await sm.autocomplete('tat'))).to.deep.equalInAnyOrder([new AutoCompleteItem('Tatooine', SearchTypes.position)]); + expect((await sm.autocomplete('star'))).to.deep.equalInAnyOrder([new AutoCompleteItem('star wars', SearchTypes.keyword), + new AutoCompleteItem('death star', SearchTypes.keyword)]); - expect((await sm.autocomplete('tat'))).to.deep.equal([new AutoCompleteItem('Tatooine', SearchTypes.position)]); - expect((await sm.autocomplete('star'))).to.deep.equal([new AutoCompleteItem('star wars', SearchTypes.keyword), - new AutoCompleteItem('death star', SearchTypes.keyword)]); + expect((await sm.autocomplete('wars'))).to.deep.equalInAnyOrder([new AutoCompleteItem('star wars', SearchTypes.keyword), + new AutoCompleteItem('wars dir', SearchTypes.directory)]); - expect((await sm.autocomplete('wars'))).to.deep.equal([new AutoCompleteItem('star wars', SearchTypes.keyword), - new AutoCompleteItem('wars dir', SearchTypes.directory)]); + expect((await sm.autocomplete('arch'))).eql([new AutoCompleteItem('Research City', SearchTypes.position)]); - expect((await sm.autocomplete('arch'))).eql([new AutoCompleteItem('Research City', SearchTypes.position)]); + Config.Client.Search.AutoComplete.maxItemsPerCategory = 99999; + expect((await sm.autocomplete('a')).sort(cmp)).eql([ + new AutoCompleteItem('Boba Fett', SearchTypes.keyword), + new AutoCompleteItem('Boba Fett', SearchTypes.person), + new AutoCompleteItem('star wars', SearchTypes.keyword), + new AutoCompleteItem('Anakin', SearchTypes.keyword), + new AutoCompleteItem('Anakin Skywalker', SearchTypes.person), + new AutoCompleteItem('Luke Skywalker', SearchTypes.person), + new AutoCompleteItem('Han Solo', SearchTypes.person), + new AutoCompleteItem('death star', SearchTypes.keyword), + new AutoCompleteItem('Padmé Amidala', SearchTypes.person), + new AutoCompleteItem('Obivan Kenobi', SearchTypes.person), + new AutoCompleteItem('Arvíztűrő Tükörfúrógép', SearchTypes.person), + new AutoCompleteItem('Padmé Amidala', SearchTypes.keyword), + new AutoCompleteItem('Natalie Portman', SearchTypes.keyword), + new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo), + new AutoCompleteItem('Kamino', SearchTypes.position), + new AutoCompleteItem('Tatooine', SearchTypes.position), + new AutoCompleteItem('wars dir', SearchTypes.directory), + new AutoCompleteItem('Research City', SearchTypes.position)].sort(cmp)); - Config.Client.Search.AutoComplete.maxItemsPerCategory = 99999; - expect((await sm.autocomplete('a')).sort(cmp)).eql([ - new AutoCompleteItem('Boba Fett', SearchTypes.keyword), - new AutoCompleteItem('Boba Fett', SearchTypes.person), - new AutoCompleteItem('star wars', SearchTypes.keyword), - new AutoCompleteItem('Anakin', SearchTypes.keyword), - new AutoCompleteItem('Anakin Skywalker', SearchTypes.person), - new AutoCompleteItem('Luke Skywalker', SearchTypes.person), - new AutoCompleteItem('Han Solo', SearchTypes.person), - new AutoCompleteItem('death star', SearchTypes.keyword), - new AutoCompleteItem('Padmé Amidala', SearchTypes.person), - new AutoCompleteItem('Obivan Kenobi', SearchTypes.person), - new AutoCompleteItem('Arvíztűrő Tükörfúrógép', SearchTypes.person), - new AutoCompleteItem('Padmé Amidala', SearchTypes.keyword), - new AutoCompleteItem('Natalie Portman', SearchTypes.keyword), - new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo), - new AutoCompleteItem('Kamino', SearchTypes.position), - new AutoCompleteItem('Tatooine', SearchTypes.position), - new AutoCompleteItem('wars dir', SearchTypes.directory), - new AutoCompleteItem('Research City', SearchTypes.position)].sort(cmp)); + Config.Client.Search.AutoComplete.maxItemsPerCategory = 1; + expect((await sm.autocomplete('a')).sort(cmp)).eql([ + new AutoCompleteItem('Anakin', SearchTypes.keyword), + new AutoCompleteItem('star wars', SearchTypes.keyword), + new AutoCompleteItem('death star', SearchTypes.keyword), + new AutoCompleteItem('Anakin Skywalker', SearchTypes.person), + new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo), + new AutoCompleteItem('Kamino', SearchTypes.position), + new AutoCompleteItem('Research City', SearchTypes.position), + new AutoCompleteItem('wars dir', SearchTypes.directory), + new AutoCompleteItem('Boba Fett', SearchTypes.keyword)].sort(cmp)); + Config.Client.Search.AutoComplete.maxItemsPerCategory = 5; - Config.Client.Search.AutoComplete.maxItemsPerCategory = 1; - expect((await sm.autocomplete('a')).sort(cmp)).eql([ - new AutoCompleteItem('Anakin', SearchTypes.keyword), - new AutoCompleteItem('star wars', SearchTypes.keyword), - new AutoCompleteItem('death star', SearchTypes.keyword), - new AutoCompleteItem('Anakin Skywalker', SearchTypes.person), - new AutoCompleteItem('Han Solo\'s dice', SearchTypes.photo), - new AutoCompleteItem('Kamino', SearchTypes.position), - new AutoCompleteItem('Research City', SearchTypes.position), - new AutoCompleteItem('wars dir', SearchTypes.directory), - new AutoCompleteItem('Boba Fett', SearchTypes.keyword)].sort(cmp)); - Config.Client.Search.AutoComplete.maxItemsPerCategory = 5; + expect((await sm.autocomplete('sw')).sort(cmp)).to.deep.equalInAnyOrder([new AutoCompleteItem('sw1', SearchTypes.photo), + new AutoCompleteItem('sw2', SearchTypes.photo), new AutoCompleteItem(v.name, SearchTypes.video)].sort(cmp)); - expect((await sm.autocomplete('sw')).sort(cmp)).to.deep.equal([new AutoCompleteItem('sw1', SearchTypes.photo), - new AutoCompleteItem('sw2', SearchTypes.photo), new AutoCompleteItem(v.name, SearchTypes.video)].sort(cmp)); + expect((await sm.autocomplete(v.name)).sort(cmp)).to.deep.equalInAnyOrder([new AutoCompleteItem(v.name, SearchTypes.video)]); - expect((await sm.autocomplete(v.name)).sort(cmp)).to.deep.equal([new AutoCompleteItem(v.name, SearchTypes.video)]); + }); + */ + /* + it('should search', async () => { + const sm = new SearchManager(); + + + expect(Utils.clone(await sm.search('sw', null))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'sw', + searchType: null, + directories: [], + media: [p, p2, v], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('Boba', null))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'Boba', + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('Tatooine', SearchTypes.position))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'Tatooine', + searchType: SearchTypes.position, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('ortm', SearchTypes.keyword))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'ortm', + searchType: SearchTypes.keyword, + directories: [], + media: [p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('ortm', SearchTypes.keyword))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'ortm', + searchType: SearchTypes.keyword, + directories: [], + media: [p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('wa', SearchTypes.keyword))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'wa', + searchType: SearchTypes.keyword, + directories: [dir], + media: [p, p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('han', SearchTypes.photo))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'han', + searchType: SearchTypes.photo, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('sw', SearchTypes.video))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'sw', + searchType: SearchTypes.video, + directories: [], + media: [v], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('han', SearchTypes.keyword))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'han', + searchType: SearchTypes.keyword, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.search('Boba', SearchTypes.person))).to.deep.equalInAnyOrder(removeDir({ + searchText: 'Boba', + searchType: SearchTypes.person, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + }); + */ + + const searchifyMedia = (m: MediaDTO): MediaDTO => { + const tmpM = m.directory.media; + const tmpD = m.directory.directories; + const tmpMT = m.directory.metaFile; + delete m.directory.directories; + delete m.directory.media; + delete m.directory.metaFile; + const ret = Utils.clone(m); + if ((ret.metadata as PhotoMetadata).faces && !(ret.metadata as PhotoMetadata).faces.length) { + delete (ret.metadata as PhotoMetadata).faces; + } + m.directory.directories = tmpD; + m.directory.media = tmpM; + m.directory.metaFile = tmpMT; + return ret; + }; + + const removeDir = (result: SearchResultDTO) => { + result.media = result.media.map(m => searchifyMedia(m)); + return Utils.clone(result); + }; + + describe('advanced search', async () => { + + it('should AND', async () => { + const sm = new SearchManager(); + + let query: SearchQueryDTO = { + type: SearchQueryTypes.AND, + list: [{text: p.metadata.faces[0].name, type: SearchQueryTypes.person}, + {text: p2.metadata.caption, type: SearchQueryTypes.caption}] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + query = { + type: SearchQueryTypes.AND, + list: [{text: p.metadata.faces[0].name, type: SearchQueryTypes.person}, + {text: p.metadata.caption, type: SearchQueryTypes.caption}] + }; + expect(await sm.aSearch(query)).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + // make sure that this shows both photos. We need this the the rest of the tests + query = {text: 'a', type: SearchQueryTypes.person}; + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2], + metaFile: [], + resultOverflow: false + })); + + query = { + type: SearchQueryTypes.AND, + list: [{ + type: SearchQueryTypes.AND, + list: [{text: 'a', type: SearchQueryTypes.person}, + {text: p.metadata.keywords[0], type: SearchQueryTypes.keyword}] + }, + {text: p.metadata.caption, type: SearchQueryTypes.caption} + ] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + }); + + it('should OR', async () => { + const sm = new SearchManager(); + + let query: SearchQueryDTO = { + type: SearchQueryTypes.OR, + list: [{text: 'Not a person', type: SearchQueryTypes.person}, + {text: 'Not a caption', type: SearchQueryTypes.caption}] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + query = { + type: SearchQueryTypes.OR, + list: [{text: p.metadata.faces[0].name, type: SearchQueryTypes.person}, + {text: p2.metadata.caption, type: SearchQueryTypes.caption}] + }; + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2], + metaFile: [], + resultOverflow: false + })); + + query = { + type: SearchQueryTypes.OR, + list: [{text: p.metadata.faces[0].name, type: SearchQueryTypes.person}, + {text: p.metadata.caption, type: SearchQueryTypes.caption}] + }; + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + // make sure that this shows both photos. We need this the the rest of the tests + query = {text: 'a', type: SearchQueryTypes.person}; + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2], + metaFile: [], + resultOverflow: false + })); + + query = { + type: SearchQueryTypes.OR, + list: [{ + type: SearchQueryTypes.OR, + list: [{text: 'a', type: SearchQueryTypes.person}, + {text: p.metadata.keywords[0], type: SearchQueryTypes.keyword}] + }, + {text: p.metadata.caption, type: SearchQueryTypes.caption} + ] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2], + metaFile: [], + resultOverflow: false + })); + + + query = { + type: SearchQueryTypes.OR, + list: [{ + type: SearchQueryTypes.OR, + list: [{text: p.metadata.keywords[0], type: SearchQueryTypes.keyword}, + {text: p2.metadata.keywords[0], type: SearchQueryTypes.keyword}] + }, + {text: p_faceLess.metadata.caption, type: SearchQueryTypes.caption} + ] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + }); + + describe('should search text', async () => { + it('as any', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({text: 'sw', type: SearchQueryTypes.any_text}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess, v, p4], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({text: 'Boba', type: SearchQueryTypes.any_text}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'Boba', + type: SearchQueryTypes.any_text, + matchType: TextSearchQueryTypes.exact_match + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'Boba Fett', + type: SearchQueryTypes.any_text, + matchType: TextSearchQueryTypes.exact_match + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + }); + + it('as position', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({text: 'Tatooine', type: SearchQueryTypes.position}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + }); + + + it('as keyword', async () => { + const sm = new SearchManager(); + + + expect(Utils.clone(await sm.aSearch({ + text: 'kie', + type: SearchQueryTypes.keyword + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'wa', + type: SearchQueryTypes.keyword + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess, p4], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'han', + type: SearchQueryTypes.keyword + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + }); + + + it('as caption', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({ + text: 'han', + type: SearchQueryTypes.caption + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + }); + + it('as file_name', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({ + text: 'sw', + type: SearchQueryTypes.file_name + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess, v, p4], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'sw4', + type: SearchQueryTypes.file_name + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p4], + metaFile: [], + resultOverflow: false + })); + + }); + + it('as directory', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({ + text: 'of the J', + type: SearchQueryTypes.directory + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p4], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'wars dir', + type: SearchQueryTypes.directory + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, v, p_faceLess, p4], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: '/wars dir', + matchType: TextSearchQueryTypes.exact_match, + type: SearchQueryTypes.directory + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, v], + metaFile: [], + resultOverflow: false + })); + + + expect(Utils.clone(await sm.aSearch({ + text: '/wars dir/Return of the Jedi', + matchType: TextSearchQueryTypes.exact_match, + type: SearchQueryTypes.directory + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p4], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: '/wars dir/Return of the Jedi', + matchType: TextSearchQueryTypes.exact_match, + type: SearchQueryTypes.directory + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p4], + metaFile: [], + resultOverflow: false + })); + + + }); + + it('as person', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({ + text: 'Boba', + type: SearchQueryTypes.person + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'Boba', + type: SearchQueryTypes.person, + matchType: TextSearchQueryTypes.exact_match + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + text: 'Boba Fett', + type: SearchQueryTypes.person, + matchType: TextSearchQueryTypes.exact_match + }))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + }); + + }); + + + it('should search date', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({before: 0, after: 0, type: SearchQueryTypes.date}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + before: p.metadata.creationDate, + after: p.metadata.creationDate, type: SearchQueryTypes.date + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + before: p.metadata.creationDate + 1000000000, + after: 0, type: SearchQueryTypes.date + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess, v, p4], + metaFile: [], + resultOverflow: false + })); + + }); + + + it('should search rating', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({min: 0, max: 0, type: SearchQueryTypes.rating}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({min: 0, max: 5, type: SearchQueryTypes.rating}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({min: 2, max: 2, type: SearchQueryTypes.rating}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p2], + metaFile: [], + resultOverflow: false + })); + + }); + + + it('should search resolution', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({min: 0, max: 0, type: SearchQueryTypes.resolution}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({max: 1, type: SearchQueryTypes.resolution}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, v], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({min: 2, max: 3, type: SearchQueryTypes.resolution}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({min: 3, type: SearchQueryTypes.resolution}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p4], + metaFile: [], + resultOverflow: false + })); + + }); + + + it('should search orientation', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.aSearch({ + orientation: OrientationSearchTypes.portrait, + type: SearchQueryTypes.orientation + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p4, v], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + orientation: OrientationSearchTypes.landscape, + type: SearchQueryTypes.orientation + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p_faceLess, v], + metaFile: [], + resultOverflow: false + })); + + + }); + + it('should search distance', async () => { + ObjectManagers.getInstance().LocationManager = new LocationManager(); + const sm = new SearchManager(); + + ObjectManagers.getInstance().LocationManager.getGPSData = async (): Promise => { + return { + longitude: 10, + latitude: 10, + altitude: 0 + }; + }; + expect(Utils.clone(await sm.aSearch({ + from: {text: 'Tatooine'}, + distance: 1, + type: SearchQueryTypes.distance + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + from: {GPSData: {latitude: 0, longitude: 0}}, + distance: 112 * 10, // number of km per degree = ~111km + type: SearchQueryTypes.distance + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + from: {GPSData: {latitude: 10, longitude: 10}}, + distance: 1, + type: SearchQueryTypes.distance + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.aSearch({ + from: {GPSData: {latitude: 10, longitude: 10}}, + distance: 112 * 5, // number of km per degree = ~111km + type: SearchQueryTypes.distance + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p_faceLess, p4], + metaFile: [], + resultOverflow: false + })); + + }); }); + /* + it('should instant search', async () => { + const sm = new SearchManager(); + + expect(Utils.clone(await sm.instantSearch('sw'))).to.deep.equalInAnyOrder(Utils.clone({ + searchText: 'sw', + directories: [], + media: [p, p2, v], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.instantSearch('Tatooine'))).to.deep.equalInAnyOrder(Utils.clone({ + searchText: 'Tatooine', + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + + expect(Utils.clone(await sm.instantSearch('ortm'))).to.deep.equalInAnyOrder(Utils.clone({ + searchText: 'ortm', + directories: [], + media: [p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); - it('should search', async () => { - const sm = new SearchManager(); - - - expect(Utils.clone(await sm.search('sw', null))).to.deep.equal(Utils.clone({ - searchText: 'sw', - searchType: null, - directories: [], - media: [p, p2, v], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('Boba', null))).to.deep.equal(Utils.clone({ - searchText: 'Boba', - searchType: null, - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('Tatooine', SearchTypes.position))).to.deep.equal(Utils.clone({ - searchText: 'Tatooine', - searchType: SearchTypes.position, - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('ortm', SearchTypes.keyword))).to.deep.equal(Utils.clone({ - searchText: 'ortm', - searchType: SearchTypes.keyword, - directories: [], - media: [p2, p_faceLess], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('ortm', SearchTypes.keyword))).to.deep.equal(Utils.clone({ - searchText: 'ortm', - searchType: SearchTypes.keyword, - directories: [], - media: [p2, p_faceLess], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('wa', SearchTypes.keyword))).to.deep.equal(Utils.clone({ - searchText: 'wa', - searchType: SearchTypes.keyword, - directories: [dir], - media: [p, p2, p_faceLess], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('han', SearchTypes.photo))).to.deep.equal(Utils.clone({ - searchText: 'han', - searchType: SearchTypes.photo, - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('sw', SearchTypes.video))).to.deep.equal(Utils.clone({ - searchText: 'sw', - searchType: SearchTypes.video, - directories: [], - media: [v], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('han', SearchTypes.keyword))).to.deep.equal(Utils.clone({ - searchText: 'han', - searchType: SearchTypes.keyword, - directories: [], - media: [], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.search('Boba', SearchTypes.person))).to.deep.equal(Utils.clone({ - searchText: 'Boba', - searchType: SearchTypes.person, - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - }); - - - it('should instant search', async () => { - const sm = new SearchManager(); - - expect(Utils.clone(await sm.instantSearch('sw'))).to.deep.equal(Utils.clone({ - searchText: 'sw', - directories: [], - media: [p, p2, v], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.instantSearch('Tatooine'))).to.deep.equal(Utils.clone({ - searchText: 'Tatooine', - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.instantSearch('ortm'))).to.deep.equal(Utils.clone({ - searchText: 'ortm', - directories: [], - media: [p2, p_faceLess], - metaFile: [], - resultOverflow: false - })); - - - expect(Utils.clone(await sm.instantSearch('wa'))).to.deep.equal(Utils.clone({ - searchText: 'wa', - directories: [dir], - media: [p, p2, p_faceLess], - metaFile: [], - resultOverflow: false - })); - - expect(Utils.clone(await sm.instantSearch('han'))).to.deep.equal(Utils.clone({ - searchText: 'han', - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - expect(Utils.clone(await sm.instantSearch('Boba'))).to.deep.equal(Utils.clone({ - searchText: 'Boba', - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - })); - }); + expect(Utils.clone(await sm.instantSearch('wa'))).to.deep.equalInAnyOrder(Utils.clone({ + searchText: 'wa', + directories: [dir], + media: [p, p2, p_faceLess], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.instantSearch('han'))).to.deep.equalInAnyOrder(Utils.clone({ + searchText: 'han', + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.instantSearch('Boba'))).to.deep.equalInAnyOrder(Utils.clone({ + searchText: 'Boba', + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + })); + }); + */ }); diff --git a/test/backend/unit/model/sql/TestHelper.ts b/test/backend/unit/model/sql/TestHelper.ts index 136bd47c..8decc91d 100644 --- a/test/backend/unit/model/sql/TestHelper.ts +++ b/test/backend/unit/model/sql/TestHelper.ts @@ -24,19 +24,26 @@ import {DiskMangerWorker} from '../../../../../src/backend/model/threading/DiskM export class TestHelper { - public static getDirectoryEntry() { + public static getDirectoryEntry(parent: DirectoryDTO = null, name = 'wars dir') { const dir = new DirectoryEntity(); - dir.name = 'wars dir'; - dir.path = '.'; + dir.name = name; + dir.path = DiskMangerWorker.pathFromParent({path: '', name: '.'}); dir.mediaCount = 0; + dir.directories = []; + dir.metaFile = []; + dir.media = []; dir.lastModified = Date.now(); - dir.lastScanned = null; - + dir.lastScanned = Date.now(); + // dir.parent = null; + if (parent !== null) { + dir.path = DiskMangerWorker.pathFromParent(parent); + parent.directories.push(dir); + } return dir; } - public static getPhotoEntry(dir: DirectoryEntity) { + public static getPhotoEntry(dir: DirectoryDTO) { const sd = new MediaDimensionEntity(); sd.height = 200; sd.width = 200; @@ -66,7 +73,7 @@ export class TestHelper { m.creationDate = Date.now(); m.fileSize = 123456789; m.orientation = OrientationTypes.TOP_LEFT; - m.rating = 2; + // m.rating = 0; no rating by default // TODO: remove when typeorm is fixed m.duration = null; @@ -75,13 +82,13 @@ export class TestHelper { const d = new PhotoEntity(); d.name = 'test media.jpg'; - d.directory = dir; + dir.media.push(d); d.metadata = m; dir.mediaCount++; return d; } - public static getVideoEntry(dir: DirectoryEntity) { + public static getVideoEntry(dir: DirectoryDTO) { const sd = new MediaDimensionEntity(); sd.height = 200; sd.width = 200; @@ -99,20 +106,32 @@ export class TestHelper { const d = new VideoEntity(); - d.name = 'test video.jpg'; - d.directory = dir; + d.name = 'test video.mp4'; + dir.media.push(d); d.metadata = m; return d; } - public static getPhotoEntry1(dir: DirectoryEntity) { + public static getVideoEntry1(dir: DirectoryDTO) { + const p = TestHelper.getVideoEntry(dir); + p.name = 'swVideo.mp4'; + return p; + } + + public static getPhotoEntry1(dir: DirectoryDTO) { const p = TestHelper.getPhotoEntry(dir); p.metadata.caption = 'Han Solo\'s dice'; p.metadata.keywords = ['Boba Fett', 'star wars', 'Anakin', 'death star']; p.metadata.positionData.city = 'Mos Eisley'; p.metadata.positionData.country = 'Tatooine'; - p.name = 'sw1'; + p.name = 'sw1.jpg'; + p.metadata.positionData.GPSData.latitude = 10; + p.metadata.positionData.GPSData.longitude = 10; + p.metadata.creationDate = Date.now() - 1000; + p.metadata.rating = 1; + p.metadata.size.height = 1000; + p.metadata.size.width = 1000; p.metadata.faces = [{ box: {height: 10, width: 10, left: 10, top: 10}, @@ -133,20 +152,23 @@ export class TestHelper { return p; } - public static getVideoEntry1(dir: DirectoryEntity) { - const p = TestHelper.getVideoEntry(dir); - p.name = 'swVideo'; - return p; - } - public static getPhotoEntry2(dir: DirectoryEntity) { + public static getPhotoEntry2(dir: DirectoryDTO) { const p = TestHelper.getPhotoEntry(dir); - p.metadata.keywords = ['Padmé Amidala', 'star wars', 'Natalie Portman', 'death star']; + p.metadata.caption = 'Light saber'; + p.metadata.keywords = ['Padmé Amidala', 'star wars', 'Natalie Portman', 'death star', 'wookiee']; p.metadata.positionData.city = 'Derem City'; p.metadata.positionData.state = 'Research City'; p.metadata.positionData.country = 'Kamino'; - p.name = 'sw2'; + p.name = 'sw2.jpg'; + p.metadata.positionData.GPSData.latitude = -10; + p.metadata.positionData.GPSData.longitude = -10; + p.metadata.creationDate = Date.now() - 2000; + p.metadata.rating = 2; + p.metadata.size.height = 2000; + p.metadata.size.width = 1000; + p.metadata.faces = [{ box: {height: 10, width: 10, left: 10, top: 10}, name: 'Padmé Amidala' @@ -160,6 +182,61 @@ export class TestHelper { return p; } + public static getPhotoEntry3(dir: DirectoryDTO) { + const p = TestHelper.getPhotoEntry(dir); + + p.metadata.caption = 'Amber stone'; + p.metadata.keywords = ['star wars', 'wookiees']; + p.metadata.positionData.city = 'Castilon'; + p.metadata.positionData.state = 'Devaron'; + p.metadata.positionData.country = 'Ajan Kloss'; + p.name = 'sw3.jpg'; + p.metadata.positionData.GPSData.latitude = 10; + p.metadata.positionData.GPSData.longitude = 15; + p.metadata.creationDate = Date.now() - 3000; + p.metadata.rating = 3; + p.metadata.size.height = 1000; + p.metadata.size.width = 2000; + p.metadata.faces = [{ + box: {height: 10, width: 10, left: 10, top: 10}, + name: 'Kylo Ren' + }, { + box: {height: 10, width: 10, left: 101, top: 101}, + name: 'Leia Organa' + }, { + box: {height: 10, width: 10, left: 103, top: 103}, + name: 'Han Solo' + }] as any[]; + return p; + } + + public static getPhotoEntry4(dir: DirectoryDTO) { + const p = TestHelper.getPhotoEntry(dir); + + p.metadata.caption = 'Millennium falcon'; + p.metadata.keywords = ['star wars', 'ewoks']; + p.metadata.positionData.city = 'Tipoca City'; + p.metadata.positionData.state = 'Exegol'; + p.metadata.positionData.country = 'Jedha'; + p.name = 'sw4.jpg'; + p.metadata.positionData.GPSData.latitude = 15; + p.metadata.positionData.GPSData.longitude = 10; + p.metadata.creationDate = Date.now() - 4000; + p.metadata.size.height = 3000; + p.metadata.size.width = 2000; + p.metadata.faces = [{ + box: {height: 10, width: 10, left: 10, top: 10}, + name: 'Kylo Ren' + }, { + box: {height: 10, width: 10, left: 101, top: 101}, + name: 'Anakin Skywalker' + }, { + box: {height: 10, width: 10, left: 101, top: 101}, + name: 'Obivan Kenobi' + }] as any[]; + return p; + } + public static getRandomizedDirectoryEntry(parent: DirectoryDTO = null, forceStr: string = null) {