mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
Implementing model for advanced searching. #58
This commit is contained in:
parent
928f282311
commit
9a923aa8ab
33
package-lock.json
generated
33
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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');
|
||||
}
|
||||
|
14
src/backend/model/database/LocationManager.ts
Normal file
14
src/backend/model/database/LocationManager.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {GPSMetadata} from '../../../common/entities/PhotoDTO';
|
||||
|
||||
|
||||
export class LocationManager {
|
||||
|
||||
async getGPSData(text: string): Promise<GPSMetadata> {
|
||||
return {
|
||||
longitude: 0,
|
||||
latitude: 0,
|
||||
altitude: 0
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -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<DirectoryDTO> {
|
||||
|
||||
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
|
||||
|
@ -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 && (<DistanceSearch>query).from.text) {
|
||||
(<DistanceSearch>query).from.GPSData =
|
||||
await ObjectManagers.getInstance().LocationManager.getGPSData((<DistanceSearch>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<SearchResultDTO> {
|
||||
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 => {
|
||||
(<ANDSearchQuery>query).list.forEach(sq => q.andWhere(this.buildWhereQuery(sq, paramCounter)));
|
||||
return q;
|
||||
});
|
||||
case SearchQueryTypes.OR:
|
||||
return new Brackets(q => {
|
||||
(<ANDSearchQuery>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 = (<DistanceSearch>query).from.GPSData.latitude - ((<DistanceSearch>query).distance * latDelta),
|
||||
maxLat = (<DistanceSearch>query).from.GPSData.latitude + ((<DistanceSearch>query).distance * latDelta),
|
||||
minLon = (<DistanceSearch>query).from.GPSData.latitude -
|
||||
((<DistanceSearch>query).distance * lonDelta) / Math.cos(minLat * (Math.PI / 180)),
|
||||
maxLon = (<DistanceSearch>query).from.GPSData.latitude +
|
||||
((<DistanceSearch>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 (<DateSearch>query).before === 'undefined' && typeof (<DateSearch>query).after === 'undefined') {
|
||||
throw new Error('Invalid search query: Date Query should contain before or after value');
|
||||
}
|
||||
if (typeof (<DateSearch>query).before !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['before' + paramCounter.value] = (<DateSearch>query).before;
|
||||
q.where(`media.metadata.creationDate <= :before${paramCounter.value}`, textParam);
|
||||
}
|
||||
|
||||
if (typeof (<DateSearch>query).after !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['after' + paramCounter.value] = (<DateSearch>query).after;
|
||||
q.andWhere(`media.metadata.creationDate >= :after${paramCounter.value}`, textParam);
|
||||
}
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
case SearchQueryTypes.rating:
|
||||
return new Brackets(q => {
|
||||
if (typeof (<RatingSearch>query).min === 'undefined' && typeof (<RatingSearch>query).max === 'undefined') {
|
||||
throw new Error('Invalid search query: Rating Query should contain min or max value');
|
||||
}
|
||||
if (typeof (<RatingSearch>query).min !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['min' + paramCounter.value] = (<RatingSearch>query).min;
|
||||
q.where(`media.metadata.rating >= :min${paramCounter.value}`, textParam);
|
||||
}
|
||||
|
||||
if (typeof (<RatingSearch>query).max !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['max' + paramCounter.value] = (<RatingSearch>query).max;
|
||||
q.andWhere(`media.metadata.rating <= :max${paramCounter.value}`, textParam);
|
||||
}
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
case SearchQueryTypes.resolution:
|
||||
return new Brackets(q => {
|
||||
if (typeof (<ResolutionSearch>query).min === 'undefined' && typeof (<ResolutionSearch>query).max === 'undefined') {
|
||||
throw new Error('Invalid search query: Rating Query should contain min or max value');
|
||||
}
|
||||
if (typeof (<ResolutionSearch>query).min !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['min' + paramCounter.value] = (<RatingSearch>query).min * 1000 * 1000;
|
||||
q.where(`media.metadata.size.width * media.metadata.size.height >= :min${paramCounter.value}`, textParam);
|
||||
}
|
||||
|
||||
if (typeof (<ResolutionSearch>query).max !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['max' + paramCounter.value] = (<RatingSearch>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 ((<OrientationSearch>query).orientation === OrientationSearchTypes.landscape) {
|
||||
q.where('media.metadata.size.width >= media.metadata.size.height');
|
||||
}
|
||||
if ((<OrientationSearch>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 (<TextSearch>query).matchType === TextSearchQueryTypes.exact_match ? str : `%${str}%`;
|
||||
};
|
||||
|
||||
const textParam: any = {};
|
||||
paramCounter.value++;
|
||||
textParam['text' + paramCounter.value] = createMatchString((<TextSearch>query).text);
|
||||
|
||||
if (query.type === SearchQueryTypes.any_text ||
|
||||
query.type === SearchQueryTypes.directory) {
|
||||
const dirPathStr = ((<TextSearch>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<AutoCompleteItem> {
|
||||
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].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) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {MediaDTO} from './MediaDTO';
|
||||
import {FileDTO} from './FileDTO';
|
||||
import {PhotoDTO} from './PhotoDTO';
|
||||
|
||||
export interface DirectoryDTO<S extends FileDTO = MediaDTO> {
|
||||
id: number;
|
||||
@ -54,4 +55,10 @@ export module DirectoryDTO {
|
||||
}
|
||||
|
||||
};
|
||||
export const filterPhotos = (dir: DirectoryDTO): PhotoDTO[] => {
|
||||
return <PhotoDTO[]>dir.media.filter(m => MediaDTO.isPhoto(m));
|
||||
};
|
||||
export const filterVideos = (dir: DirectoryDTO): PhotoDTO[] => {
|
||||
return <PhotoDTO[]>dir.media.filter(m => MediaDTO.isPhoto(m));
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
106
src/common/entities/SearchQueryDTO.ts
Normal file
106
src/common/entities/SearchQueryDTO.ts
Normal file
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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 = [<FaceRegion>{
|
||||
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 = [<FaceRegion>{
|
||||
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 = [<FaceRegion>{
|
||||
box: {height: 10, width: 10, left: 10, top: 10},
|
||||
name: 'Kylo Ren'
|
||||
}, <FaceRegion>{
|
||||
box: {height: 10, width: 10, left: 101, top: 101},
|
||||
name: 'Leia Organa'
|
||||
}, <FaceRegion>{
|
||||
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 = [<FaceRegion>{
|
||||
box: {height: 10, width: 10, left: 10, top: 10},
|
||||
name: 'Kylo Ren'
|
||||
}, <FaceRegion>{
|
||||
box: {height: 10, width: 10, left: 101, top: 101},
|
||||
name: 'Anakin Skywalker'
|
||||
}, <FaceRegion>{
|
||||
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) {
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user