From 0e460d07af0335e1f8e871c68302130b0510b3bc Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Tue, 1 Aug 2023 22:57:36 +0200 Subject: [PATCH] Add person count search and sorting support. Note it will trigger a DB drop as the scheme changed. #683 --- src/backend/model/database/IndexingManager.ts | 1 + src/backend/model/database/SearchManager.ts | 56 ++++++++++++- .../model/database/enitites/MediaEntity.ts | 72 ++++++++--------- src/backend/model/jobs/jobs/TopPickSendJob.ts | 4 +- src/common/DataStructureVersion.ts | 2 +- src/common/PG2ConfMap.ts | 2 + src/common/SearchQueryParser.ts | 78 +++++++++--------- src/common/config/private/PrivateConfig.ts | 1 + src/common/entities/SearchQueryDTO.ts | 19 ++++- src/common/entities/SortingMethods.ts | 20 +++-- .../app/pipes/IconizeSortingMethod.ts | 4 + src/frontend/app/ui/EnumTranslations.ts | 2 + .../ui/gallery/navigator/sorting.service.ts | 12 +++ .../ui/gallery/search/autocomplete.service.ts | 45 +++++++---- .../search/search-query-parser.service.ts | 4 +- .../unit/model/sql/SearchManager.spec.ts | 79 +++++++++++++++++++ test/common/unit/SearchQueryParser.ts | 9 ++- 17 files changed, 305 insertions(+), 105 deletions(-) diff --git a/src/backend/model/database/IndexingManager.ts b/src/backend/model/database/IndexingManager.ts index df26b1d9..791dde97 100644 --- a/src/backend/model/database/IndexingManager.ts +++ b/src/backend/model/database/IndexingManager.ts @@ -367,6 +367,7 @@ export class IndexingManager { ), ]; } + (media[i].metadata as PhotoMetadataEntity).personsLength = (media[i].metadata as PhotoMetadataEntity)?.persons?.length || 0; if (mediaItem == null) { diff --git a/src/backend/model/database/SearchManager.ts b/src/backend/model/database/SearchManager.ts index 5e60b33c..61249bf1 100644 --- a/src/backend/model/database/SearchManager.ts +++ b/src/backend/model/database/SearchManager.ts @@ -13,9 +13,9 @@ import { DatePatternFrequency, DatePatternSearch, DistanceSearch, - FromDateSearch, + FromDateSearch, MaxPersonCountSearch, MaxRatingSearch, - MaxResolutionSearch, + MaxResolutionSearch, MinPersonCountSearch, MinRatingSearch, MinResolutionSearch, OrientationSearch, @@ -379,6 +379,12 @@ export class SearchManager { case SortingMethods.ascName: query.addOrderBy('media.name', 'ASC'); break; + case SortingMethods.descPersonCount: + query.addOrderBy('media.metadata.personsLength', 'DESC'); + break; + case SortingMethods.ascPersonCount: + query.addOrderBy('media.metadata.personsLength', 'ASC'); + break; case SortingMethods.random: if (Config.Database.type === DatabaseType.mysql) { query.groupBy('RAND(), media.id'); @@ -639,6 +645,52 @@ export class SearchManager { return q; }); + case SearchQueryTypes.min_person_count: + if (directoryOnly) { + throw new Error('not supported in directoryOnly mode'); + } + return new Brackets((q): unknown => { + if (typeof (query as MinPersonCountSearch).value === 'undefined') { + throw new Error( + 'Invalid search query: Person count Query should contain minvalue' + ); + } + + const relation = (query as TextSearch).negate ? '<' : '>='; + + const textParam: { [key: string]: unknown } = {}; + textParam['min' + queryId] = (query as MinPersonCountSearch).value; + q.where( + `media.metadata.personsLength ${relation} :min${queryId}`, + textParam + ); + + return q; + }); + case SearchQueryTypes.max_person_count: + if (directoryOnly) { + throw new Error('not supported in directoryOnly mode'); + } + return new Brackets((q): unknown => { + if (typeof (query as MaxPersonCountSearch).value === 'undefined') { + throw new Error( + 'Invalid search query: Person count Query should contain max value' + ); + } + + const relation = (query as TextSearch).negate ? '>' : '<='; + + if (typeof (query as MaxRatingSearch).value !== 'undefined') { + const textParam: { [key: string]: unknown } = {}; + textParam['max' + queryId] = (query as MaxPersonCountSearch).value; + q.where( + `media.metadata.personsLength ${relation} :max${queryId}`, + textParam + ); + } + return q; + }); + case SearchQueryTypes.min_resolution: if (directoryOnly) { throw new Error('not supported in directoryOnly mode'); diff --git a/src/backend/model/database/enitites/MediaEntity.ts b/src/backend/model/database/enitites/MediaEntity.ts index 46d0e317..fb5495e7 100644 --- a/src/backend/model/database/enitites/MediaEntity.ts +++ b/src/backend/model/database/enitites/MediaEntity.ts @@ -1,26 +1,9 @@ -import { - Column, - Entity, - Index, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - TableInheritance, - Unique, -} from 'typeorm'; -import { DirectoryEntity } from './DirectoryEntity'; -import { - MediaDimension, - MediaDTO, - MediaMetadata, -} from '../../../../common/entities/MediaDTO'; -import { PersonJunctionTable} from './PersonJunctionTable'; -import { columnCharsetCS } from './EntityUtils'; -import { - CameraMetadata, FaceRegion, - GPSMetadata, - PositionMetaData, -} from '../../../../common/entities/PhotoDTO'; +import {Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance, Unique,} from 'typeorm'; +import {DirectoryEntity} from './DirectoryEntity'; +import {MediaDimension, MediaDTO, MediaMetadata,} from '../../../../common/entities/MediaDTO'; +import {PersonJunctionTable} from './PersonJunctionTable'; +import {columnCharsetCS} from './EntityUtils'; +import {CameraMetadata, FaceRegion, GPSMetadata, PositionMetaData,} from '../../../../common/entities/PhotoDTO'; export class MediaDimensionEntity implements MediaDimension { @Column('int') @@ -31,7 +14,7 @@ export class MediaDimensionEntity implements MediaDimension { } export class CameraMetadataEntity implements CameraMetadata { - @Column('int', { nullable: true, unsigned: true }) + @Column('int', {nullable: true, unsigned: true}) ISO: number; @Column({ @@ -50,23 +33,23 @@ export class CameraMetadataEntity implements CameraMetadata { }) make: string; - @Column('float', { nullable: true }) + @Column('float', {nullable: true}) fStop: number; - @Column('float', { nullable: true }) + @Column('float', {nullable: true}) exposure: number; - @Column('float', { nullable: true }) + @Column('float', {nullable: true}) focalLength: number; - @Column('text', { nullable: true }) + @Column('text', {nullable: true}) lens: string; } export class GPSMetadataEntity implements GPSMetadata { - @Column('float', { nullable: true }) + @Column('float', {nullable: true}) latitude: number; - @Column('float', { nullable: true }) + @Column('float', {nullable: true}) longitude: number; } @@ -120,7 +103,7 @@ export class MediaMetadataEntity implements MediaMetadata { @Index() creationDate: number; - @Column('int', { unsigned: true }) + @Column('int', {unsigned: true}) fileSize: number; @Column({ @@ -136,7 +119,7 @@ export class MediaMetadataEntity implements MediaMetadata { @Column((type) => PositionMetaDataEntity) positionData: PositionMetaDataEntity; - @Column('tinyint', { unsigned: true }) + @Column('tinyint', {unsigned: true}) @Index() rating: 0 | 1 | 2 | 3 | 4 | 5; @@ -144,10 +127,11 @@ export class MediaMetadataEntity implements MediaMetadata { personJunction: PersonJunctionTable[]; @Column({ - type:'simple-json', + type: 'simple-json', nullable: true, charset: columnCharsetCS.charset, - collation: columnCharsetCS.collation}) + collation: columnCharsetCS.collation + }) faces: FaceRegion[]; /** @@ -162,20 +146,32 @@ export class MediaMetadataEntity implements MediaMetadata { }) persons: string[]; - @Column('int', { unsigned: true }) + /** + * Caches the list of persons' length. Only used for searching + */ + @Column({ + type: 'tinyint', + select: false, + nullable: false, + default: 0 + }) + personsLength: number; + + + @Column('int', {unsigned: true}) bitRate: number; - @Column('int', { unsigned: true }) + @Column('int', {unsigned: true}) duration: number; } // TODO: fix inheritance once its working in typeorm @Entity() @Unique(['name', 'directory']) -@TableInheritance({ column: { type: 'varchar', name: 'type', length: 16 } }) +@TableInheritance({column: {type: 'varchar', name: 'type', length: 16}}) export abstract class MediaEntity implements MediaDTO { @Index() - @PrimaryGeneratedColumn({ unsigned: true }) + @PrimaryGeneratedColumn({unsigned: true}) id: number; @Column(columnCharsetCS) diff --git a/src/backend/model/jobs/jobs/TopPickSendJob.ts b/src/backend/model/jobs/jobs/TopPickSendJob.ts index 4e1ff8da..bd47a567 100644 --- a/src/backend/model/jobs/jobs/TopPickSendJob.ts +++ b/src/backend/model/jobs/jobs/TopPickSendJob.ts @@ -35,7 +35,7 @@ export class TopPickSendJob extends Job<{ type: 'sort-array', name: backendTexts.sortBy.name, description: backendTexts.sortBy.description, - defaultValue: [SortingMethods.descRating], + defaultValue: [SortingMethods.descRating, SortingMethods.descPersonCount], }, { id: 'pickAmount', type: 'number', @@ -48,7 +48,7 @@ export class TopPickSendJob extends Job<{ name: backendTexts.emailTo.name, description: backendTexts.emailTo.description, defaultValue: [], - }, { + }, { id: 'emailSubject', type: 'string', name: backendTexts.emailSubject.name, diff --git a/src/common/DataStructureVersion.ts b/src/common/DataStructureVersion.ts index 15f48ded..472faefa 100644 --- a/src/common/DataStructureVersion.ts +++ b/src/common/DataStructureVersion.ts @@ -1,4 +1,4 @@ /** * This version indicates that the sql/entities/*Entity.ts files got changed and the db needs to be recreated */ -export const DataStructureVersion = 30; +export const DataStructureVersion = 31; diff --git a/src/common/PG2ConfMap.ts b/src/common/PG2ConfMap.ts index 8a503516..54b63696 100644 --- a/src/common/PG2ConfMap.ts +++ b/src/common/PG2ConfMap.ts @@ -13,6 +13,8 @@ export const PG2ConfMap = { '.order_descending_rating.pg2conf': SortingMethods.descRating, '.order_ascending_rating.pg2conf': SortingMethods.ascRating, '.order_random.pg2conf': SortingMethods.random, + '.order_descending_person_count.pg2conf': SortingMethods.descPersonCount, + '.order_ascending_person_count.pg2conf': SortingMethods.descPersonCount, }, }; diff --git a/src/common/SearchQueryParser.ts b/src/common/SearchQueryParser.ts index 225e32a8..ef76f9ae 100644 --- a/src/common/SearchQueryParser.ts +++ b/src/common/SearchQueryParser.ts @@ -4,10 +4,6 @@ import { DatePatternSearch, DistanceSearch, FromDateSearch, - MaxRatingSearch, - MaxResolutionSearch, - MinRatingSearch, - MinResolutionSearch, NegatableSearchQuery, OrientationSearch, ORSearchQuery, @@ -41,6 +37,8 @@ export interface QueryKeywords { minResolution: string; maxRating: string; minRating: string; + maxPersonCount: string; + minPersonCount: string; NSomeOf: string; someOf: string; or: string; @@ -65,8 +63,10 @@ export const defaultQueryKeywords: QueryKeywords = { to: 'before', maxRating: 'max-rating', - maxResolution: 'max-resolution', minRating: 'min-rating', + maxPersonCount: 'max-persons', + minPersonCount: 'min-persons', + maxResolution: 'max-resolution', minResolution: 'min-resolution', kmFrom: 'km-from', @@ -303,38 +303,28 @@ export class SearchQueryParser { } as ToDateSearch; } - if (kwStartsWith(str, this.keywords.minRating)) { - return { - type: SearchQueryTypes.min_rating, - value: parseInt(str.substring(str.indexOf(':') + 1), 10), - ...(str.startsWith(this.keywords.minRating + '!:') && {negate: true}), // only add if the value is true - } as MinRatingSearch; - } - if (kwStartsWith(str, this.keywords.maxRating)) { - return { - type: SearchQueryTypes.max_rating, - value: parseInt(str.substring(str.indexOf(':') + 1), 10), - ...(str.startsWith(this.keywords.maxRating + '!:') && {negate: true}), // only add if the value is true - } as MaxRatingSearch; - } - if (kwStartsWith(str, this.keywords.minResolution)) { - return { - type: SearchQueryTypes.min_resolution, - value: parseInt(str.substring(str.indexOf(':') + 1), 10), - ...(str.startsWith(this.keywords.minResolution + '!:') && { - negate: true, - }), // only add if the value is true - } as MinResolutionSearch; - } - if (kwStartsWith(str, this.keywords.maxResolution)) { - return { - type: SearchQueryTypes.max_resolution, - value: parseInt(str.substring(str.indexOf(':') + 1), 10), - ...(str.startsWith(this.keywords.maxResolution + '!:') && { - negate: true, - }), // only add if the value is true - } as MaxResolutionSearch; + const addValueRangeParser = (matcher: string, type: SearchQueryTypes): RangeSearch | undefined => { + if (kwStartsWith(str, matcher)) { + return { + type: type, + value: parseInt(str.substring(str.indexOf(':') + 1), 10), + ...(str.startsWith(matcher + '!:') && {negate: true}), // only add if the value is true + } as RangeSearch; + } + }; + + const range = addValueRangeParser(this.keywords.minRating, SearchQueryTypes.min_rating) || + addValueRangeParser(this.keywords.maxRating, SearchQueryTypes.max_rating) || + addValueRangeParser(this.keywords.minResolution, SearchQueryTypes.min_resolution) || + addValueRangeParser(this.keywords.maxResolution, SearchQueryTypes.max_resolution) || + addValueRangeParser(this.keywords.minPersonCount, SearchQueryTypes.min_person_count) || + addValueRangeParser(this.keywords.maxPersonCount, SearchQueryTypes.max_person_count); + + if (range) { + return range; } + + if (new RegExp('^\\d*-' + this.keywords.kmFrom + '!?:').test(str)) { let from = str.slice( new RegExp('^\\d*-' + this.keywords.kmFrom + '!?:').exec(str)[0].length @@ -528,6 +518,22 @@ export class SearchQueryParser { ? '' : (query as RangeSearch).value) ); + case SearchQueryTypes.min_person_count: + return ( + this.keywords.minPersonCount + + colon + + (isNaN((query as RangeSearch).value) + ? '' + : (query as RangeSearch).value) + ); + case SearchQueryTypes.max_person_count: + return ( + this.keywords.maxPersonCount + + colon + + (isNaN((query as RangeSearch).value) + ? '' + : (query as RangeSearch).value) + ); case SearchQueryTypes.min_resolution: return ( this.keywords.minResolution + diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index e9b60a19..8c8aa6e5 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -898,6 +898,7 @@ export class ServerPreviewConfig { Sorting: SortingMethods[] = [ SortingMethods.descRating, SortingMethods.descDate, + SortingMethods.descPersonCount, ]; } diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts index f08fb51b..18a61f15 100644 --- a/src/common/entities/SearchQueryDTO.ts +++ b/src/common/entities/SearchQueryDTO.ts @@ -14,10 +14,14 @@ export enum SearchQueryTypes { max_rating, min_resolution, max_resolution, + min_person_count, + max_person_count, - distance, + distance = 50, orientation, - date_pattern, + + + date_pattern = 60, // TEXT search types any_text = 100, @@ -211,6 +215,17 @@ export interface MaxRatingSearch extends RangeSearch { value: number; } + +export interface MinPersonCountSearch extends RangeSearch { + type: SearchQueryTypes.min_person_count; + value: number; +} + +export interface MaxPersonCountSearch extends RangeSearch { + type: SearchQueryTypes.max_person_count; + value: number; +} + export interface MinResolutionSearch extends RangeSearch { type: SearchQueryTypes.min_resolution; value: number; // in megapixels diff --git a/src/common/entities/SortingMethods.ts b/src/common/entities/SortingMethods.ts index f5533b3d..5e4c49b1 100644 --- a/src/common/entities/SortingMethods.ts +++ b/src/common/entities/SortingMethods.ts @@ -1,9 +1,15 @@ +/** + * Order of these enums determines the order in the UI. + * Keep spaces between the values, so new value can be added in between without changing the existing ones + */ export enum SortingMethods { - ascName = 1, - descName, - ascDate, - descDate, - ascRating, - descRating, - random, + ascName = 10, + descName = 11, + ascDate = 20, + descDate = 21, + ascRating = 30, + descRating = 31, + ascPersonCount = 40, + descPersonCount = 41, + random = 100 // let's keep random as the last in the UI } diff --git a/src/frontend/app/pipes/IconizeSortingMethod.ts b/src/frontend/app/pipes/IconizeSortingMethod.ts index 5f5f3c8b..d9d5c4dc 100644 --- a/src/frontend/app/pipes/IconizeSortingMethod.ts +++ b/src/frontend/app/pipes/IconizeSortingMethod.ts @@ -9,6 +9,10 @@ export class IconizeSortingMethod implements PipeTransform { return ''; case SortingMethods.descRating: return ''; + case SortingMethods.ascPersonCount: + return ''; + case SortingMethods.descPersonCount: + return ''; case SortingMethods.ascName: return 'A'; case SortingMethods.descName: diff --git a/src/frontend/app/ui/EnumTranslations.ts b/src/frontend/app/ui/EnumTranslations.ts index f4da98d1..17c6c89d 100644 --- a/src/frontend/app/ui/EnumTranslations.ts +++ b/src/frontend/app/ui/EnumTranslations.ts @@ -43,6 +43,8 @@ EnumTranslations[SortingMethods[SortingMethods.ascName]] = $localize`ascending n EnumTranslations[SortingMethods[SortingMethods.descRating]] = $localize`descending rating`; EnumTranslations[SortingMethods[SortingMethods.ascRating]] = $localize`ascending rating`; EnumTranslations[SortingMethods[SortingMethods.random]] = $localize`random`; +EnumTranslations[SortingMethods[SortingMethods.ascPersonCount]] = $localize`ascending persons`; +EnumTranslations[SortingMethods[SortingMethods.descPersonCount]] = $localize`descending persons`; EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.url]] = $localize`Url`; diff --git a/src/frontend/app/ui/gallery/navigator/sorting.service.ts b/src/frontend/app/ui/gallery/navigator/sorting.service.ts index 3c08b601..f5362805 100644 --- a/src/frontend/app/ui/gallery/navigator/sorting.service.ts +++ b/src/frontend/app/ui/gallery/navigator/sorting.service.ts @@ -179,6 +179,18 @@ export class GallerySortingService { (b.metadata.rating || 0) - (a.metadata.rating || 0) ); break; + case SortingMethods.ascPersonCount: + c.media.sort( + (a: PhotoDTO, b: PhotoDTO) => + (a.metadata?.faces?.length || 0) - (b.metadata?.faces?.length || 0) + ); + break; + case SortingMethods.descPersonCount: + c.media.sort( + (a: PhotoDTO, b: PhotoDTO) => + (b.metadata?.faces?.length || 0) - (a.metadata?.faces?.length || 0) + ); + break; case SortingMethods.random: this.rndService.setSeed(c.media.length); c.media diff --git a/src/frontend/app/ui/gallery/search/autocomplete.service.ts b/src/frontend/app/ui/gallery/search/autocomplete.service.ts index 266469b6..8a9431cf 100644 --- a/src/frontend/app/ui/gallery/search/autocomplete.service.ts +++ b/src/frontend/app/ui/gallery/search/autocomplete.service.ts @@ -29,6 +29,8 @@ export class AutoCompleteService { k !== this.searchQueryParserService.keywords.NSomeOf && k !== this.searchQueryParserService.keywords.minRating && k !== this.searchQueryParserService.keywords.maxRating && + k !== this.searchQueryParserService.keywords.minPersonCount && + k !== this.searchQueryParserService.keywords.maxPersonCount && k !== this.searchQueryParserService.keywords.every_week && k !== this.searchQueryParserService.keywords.every_month && k !== this.searchQueryParserService.keywords.every_year && @@ -91,6 +93,11 @@ export class AutoCompleteService { this.noACKeywordsMap[this.searchQueryParserService.keywords.maxRating] = SearchQueryTypes.max_rating; + this.noACKeywordsMap[this.searchQueryParserService.keywords.minPersonCount] + = SearchQueryTypes.min_person_count; + this.noACKeywordsMap[this.searchQueryParserService.keywords.maxPersonCount] + = SearchQueryTypes.max_person_count; + this.noACKeywordsMap[this.searchQueryParserService.keywords.minResolution] = SearchQueryTypes.min_resolution; this.noACKeywordsMap[this.searchQueryParserService.keywords.maxResolution] @@ -321,25 +328,33 @@ export class AutoCompleteService { } } - // only showing rating recommendations of the full query is typed - const mrKey = this.searchQueryParserService.keywords.minRating + ':'; - const mxrKey = this.searchQueryParserService.keywords.maxRating + ':'; - if (text.current.toLowerCase().startsWith(mrKey)) { - for (let i = 1; i <= 5; ++i) { - ret.push(generateMatch(mrKey + i)); + + const addRangeAutoComp = (minStr: string, maxStr: string, minRange: number, maxRange: number) => { + // only showing rating recommendations of the full query is typed + const mrKey = minStr + ':'; + const mxrKey = maxStr + ':'; + if (text.current.toLowerCase().startsWith(mrKey)) { + for (let i = minRange; i <= maxRange; ++i) { + ret.push(generateMatch(mrKey + i)); + } + } else if (mrKey.startsWith(text.current.toLowerCase())) { + ret.push(generateMatch(mrKey)); } - } else if (mrKey.startsWith(text.current.toLowerCase())) { - ret.push(generateMatch(mrKey)); - } - if (text.current.toLowerCase().startsWith(mxrKey)) { - for (let i = 1; i <= 5; ++i) { - ret.push(generateMatch(mxrKey + i)); + if (text.current.toLowerCase().startsWith(mxrKey)) { + for (let i = minRange; i <= maxRange; ++i) { + ret.push(generateMatch(mxrKey + i)); + } + } else if (mxrKey.startsWith(text.current.toLowerCase())) { + ret.push(generateMatch(mxrKey)); } - } else if (mxrKey.startsWith(text.current.toLowerCase())) { - ret.push(generateMatch(mxrKey)); - } + }; + addRangeAutoComp(this.searchQueryParserService.keywords.minRating, + this.searchQueryParserService.keywords.maxRating, 1, 5); + addRangeAutoComp(this.searchQueryParserService.keywords.minPersonCount, + this.searchQueryParserService.keywords.maxPersonCount, 0, 9); + // Date patterns if (new RegExp('^' + diff --git a/src/frontend/app/ui/gallery/search/search-query-parser.service.ts b/src/frontend/app/ui/gallery/search/search-query-parser.service.ts index 320fd7d0..abfdc91c 100644 --- a/src/frontend/app/ui/gallery/search/search-query-parser.service.ts +++ b/src/frontend/app/ui/gallery/search/search-query-parser.service.ts @@ -16,8 +16,10 @@ export class SearchQueryParserService { to: 'before', landscape: 'landscape', maxRating: 'max-rating', - maxResolution: 'max-resolution', minRating: 'min-rating', + minPersonCount: 'min-persons', + maxPersonCount: 'max-persons', + maxResolution: 'max-resolution', minResolution: 'min-resolution', orientation: 'orientation', diff --git a/test/backend/unit/model/sql/SearchManager.spec.ts b/test/backend/unit/model/sql/SearchManager.spec.ts index 3837aaee..d35d3770 100644 --- a/test/backend/unit/model/sql/SearchManager.spec.ts +++ b/test/backend/unit/model/sql/SearchManager.spec.ts @@ -9,8 +9,10 @@ import { DatePatternSearch, DistanceSearch, FromDateSearch, + MaxPersonCountSearch, MaxRatingSearch, MaxResolutionSearch, + MinPersonCountSearch, MinRatingSearch, MinResolutionSearch, OrientationSearch, @@ -1094,6 +1096,83 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { }); + it('should search person count', async () => { + const sm = new SearchManager(); + + let query: MinPersonCountSearch | MaxPersonCountSearch = {value: 0, type: SearchQueryTypes.max_person_count} as MaxPersonCountSearch; + + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [pFaceLess, v], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = ({value: 20, type: SearchQueryTypes.max_person_count} as MaxPersonCountSearch); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, pFaceLess, p4, v], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = ({value: 20, negate: true, type: SearchQueryTypes.max_person_count} as MaxPersonCountSearch); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + + query = ({value: 4, type: SearchQueryTypes.max_person_count} as MaxPersonCountSearch); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p2, p4, pFaceLess, v], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = ({value: 2, type: SearchQueryTypes.min_person_count} as MinPersonCountSearch); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = ({value: 6, type: SearchQueryTypes.min_person_count} as MinPersonCountSearch); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = ({value: 2, negate: true, type: SearchQueryTypes.min_person_count} as MinPersonCountSearch); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [v, pFaceLess], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + }); + it('should search resolution', async () => { const sm = new SearchManager(); diff --git a/test/common/unit/SearchQueryParser.ts b/test/common/unit/SearchQueryParser.ts index f52c71cc..04497dd0 100644 --- a/test/common/unit/SearchQueryParser.ts +++ b/test/common/unit/SearchQueryParser.ts @@ -5,8 +5,9 @@ import { DatePatternSearch, DistanceSearch, FromDateSearch, + MaxPersonCountSearch, MaxRatingSearch, - MaxResolutionSearch, + MaxResolutionSearch, MinPersonCountSearch, MinRatingSearch, MinResolutionSearch, OrientationSearch, @@ -98,6 +99,12 @@ describe('SearchQueryParser', () => { check({type: SearchQueryTypes.min_rating, value: 10, negate: true} as MinRatingSearch); check({type: SearchQueryTypes.max_rating, value: 1, negate: true} as MaxRatingSearch); }); + it('Person count search', () => { + check({type: SearchQueryTypes.min_person_count, value: 10} as MinPersonCountSearch); + check({type: SearchQueryTypes.max_person_count, value: 1} as MaxPersonCountSearch); + check({type: SearchQueryTypes.min_person_count, value: 10, negate: true} as MinPersonCountSearch); + check({type: SearchQueryTypes.max_person_count, value: 1, negate: true} as MaxPersonCountSearch); + }); it('Resolution search', () => { check({type: SearchQueryTypes.min_resolution, value: 10} as MinResolutionSearch); check({type: SearchQueryTypes.max_resolution, value: 5} as MaxResolutionSearch);