mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
Add person count search and sorting support.
Note it will trigger a DB drop as the scheme changed. #683
This commit is contained in:
parent
1974323b61
commit
0e460d07af
@ -367,6 +367,7 @@ export class IndexingManager {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
(media[i].metadata as PhotoMetadataEntity).personsLength = (media[i].metadata as PhotoMetadataEntity)?.persons?.length || 0;
|
||||||
|
|
||||||
|
|
||||||
if (mediaItem == null) {
|
if (mediaItem == null) {
|
||||||
|
@ -13,9 +13,9 @@ import {
|
|||||||
DatePatternFrequency,
|
DatePatternFrequency,
|
||||||
DatePatternSearch,
|
DatePatternSearch,
|
||||||
DistanceSearch,
|
DistanceSearch,
|
||||||
FromDateSearch,
|
FromDateSearch, MaxPersonCountSearch,
|
||||||
MaxRatingSearch,
|
MaxRatingSearch,
|
||||||
MaxResolutionSearch,
|
MaxResolutionSearch, MinPersonCountSearch,
|
||||||
MinRatingSearch,
|
MinRatingSearch,
|
||||||
MinResolutionSearch,
|
MinResolutionSearch,
|
||||||
OrientationSearch,
|
OrientationSearch,
|
||||||
@ -379,6 +379,12 @@ export class SearchManager {
|
|||||||
case SortingMethods.ascName:
|
case SortingMethods.ascName:
|
||||||
query.addOrderBy('media.name', 'ASC');
|
query.addOrderBy('media.name', 'ASC');
|
||||||
break;
|
break;
|
||||||
|
case SortingMethods.descPersonCount:
|
||||||
|
query.addOrderBy('media.metadata.personsLength', 'DESC');
|
||||||
|
break;
|
||||||
|
case SortingMethods.ascPersonCount:
|
||||||
|
query.addOrderBy('media.metadata.personsLength', 'ASC');
|
||||||
|
break;
|
||||||
case SortingMethods.random:
|
case SortingMethods.random:
|
||||||
if (Config.Database.type === DatabaseType.mysql) {
|
if (Config.Database.type === DatabaseType.mysql) {
|
||||||
query.groupBy('RAND(), media.id');
|
query.groupBy('RAND(), media.id');
|
||||||
@ -639,6 +645,52 @@ export class SearchManager {
|
|||||||
return q;
|
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:
|
case SearchQueryTypes.min_resolution:
|
||||||
if (directoryOnly) {
|
if (directoryOnly) {
|
||||||
throw new Error('not supported in directoryOnly mode');
|
throw new Error('not supported in directoryOnly mode');
|
||||||
|
@ -1,26 +1,9 @@
|
|||||||
import {
|
import {Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance, Unique,} from 'typeorm';
|
||||||
Column,
|
import {DirectoryEntity} from './DirectoryEntity';
|
||||||
Entity,
|
import {MediaDimension, MediaDTO, MediaMetadata,} from '../../../../common/entities/MediaDTO';
|
||||||
Index,
|
import {PersonJunctionTable} from './PersonJunctionTable';
|
||||||
ManyToOne,
|
import {columnCharsetCS} from './EntityUtils';
|
||||||
OneToMany,
|
import {CameraMetadata, FaceRegion, GPSMetadata, PositionMetaData,} from '../../../../common/entities/PhotoDTO';
|
||||||
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 {
|
export class MediaDimensionEntity implements MediaDimension {
|
||||||
@Column('int')
|
@Column('int')
|
||||||
@ -31,7 +14,7 @@ export class MediaDimensionEntity implements MediaDimension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CameraMetadataEntity implements CameraMetadata {
|
export class CameraMetadataEntity implements CameraMetadata {
|
||||||
@Column('int', { nullable: true, unsigned: true })
|
@Column('int', {nullable: true, unsigned: true})
|
||||||
ISO: number;
|
ISO: number;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
@ -50,23 +33,23 @@ export class CameraMetadataEntity implements CameraMetadata {
|
|||||||
})
|
})
|
||||||
make: string;
|
make: string;
|
||||||
|
|
||||||
@Column('float', { nullable: true })
|
@Column('float', {nullable: true})
|
||||||
fStop: number;
|
fStop: number;
|
||||||
|
|
||||||
@Column('float', { nullable: true })
|
@Column('float', {nullable: true})
|
||||||
exposure: number;
|
exposure: number;
|
||||||
|
|
||||||
@Column('float', { nullable: true })
|
@Column('float', {nullable: true})
|
||||||
focalLength: number;
|
focalLength: number;
|
||||||
|
|
||||||
@Column('text', { nullable: true })
|
@Column('text', {nullable: true})
|
||||||
lens: string;
|
lens: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GPSMetadataEntity implements GPSMetadata {
|
export class GPSMetadataEntity implements GPSMetadata {
|
||||||
@Column('float', { nullable: true })
|
@Column('float', {nullable: true})
|
||||||
latitude: number;
|
latitude: number;
|
||||||
@Column('float', { nullable: true })
|
@Column('float', {nullable: true})
|
||||||
longitude: number;
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +103,7 @@ export class MediaMetadataEntity implements MediaMetadata {
|
|||||||
@Index()
|
@Index()
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
|
||||||
@Column('int', { unsigned: true })
|
@Column('int', {unsigned: true})
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
@ -136,7 +119,7 @@ export class MediaMetadataEntity implements MediaMetadata {
|
|||||||
@Column((type) => PositionMetaDataEntity)
|
@Column((type) => PositionMetaDataEntity)
|
||||||
positionData: PositionMetaDataEntity;
|
positionData: PositionMetaDataEntity;
|
||||||
|
|
||||||
@Column('tinyint', { unsigned: true })
|
@Column('tinyint', {unsigned: true})
|
||||||
@Index()
|
@Index()
|
||||||
rating: 0 | 1 | 2 | 3 | 4 | 5;
|
rating: 0 | 1 | 2 | 3 | 4 | 5;
|
||||||
|
|
||||||
@ -144,10 +127,11 @@ export class MediaMetadataEntity implements MediaMetadata {
|
|||||||
personJunction: PersonJunctionTable[];
|
personJunction: PersonJunctionTable[];
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type:'simple-json',
|
type: 'simple-json',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
charset: columnCharsetCS.charset,
|
charset: columnCharsetCS.charset,
|
||||||
collation: columnCharsetCS.collation})
|
collation: columnCharsetCS.collation
|
||||||
|
})
|
||||||
faces: FaceRegion[];
|
faces: FaceRegion[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,20 +146,32 @@ export class MediaMetadataEntity implements MediaMetadata {
|
|||||||
})
|
})
|
||||||
persons: string[];
|
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;
|
bitRate: number;
|
||||||
|
|
||||||
@Column('int', { unsigned: true })
|
@Column('int', {unsigned: true})
|
||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix inheritance once its working in typeorm
|
// TODO: fix inheritance once its working in typeorm
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['name', 'directory'])
|
@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 {
|
export abstract class MediaEntity implements MediaDTO {
|
||||||
@Index()
|
@Index()
|
||||||
@PrimaryGeneratedColumn({ unsigned: true })
|
@PrimaryGeneratedColumn({unsigned: true})
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column(columnCharsetCS)
|
@Column(columnCharsetCS)
|
||||||
|
@ -35,7 +35,7 @@ export class TopPickSendJob extends Job<{
|
|||||||
type: 'sort-array',
|
type: 'sort-array',
|
||||||
name: backendTexts.sortBy.name,
|
name: backendTexts.sortBy.name,
|
||||||
description: backendTexts.sortBy.description,
|
description: backendTexts.sortBy.description,
|
||||||
defaultValue: [SortingMethods.descRating],
|
defaultValue: [SortingMethods.descRating, SortingMethods.descPersonCount],
|
||||||
}, {
|
}, {
|
||||||
id: 'pickAmount',
|
id: 'pickAmount',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@ -48,7 +48,7 @@ export class TopPickSendJob extends Job<{
|
|||||||
name: backendTexts.emailTo.name,
|
name: backendTexts.emailTo.name,
|
||||||
description: backendTexts.emailTo.description,
|
description: backendTexts.emailTo.description,
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
}, {
|
}, {
|
||||||
id: 'emailSubject',
|
id: 'emailSubject',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: backendTexts.emailSubject.name,
|
name: backendTexts.emailSubject.name,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/**
|
/**
|
||||||
* This version indicates that the sql/entities/*Entity.ts files got changed and the db needs to be recreated
|
* 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;
|
||||||
|
@ -13,6 +13,8 @@ export const PG2ConfMap = {
|
|||||||
'.order_descending_rating.pg2conf': SortingMethods.descRating,
|
'.order_descending_rating.pg2conf': SortingMethods.descRating,
|
||||||
'.order_ascending_rating.pg2conf': SortingMethods.ascRating,
|
'.order_ascending_rating.pg2conf': SortingMethods.ascRating,
|
||||||
'.order_random.pg2conf': SortingMethods.random,
|
'.order_random.pg2conf': SortingMethods.random,
|
||||||
|
'.order_descending_person_count.pg2conf': SortingMethods.descPersonCount,
|
||||||
|
'.order_ascending_person_count.pg2conf': SortingMethods.descPersonCount,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,10 +4,6 @@ import {
|
|||||||
DatePatternSearch,
|
DatePatternSearch,
|
||||||
DistanceSearch,
|
DistanceSearch,
|
||||||
FromDateSearch,
|
FromDateSearch,
|
||||||
MaxRatingSearch,
|
|
||||||
MaxResolutionSearch,
|
|
||||||
MinRatingSearch,
|
|
||||||
MinResolutionSearch,
|
|
||||||
NegatableSearchQuery,
|
NegatableSearchQuery,
|
||||||
OrientationSearch,
|
OrientationSearch,
|
||||||
ORSearchQuery,
|
ORSearchQuery,
|
||||||
@ -41,6 +37,8 @@ export interface QueryKeywords {
|
|||||||
minResolution: string;
|
minResolution: string;
|
||||||
maxRating: string;
|
maxRating: string;
|
||||||
minRating: string;
|
minRating: string;
|
||||||
|
maxPersonCount: string;
|
||||||
|
minPersonCount: string;
|
||||||
NSomeOf: string;
|
NSomeOf: string;
|
||||||
someOf: string;
|
someOf: string;
|
||||||
or: string;
|
or: string;
|
||||||
@ -65,8 +63,10 @@ export const defaultQueryKeywords: QueryKeywords = {
|
|||||||
to: 'before',
|
to: 'before',
|
||||||
|
|
||||||
maxRating: 'max-rating',
|
maxRating: 'max-rating',
|
||||||
maxResolution: 'max-resolution',
|
|
||||||
minRating: 'min-rating',
|
minRating: 'min-rating',
|
||||||
|
maxPersonCount: 'max-persons',
|
||||||
|
minPersonCount: 'min-persons',
|
||||||
|
maxResolution: 'max-resolution',
|
||||||
minResolution: 'min-resolution',
|
minResolution: 'min-resolution',
|
||||||
|
|
||||||
kmFrom: 'km-from',
|
kmFrom: 'km-from',
|
||||||
@ -303,38 +303,28 @@ export class SearchQueryParser {
|
|||||||
} as ToDateSearch;
|
} as ToDateSearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kwStartsWith(str, this.keywords.minRating)) {
|
const addValueRangeParser = (matcher: string, type: SearchQueryTypes): RangeSearch | undefined => {
|
||||||
return {
|
if (kwStartsWith(str, matcher)) {
|
||||||
type: SearchQueryTypes.min_rating,
|
return {
|
||||||
value: parseInt(str.substring(str.indexOf(':') + 1), 10),
|
type: type,
|
||||||
...(str.startsWith(this.keywords.minRating + '!:') && {negate: true}), // only add if the value is true
|
value: parseInt(str.substring(str.indexOf(':') + 1), 10),
|
||||||
} as MinRatingSearch;
|
...(str.startsWith(matcher + '!:') && {negate: true}), // only add if the value is true
|
||||||
}
|
} as RangeSearch;
|
||||||
if (kwStartsWith(str, this.keywords.maxRating)) {
|
}
|
||||||
return {
|
};
|
||||||
type: SearchQueryTypes.max_rating,
|
|
||||||
value: parseInt(str.substring(str.indexOf(':') + 1), 10),
|
const range = addValueRangeParser(this.keywords.minRating, SearchQueryTypes.min_rating) ||
|
||||||
...(str.startsWith(this.keywords.maxRating + '!:') && {negate: true}), // only add if the value is true
|
addValueRangeParser(this.keywords.maxRating, SearchQueryTypes.max_rating) ||
|
||||||
} as MaxRatingSearch;
|
addValueRangeParser(this.keywords.minResolution, SearchQueryTypes.min_resolution) ||
|
||||||
}
|
addValueRangeParser(this.keywords.maxResolution, SearchQueryTypes.max_resolution) ||
|
||||||
if (kwStartsWith(str, this.keywords.minResolution)) {
|
addValueRangeParser(this.keywords.minPersonCount, SearchQueryTypes.min_person_count) ||
|
||||||
return {
|
addValueRangeParser(this.keywords.maxPersonCount, SearchQueryTypes.max_person_count);
|
||||||
type: SearchQueryTypes.min_resolution,
|
|
||||||
value: parseInt(str.substring(str.indexOf(':') + 1), 10),
|
if (range) {
|
||||||
...(str.startsWith(this.keywords.minResolution + '!:') && {
|
return range;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (new RegExp('^\\d*-' + this.keywords.kmFrom + '!?:').test(str)) {
|
if (new RegExp('^\\d*-' + this.keywords.kmFrom + '!?:').test(str)) {
|
||||||
let from = str.slice(
|
let from = str.slice(
|
||||||
new RegExp('^\\d*-' + this.keywords.kmFrom + '!?:').exec(str)[0].length
|
new RegExp('^\\d*-' + this.keywords.kmFrom + '!?:').exec(str)[0].length
|
||||||
@ -528,6 +518,22 @@ export class SearchQueryParser {
|
|||||||
? ''
|
? ''
|
||||||
: (query as RangeSearch).value)
|
: (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:
|
case SearchQueryTypes.min_resolution:
|
||||||
return (
|
return (
|
||||||
this.keywords.minResolution +
|
this.keywords.minResolution +
|
||||||
|
@ -898,6 +898,7 @@ export class ServerPreviewConfig {
|
|||||||
Sorting: SortingMethods[] = [
|
Sorting: SortingMethods[] = [
|
||||||
SortingMethods.descRating,
|
SortingMethods.descRating,
|
||||||
SortingMethods.descDate,
|
SortingMethods.descDate,
|
||||||
|
SortingMethods.descPersonCount,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,14 @@ export enum SearchQueryTypes {
|
|||||||
max_rating,
|
max_rating,
|
||||||
min_resolution,
|
min_resolution,
|
||||||
max_resolution,
|
max_resolution,
|
||||||
|
min_person_count,
|
||||||
|
max_person_count,
|
||||||
|
|
||||||
distance,
|
distance = 50,
|
||||||
orientation,
|
orientation,
|
||||||
date_pattern,
|
|
||||||
|
|
||||||
|
date_pattern = 60,
|
||||||
|
|
||||||
// TEXT search types
|
// TEXT search types
|
||||||
any_text = 100,
|
any_text = 100,
|
||||||
@ -211,6 +215,17 @@ export interface MaxRatingSearch extends RangeSearch {
|
|||||||
value: number;
|
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 {
|
export interface MinResolutionSearch extends RangeSearch {
|
||||||
type: SearchQueryTypes.min_resolution;
|
type: SearchQueryTypes.min_resolution;
|
||||||
value: number; // in megapixels
|
value: number; // in megapixels
|
||||||
|
@ -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 {
|
export enum SortingMethods {
|
||||||
ascName = 1,
|
ascName = 10,
|
||||||
descName,
|
descName = 11,
|
||||||
ascDate,
|
ascDate = 20,
|
||||||
descDate,
|
descDate = 21,
|
||||||
ascRating,
|
ascRating = 30,
|
||||||
descRating,
|
descRating = 31,
|
||||||
random,
|
ascPersonCount = 40,
|
||||||
|
descPersonCount = 41,
|
||||||
|
random = 100 // let's keep random as the last in the UI
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ export class IconizeSortingMethod implements PipeTransform {
|
|||||||
return '<span class="oi oi-sort-ascending"></span><span class="oi oi-star text-bold"></span>';
|
return '<span class="oi oi-sort-ascending"></span><span class="oi oi-star text-bold"></span>';
|
||||||
case SortingMethods.descRating:
|
case SortingMethods.descRating:
|
||||||
return '<span class="oi oi-sort-descending"></span><span class="oi oi-star text-bold"></span>';
|
return '<span class="oi oi-sort-descending"></span><span class="oi oi-star text-bold"></span>';
|
||||||
|
case SortingMethods.ascPersonCount:
|
||||||
|
return '<span class="oi oi-sort-ascending"></span><span class="oi oi-person text-bold"></span>';
|
||||||
|
case SortingMethods.descPersonCount:
|
||||||
|
return '<span class="oi oi-sort-descending"></span><span class="oi oi-person text-bold"></span>';
|
||||||
case SortingMethods.ascName:
|
case SortingMethods.ascName:
|
||||||
return '<span class="oi oi-sort-ascending"></span><strong>A</strong>';
|
return '<span class="oi oi-sort-ascending"></span><strong>A</strong>';
|
||||||
case SortingMethods.descName:
|
case SortingMethods.descName:
|
||||||
|
@ -43,6 +43,8 @@ EnumTranslations[SortingMethods[SortingMethods.ascName]] = $localize`ascending n
|
|||||||
EnumTranslations[SortingMethods[SortingMethods.descRating]] = $localize`descending rating`;
|
EnumTranslations[SortingMethods[SortingMethods.descRating]] = $localize`descending rating`;
|
||||||
EnumTranslations[SortingMethods[SortingMethods.ascRating]] = $localize`ascending rating`;
|
EnumTranslations[SortingMethods[SortingMethods.ascRating]] = $localize`ascending rating`;
|
||||||
EnumTranslations[SortingMethods[SortingMethods.random]] = $localize`random`;
|
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`;
|
EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.url]] = $localize`Url`;
|
||||||
|
@ -179,6 +179,18 @@ export class GallerySortingService {
|
|||||||
(b.metadata.rating || 0) - (a.metadata.rating || 0)
|
(b.metadata.rating || 0) - (a.metadata.rating || 0)
|
||||||
);
|
);
|
||||||
break;
|
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:
|
case SortingMethods.random:
|
||||||
this.rndService.setSeed(c.media.length);
|
this.rndService.setSeed(c.media.length);
|
||||||
c.media
|
c.media
|
||||||
|
@ -29,6 +29,8 @@ export class AutoCompleteService {
|
|||||||
k !== this.searchQueryParserService.keywords.NSomeOf &&
|
k !== this.searchQueryParserService.keywords.NSomeOf &&
|
||||||
k !== this.searchQueryParserService.keywords.minRating &&
|
k !== this.searchQueryParserService.keywords.minRating &&
|
||||||
k !== this.searchQueryParserService.keywords.maxRating &&
|
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_week &&
|
||||||
k !== this.searchQueryParserService.keywords.every_month &&
|
k !== this.searchQueryParserService.keywords.every_month &&
|
||||||
k !== this.searchQueryParserService.keywords.every_year &&
|
k !== this.searchQueryParserService.keywords.every_year &&
|
||||||
@ -91,6 +93,11 @@ export class AutoCompleteService {
|
|||||||
this.noACKeywordsMap[this.searchQueryParserService.keywords.maxRating]
|
this.noACKeywordsMap[this.searchQueryParserService.keywords.maxRating]
|
||||||
= SearchQueryTypes.max_rating;
|
= 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]
|
this.noACKeywordsMap[this.searchQueryParserService.keywords.minResolution]
|
||||||
= SearchQueryTypes.min_resolution;
|
= SearchQueryTypes.min_resolution;
|
||||||
this.noACKeywordsMap[this.searchQueryParserService.keywords.maxResolution]
|
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 addRangeAutoComp = (minStr: string, maxStr: string, minRange: number, maxRange: number) => {
|
||||||
const mxrKey = this.searchQueryParserService.keywords.maxRating + ':';
|
// only showing rating recommendations of the full query is typed
|
||||||
if (text.current.toLowerCase().startsWith(mrKey)) {
|
const mrKey = minStr + ':';
|
||||||
for (let i = 1; i <= 5; ++i) {
|
const mxrKey = maxStr + ':';
|
||||||
ret.push(generateMatch(mrKey + i));
|
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)) {
|
if (text.current.toLowerCase().startsWith(mxrKey)) {
|
||||||
for (let i = 1; i <= 5; ++i) {
|
for (let i = minRange; i <= maxRange; ++i) {
|
||||||
ret.push(generateMatch(mxrKey + 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
|
// Date patterns
|
||||||
if (new RegExp('^' +
|
if (new RegExp('^' +
|
||||||
|
@ -16,8 +16,10 @@ export class SearchQueryParserService {
|
|||||||
to: 'before',
|
to: 'before',
|
||||||
landscape: 'landscape',
|
landscape: 'landscape',
|
||||||
maxRating: 'max-rating',
|
maxRating: 'max-rating',
|
||||||
maxResolution: 'max-resolution',
|
|
||||||
minRating: 'min-rating',
|
minRating: 'min-rating',
|
||||||
|
minPersonCount: 'min-persons',
|
||||||
|
maxPersonCount: 'max-persons',
|
||||||
|
maxResolution: 'max-resolution',
|
||||||
minResolution: 'min-resolution',
|
minResolution: 'min-resolution',
|
||||||
orientation: 'orientation',
|
orientation: 'orientation',
|
||||||
|
|
||||||
|
@ -9,8 +9,10 @@ import {
|
|||||||
DatePatternSearch,
|
DatePatternSearch,
|
||||||
DistanceSearch,
|
DistanceSearch,
|
||||||
FromDateSearch,
|
FromDateSearch,
|
||||||
|
MaxPersonCountSearch,
|
||||||
MaxRatingSearch,
|
MaxRatingSearch,
|
||||||
MaxResolutionSearch,
|
MaxResolutionSearch,
|
||||||
|
MinPersonCountSearch,
|
||||||
MinRatingSearch,
|
MinRatingSearch,
|
||||||
MinResolutionSearch,
|
MinResolutionSearch,
|
||||||
OrientationSearch,
|
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 () => {
|
it('should search resolution', async () => {
|
||||||
const sm = new SearchManager();
|
const sm = new SearchManager();
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ import {
|
|||||||
DatePatternSearch,
|
DatePatternSearch,
|
||||||
DistanceSearch,
|
DistanceSearch,
|
||||||
FromDateSearch,
|
FromDateSearch,
|
||||||
|
MaxPersonCountSearch,
|
||||||
MaxRatingSearch,
|
MaxRatingSearch,
|
||||||
MaxResolutionSearch,
|
MaxResolutionSearch, MinPersonCountSearch,
|
||||||
MinRatingSearch,
|
MinRatingSearch,
|
||||||
MinResolutionSearch,
|
MinResolutionSearch,
|
||||||
OrientationSearch,
|
OrientationSearch,
|
||||||
@ -98,6 +99,12 @@ describe('SearchQueryParser', () => {
|
|||||||
check({type: SearchQueryTypes.min_rating, value: 10, negate: true} as MinRatingSearch);
|
check({type: SearchQueryTypes.min_rating, value: 10, negate: true} as MinRatingSearch);
|
||||||
check({type: SearchQueryTypes.max_rating, value: 1, negate: true} as MaxRatingSearch);
|
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', () => {
|
it('Resolution search', () => {
|
||||||
check({type: SearchQueryTypes.min_resolution, value: 10} as MinResolutionSearch);
|
check({type: SearchQueryTypes.min_resolution, value: 10} as MinResolutionSearch);
|
||||||
check({type: SearchQueryTypes.max_resolution, value: 5} as MaxResolutionSearch);
|
check({type: SearchQueryTypes.max_resolution, value: 5} as MaxResolutionSearch);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user