mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-01-02 03:37:54 +02:00
Implementing search query and custom sorting based preview selection #80
This commit is contained in:
parent
628bc74226
commit
43343b252c
104
MANPAGE.md
104
MANPAGE.md
@ -4,33 +4,33 @@ pigallery2 uses [typeconfig](https://github.com/bpatrik/typeconfig) for configur
|
||||
`npm start -- --help` prints the following:
|
||||
|
||||
```
|
||||
Usage: <appname> [options]
|
||||
Usage: <appname> [options]
|
||||
|
||||
Meta cli options:
|
||||
--help prints this manual
|
||||
--config-path sets the config file location
|
||||
--config-attachState prints the value state (default, readonly, volatile, etc..) to the config file
|
||||
--config-attachDesc prints description to the config file
|
||||
--config-rewrite-cli updates the config file with the options from cli switches
|
||||
--config-rewrite-env updates the config file with the options from environmental variables
|
||||
--config-string-enum enums are stored as string in the config file (instead of numbers)
|
||||
--config-save-if-not-exist creates config file if not exist
|
||||
--config-save-and-exist creates config file and terminates
|
||||
Meta cli options:
|
||||
--help prints this manual
|
||||
--config-path sets the config file location
|
||||
--config-attachState prints the value state (default, readonly, volatile, etc..) to the config file
|
||||
--config-attachDesc prints description to the config file
|
||||
--config-rewrite-cli updates the config file with the options from cli switches
|
||||
--config-rewrite-env updates the config file with the options from environmental variables
|
||||
--config-string-enum enums are stored as string in the config file (instead of numbers)
|
||||
--config-save-if-not-exist creates config file if not exist
|
||||
--config-save-and-exist creates config file and terminates
|
||||
|
||||
<appname> can be configured through the configuration file, cli switches and environmental variables.
|
||||
All settings are case sensitive.
|
||||
Example for setting config MyConf through cli: '<appname> --MyConf=5'
|
||||
and through env variable: 'SET MyConf=5' .
|
||||
<appname> can be configured through the configuration file, cli switches and environmental variables.
|
||||
All settings are case sensitive.
|
||||
Example for setting config MyConf through cli: '<appname> --MyConf=5'
|
||||
and through env variable: 'SET MyConf=5' .
|
||||
|
||||
Default values can be also overwritten by prefixing the options with 'default-',
|
||||
Default values can be also overwritten by prefixing the options with 'default-',
|
||||
like '<appname> --default-MyConf=5' and 'SET default-MyConf=5'
|
||||
|
||||
App CLI options:
|
||||
App CLI options:
|
||||
--Server-sessionSecret (default: [])
|
||||
--Server-port (default: 80)
|
||||
--Server-host (default: '0.0.0.0')
|
||||
--Server-Media-folder Images are loaded from this folder (read permission required) (default: 'demo/images')
|
||||
--Server-Media-tempFolder Thumbnails, coverted photos, videos will be stored here (write permission required) (default: 'demo/tmp')
|
||||
--Server-Media-tempFolder Thumbnails, converted photos, videos will be stored here (write permission required) (default: 'demo/tmp')
|
||||
--Server-Media-Video-transcoding-bitRate (default: 5242880)
|
||||
--Server-Media-Video-transcoding-resolution (default: 720)
|
||||
--Server-Media-Video-transcoding-fps (default: 25)
|
||||
@ -43,6 +43,8 @@ App CLI options:
|
||||
--Server-Media-Photo-Converting-resolution (default: 1080)
|
||||
--Server-Media-Thumbnail-qualityPriority if true, photos will have better quality. (default: true)
|
||||
--Server-Media-Thumbnail-personFaceMargin (default: 0.6)
|
||||
--Server-Preview-SearchQuery (default: null)
|
||||
--Server-Preview-Sorting (default: [6,4])
|
||||
--Server-Threading-enabled App can run on multiple thread (default: true)
|
||||
--Server-Threading-thumbnailThreads Number of threads that are used to generate thumbnails. If 0, number of 'CPU cores -1' threads will be used. (default: 0)
|
||||
--Server-Database-type (default: 'sqlite')
|
||||
@ -64,7 +66,7 @@ App CLI options:
|
||||
--Server-Log-level (default: 'info')
|
||||
--Server-Log-sqlLevel (default: 'error')
|
||||
--Server-Jobs-maxSavedProgress Job history size (default: 10)
|
||||
--Server-Jobs-scheduled (default: [{"name":"Indexing","jobName":"Indexing","config":{},"allowParallelRun":false,"trigger":{"type":1}},{"name":"Thumbnail Generation","jobName":"Thumbnail Generation","config":{"sizes":[240],"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Indexing"}},{"name":"Photo Converting","jobName":"Photo Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Thumbnail Generation"}},{"name":"Video Converting","jobName":"Video Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Photo Converting"}},{"name":"Temp Folder Cleaning","jobName":"Temp Folder Cleaning","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Video Converting"}}])
|
||||
--Server-Jobs-scheduled (default: [{"name":"Indexing","jobName":"Indexing","config":{"indexChangesOnly":true},"allowParallelRun":false,"trigger":{"type":1}},{"name":"Thumbnail Generation","jobName":"Thumbnail Generation","config":{"sizes":[240],"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Indexing"}},{"name":"Photo Converting","jobName":"Photo Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Thumbnail Generation"}},{"name":"Video Converting","jobName":"Video Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Photo Converting"}},{"name":"Temp Folder Cleaning","jobName":"Temp Folder Cleaning","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Video Converting"}}])
|
||||
--Client-applicationTitle (default: 'PiGallery 2')
|
||||
--Client-publicUrl (default: '')
|
||||
--Client-urlBase (default: '')
|
||||
@ -73,11 +75,15 @@ App CLI options:
|
||||
--Client-Search-AutoComplete-enabled (default: true)
|
||||
--Client-Search-AutoComplete-maxItemsPerCategory (default: 5)
|
||||
--Client-Search-AutoComplete-cacheTimeout (default: 3600000)
|
||||
--Client-Search-maxMediaResult (default: 2000)
|
||||
--Client-Search-maxMediaResult (default: 10000)
|
||||
--Client-Search-listDirectories Search returns also with directories, not just media (default: false)
|
||||
--Client-Search-listMetafiles Search also returns with metafiles from directories that contain a media file of the matched search result (default: false)
|
||||
--Client-Search-maxDirectoryResult (default: 200)
|
||||
--Client-Sharing-enabled (default: true)
|
||||
--Client-Sharing-passwordProtected (default: true)
|
||||
--Client-Album-enabled (default: true)
|
||||
--Client-Map-enabled (default: true)
|
||||
--Client-Map-maxPreviewMarkers Maximum number of markers to be shown on the map preview on the gallery page. (default: 50)
|
||||
--Client-Map-useImageMarkers (default: true)
|
||||
--Client-Map-mapProvider (default: 'OpenStreetMap')
|
||||
--Client-Map-mapboxAccessToken (default: '')
|
||||
@ -86,9 +92,11 @@ App CLI options:
|
||||
--Client-Other-enableCache (default: true)
|
||||
--Client-Other-enableOnScrollRendering (default: true)
|
||||
--Client-Other-defaultPhotoSortingMethod (default: 'ascDate')
|
||||
--Client-Other-enableDirectorySortingByDate If enabled directories will be sorted by date, like photos, otherwise by name. Directory date is the last modification time of that directory not the creation date of the oldest photo (default: false)
|
||||
--Client-Other-enableOnScrollThumbnailPrioritising (default: true)
|
||||
--Client-Other-NavBar-showItemCount (default: true)
|
||||
--Client-Other-captionFirstNaming (default: false)
|
||||
--Client-Other-enableDownloadZip (default: false)
|
||||
--Client-authenticationRequired (default: true)
|
||||
--Client-unAuthenticatedUserRole (default: 'Admin')
|
||||
--Client-Media-Thumbnail-iconSize (default: 45)
|
||||
@ -96,19 +104,20 @@ App CLI options:
|
||||
--Client-Media-Thumbnail-thumbnailSizes (default: [240,480])
|
||||
--Client-Media-Video-enabled (default: true)
|
||||
--Client-Media-Photo-Converting-enabled (default: true)
|
||||
--Client-Media-Photo-loadFullImageOnZoom Enables loading the full resolution image on zoom in the ligthbox (preview). (default: true)
|
||||
--Client-MetaFile-enabled (default: true)
|
||||
--Client-Faces-enabled (default: true)
|
||||
--Client-Faces-keywordsToPersons (default: true)
|
||||
--Client-Faces-writeAccessMinRole (default: 'Admin')
|
||||
--Client-Faces-readAccessMinRole (default: 'User')
|
||||
|
||||
Environmental variables:
|
||||
Environmental variables:
|
||||
Server-sessionSecret (default: [])
|
||||
Server-port (default: 80)
|
||||
PORT same as Server-port
|
||||
Server-host (default: '0.0.0.0')
|
||||
Server-Media-folder Images are loaded from this folder (read permission required) (default: 'demo/images')
|
||||
Server-Media-tempFolder Thumbnails, coverted photos, videos will be stored here (write permission required) (default: 'demo/tmp')
|
||||
Server-Media-tempFolder Thumbnails, converted photos, videos will be stored here (write permission required) (default: 'demo/tmp')
|
||||
Server-Media-Video-transcoding-bitRate (default: 5242880)
|
||||
Server-Media-Video-transcoding-resolution (default: 720)
|
||||
Server-Media-Video-transcoding-fps (default: 25)
|
||||
@ -121,6 +130,8 @@ Environmental variables:
|
||||
Server-Media-Photo-Converting-resolution (default: 1080)
|
||||
Server-Media-Thumbnail-qualityPriority if true, photos will have better quality. (default: true)
|
||||
Server-Media-Thumbnail-personFaceMargin (default: 0.6)
|
||||
Server-Preview-SearchQuery (default: null)
|
||||
Server-Preview-Sorting (default: [6,4])
|
||||
Server-Threading-enabled App can run on multiple thread (default: true)
|
||||
Server-Threading-thumbnailThreads Number of threads that are used to generate thumbnails. If 0, number of 'CPU cores -1' threads will be used. (default: 0)
|
||||
Server-Database-type (default: 'sqlite')
|
||||
@ -140,14 +151,14 @@ Environmental variables:
|
||||
Server-sessionTimeout unit: ms (default: 604800000)
|
||||
Server-Indexing-cachedFolderTimeout (default: 3600000)
|
||||
Server-Indexing-reIndexingSensitivity (default: 'low')
|
||||
Server-Indexing-excludeFolderList If an entry starts with '/' it is treated as an absolute path. If it doesn't start with '/' but contains a '/', the path is relative to the image directory. If it doesn't contain a '/', any folder withthis name will be excluded. (default: [".Trash-1000",".dtrash","$RECYCLE.BIN"])
|
||||
Server-Indexing-excludeFolderList If an entry starts with '/' it is treated as an absolute path. If it doesn't start with '/' but contains a '/', the path is relative to the image directory. If it doesn't contain a '/', any folder with this name will be excluded. (default: [".Trash-1000",".dtrash","$RECYCLE.BIN"])
|
||||
Server-Indexing-excludeFileList Any folder that contains a file with this name will be excluded from indexing. (default: [])
|
||||
Server-photoMetadataSize only this many bites will be loaded when scanning photo for metadata (default: 524288)
|
||||
Server-Duplicates-listingLimit (default: 1000)
|
||||
Server-Log-level (default: 'info')
|
||||
Server-Log-sqlLevel (default: 'error')
|
||||
Server-Jobs-maxSavedProgress Job history size (default: 10)
|
||||
Server-Jobs-scheduled (default: [{"name":"Indexing","jobName":"Indexing","config":{},"allowParallelRun":false,"trigger":{"type":1}},{"name":"Thumbnail Generation","jobName":"Thumbnail Generation","config":{"sizes":[240],"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Indexing"}},{"name":"Photo Converting","jobName":"Photo Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Thumbnail Generation"}},{"name":"Video Converting","jobName":"Video Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Photo Converting"}},{"name":"Temp Folder Cleaning","jobName":"Temp Folder Cleaning","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Video Converting"}}])
|
||||
Server-Jobs-scheduled (default: [{"name":"Indexing","jobName":"Indexing","config":{"indexChangesOnly":true},"allowParallelRun":false,"trigger":{"type":1}},{"name":"Thumbnail Generation","jobName":"Thumbnail Generation","config":{"sizes":[240],"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Indexing"}},{"name":"Photo Converting","jobName":"Photo Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Thumbnail Generation"}},{"name":"Video Converting","jobName":"Video Converting","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Photo Converting"}},{"name":"Temp Folder Cleaning","jobName":"Temp Folder Cleaning","config":{"indexedOnly":true},"allowParallelRun":false,"trigger":{"type":4,"afterScheduleName":"Video Converting"}}])
|
||||
Client-applicationTitle (default: 'PiGallery 2')
|
||||
Client-publicUrl (default: '')
|
||||
Client-urlBase (default: '')
|
||||
@ -156,11 +167,15 @@ Environmental variables:
|
||||
Client-Search-AutoComplete-enabled (default: true)
|
||||
Client-Search-AutoComplete-maxItemsPerCategory (default: 5)
|
||||
Client-Search-AutoComplete-cacheTimeout (default: 3600000)
|
||||
Client-Search-maxMediaResult (default: 2000)
|
||||
Client-Search-maxMediaResult (default: 10000)
|
||||
Client-Search-listDirectories Search returns also with directories, not just media (default: false)
|
||||
Client-Search-listMetafiles Search also returns with metafiles from directories that contain a media file of the matched search result (default: false)
|
||||
Client-Search-maxDirectoryResult (default: 200)
|
||||
Client-Sharing-enabled (default: true)
|
||||
Client-Sharing-passwordProtected (default: true)
|
||||
Client-Album-enabled (default: true)
|
||||
Client-Map-enabled (default: true)
|
||||
Client-Map-maxPreviewMarkers Maximum number of markers to be shown on the map preview on the gallery page. (default: 50)
|
||||
Client-Map-useImageMarkers (default: true)
|
||||
Client-Map-mapProvider (default: 'OpenStreetMap')
|
||||
Client-Map-mapboxAccessToken (default: '')
|
||||
@ -169,9 +184,11 @@ Environmental variables:
|
||||
Client-Other-enableCache (default: true)
|
||||
Client-Other-enableOnScrollRendering (default: true)
|
||||
Client-Other-defaultPhotoSortingMethod (default: 'ascDate')
|
||||
Client-Other-enableDirectorySortingByDate If enabled directories will be sorted by date, like photos, otherwise by name. Directory date is the last modification time of that directory not the creation date of the oldest photo (default: false)
|
||||
Client-Other-enableOnScrollThumbnailPrioritising (default: true)
|
||||
Client-Other-NavBar-showItemCount (default: true)
|
||||
Client-Other-captionFirstNaming (default: false)
|
||||
Client-Other-enableDownloadZip (default: false)
|
||||
Client-authenticationRequired (default: true)
|
||||
Client-unAuthenticatedUserRole (default: 'Admin')
|
||||
Client-Media-Thumbnail-iconSize (default: 45)
|
||||
@ -179,6 +196,7 @@ Environmental variables:
|
||||
Client-Media-Thumbnail-thumbnailSizes (default: [240,480])
|
||||
Client-Media-Video-enabled (default: true)
|
||||
Client-Media-Photo-Converting-enabled (default: true)
|
||||
Client-Media-Photo-loadFullImageOnZoom Enables loading the full resolution image on zoom in the ligthbox (preview). (default: true)
|
||||
Client-MetaFile-enabled (default: true)
|
||||
Client-Faces-enabled (default: true)
|
||||
Client-Faces-keywordsToPersons (default: true)
|
||||
@ -196,7 +214,7 @@ Environmental variables:
|
||||
"Media": {
|
||||
"//[folder]": "Images are loaded from this folder (read permission required)",
|
||||
"folder": "demo/images",
|
||||
"//[tempFolder]": "Thumbnails, coverted photos, videos will be stored here (write permission required)",
|
||||
"//[tempFolder]": "Thumbnails, converted photos, videos will be stored here (write permission required)",
|
||||
"tempFolder": "demo/tmp",
|
||||
"Video": {
|
||||
"transcoding": {
|
||||
@ -226,6 +244,13 @@ Environmental variables:
|
||||
"personFaceMargin": 0.6
|
||||
}
|
||||
},
|
||||
"Preview": {
|
||||
"SearchQuery": null,
|
||||
"Sorting": [
|
||||
6,
|
||||
4
|
||||
]
|
||||
},
|
||||
"Threading": {
|
||||
"//[enabled]": "App can run on multiple thread",
|
||||
"enabled": true,
|
||||
@ -279,7 +304,9 @@ Environmental variables:
|
||||
{
|
||||
"name": "Indexing",
|
||||
"jobName": "Indexing",
|
||||
"config": {},
|
||||
"config": {
|
||||
"indexChangesOnly": true
|
||||
},
|
||||
"allowParallelRun": false,
|
||||
"trigger": {
|
||||
"type": "never"
|
||||
@ -351,15 +378,24 @@ Environmental variables:
|
||||
"maxItemsPerCategory": 5,
|
||||
"cacheTimeout": 3600000
|
||||
},
|
||||
"maxMediaResult": 2000,
|
||||
"maxMediaResult": 10000,
|
||||
"//[listDirectories]": "Search returns also with directories, not just media",
|
||||
"listDirectories": false,
|
||||
"//[listMetafiles]": "Search also returns with metafiles from directories that contain a media file of the matched search result",
|
||||
"listMetafiles": false,
|
||||
"maxDirectoryResult": 200
|
||||
},
|
||||
"Sharing": {
|
||||
"enabled": true,
|
||||
"passwordProtected": true
|
||||
},
|
||||
"Album": {
|
||||
"enabled": true
|
||||
},
|
||||
"Map": {
|
||||
"enabled": true,
|
||||
"//[maxPreviewMarkers]": "Maximum number of markers to be shown on the map preview on the gallery page.",
|
||||
"maxPreviewMarkers": 50,
|
||||
"useImageMarkers": true,
|
||||
"mapProvider": "OpenStreetMap",
|
||||
"mapboxAccessToken": "",
|
||||
@ -377,11 +413,14 @@ Environmental variables:
|
||||
"enableCache": true,
|
||||
"enableOnScrollRendering": true,
|
||||
"defaultPhotoSortingMethod": "ascDate",
|
||||
"//[enableDirectorySortingByDate]": "If enabled directories will be sorted by date, like photos, otherwise by name. Directory date is the last modification time of that directory not the creation date of the oldest photo",
|
||||
"enableDirectorySortingByDate": false,
|
||||
"enableOnScrollThumbnailPrioritising": true,
|
||||
"NavBar": {
|
||||
"showItemCount": true
|
||||
},
|
||||
"captionFirstNaming": false
|
||||
"captionFirstNaming": false,
|
||||
"enableDownloadZip": false
|
||||
},
|
||||
"authenticationRequired": true,
|
||||
"unAuthenticatedUserRole": "Admin",
|
||||
@ -400,7 +439,9 @@ Environmental variables:
|
||||
"Photo": {
|
||||
"Converting": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"//[loadFullImageOnZoom]": "Enables loading the full resolution image on zoom in the ligthbox (preview).",
|
||||
"loadFullImageOnZoom": true
|
||||
}
|
||||
},
|
||||
"MetaFile": {
|
||||
@ -413,5 +454,4 @@ Environmental variables:
|
||||
"readAccessMinRole": "User"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
}```
|
@ -11,6 +11,7 @@ import {IJobManager} from './database/interfaces/IJobManager';
|
||||
import {LocationManager} from './database/LocationManager';
|
||||
import {IAlbumManager} from './database/interfaces/IAlbumManager';
|
||||
import {JobManager} from './jobs/JobManager';
|
||||
import {IPreviewManager} from './database/interfaces/IPreviewManager';
|
||||
|
||||
const LOG_TAG = '[ObjectManagers]';
|
||||
|
||||
@ -24,6 +25,7 @@ export class ObjectManagers {
|
||||
private sharingManager: ISharingManager;
|
||||
private indexingManager: IIndexingManager;
|
||||
private personManager: IPersonManager;
|
||||
private previewManager: IPreviewManager;
|
||||
private versionManager: IVersionManager;
|
||||
private jobManager: IJobManager;
|
||||
private locationManager: LocationManager;
|
||||
@ -61,6 +63,13 @@ export class ObjectManagers {
|
||||
set PersonManager(value: IPersonManager) {
|
||||
this.personManager = value;
|
||||
}
|
||||
get PreviewManager(): IPreviewManager {
|
||||
return this.previewManager;
|
||||
}
|
||||
|
||||
set PreviewManager(value: IPreviewManager) {
|
||||
this.previewManager = value;
|
||||
}
|
||||
|
||||
get IndexingManager(): IIndexingManager {
|
||||
return this.indexingManager;
|
||||
@ -150,6 +159,7 @@ export class ObjectManagers {
|
||||
ObjectManagers.getInstance().GalleryManager = new (require(`./database/${type}/GalleryManager`).GalleryManager)();
|
||||
ObjectManagers.getInstance().IndexingManager = new (require(`./database/${type}/IndexingManager`).IndexingManager)();
|
||||
ObjectManagers.getInstance().PersonManager = new (require(`./database/${type}/PersonManager`).PersonManager)();
|
||||
ObjectManagers.getInstance().PreviewManager = new (require(`./database/${type}/PreviewManager`).PreviewManager)();
|
||||
ObjectManagers.getInstance().SearchManager = new (require(`./database/${type}/SearchManager`).SearchManager)();
|
||||
ObjectManagers.getInstance().SharingManager = new (require(`./database/${type}/SharingManager`).SharingManager)();
|
||||
ObjectManagers.getInstance().UserManager = new (require(`./database/${type}/UserManager`).UserManager)();
|
||||
|
13
src/backend/model/database/interfaces/IPreviewManager.ts
Normal file
13
src/backend/model/database/interfaces/IPreviewManager.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {SavedSearchDTO} from '../../../../common/entities/album/SavedSearchDTO';
|
||||
import {PreviewPhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
|
||||
export interface IPreviewManager {
|
||||
getPreviewForDirectory(dir: { id: number, name: string, path: string }): Promise<PreviewPhotoDTOWithID>;
|
||||
|
||||
getAlbumPreview(album: SavedSearchDTO): Promise<PreviewPhotoDTOWithID>;
|
||||
}
|
||||
|
||||
// ID is need within the backend so it can be saved to DB (ID is the external key)
|
||||
export interface PreviewPhotoDTOWithID extends PreviewPhotoDTO {
|
||||
id: number;
|
||||
}
|
13
src/backend/model/database/memory/PreviewManager.ts
Normal file
13
src/backend/model/database/memory/PreviewManager.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {IPreviewManager} from '../interfaces/IPreviewManager';
|
||||
import {DirectoryPathDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {SavedSearchDTO} from '../../../../common/entities/album/SavedSearchDTO';
|
||||
|
||||
export class PreviewManager implements IPreviewManager {
|
||||
getAlbumPreview(album: SavedSearchDTO): Promise<MediaDTO> {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
getPreviewForDirectory(dir: DirectoryPathDTO): Promise<MediaDTO> {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
@ -39,9 +39,13 @@ export class AlbumManager implements IAlbumManager {
|
||||
|
||||
public async getAlbums(): Promise<AlbumBaseDTO[]> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
return await connection.getRepository(AlbumBaseEntity).find({
|
||||
relations: ['preview', 'preview.directory']
|
||||
});
|
||||
return await connection.getRepository(AlbumBaseEntity)
|
||||
.createQueryBuilder('album')
|
||||
.innerJoin('album.preview', 'preview')
|
||||
.innerJoin('preview.directory', 'directory')
|
||||
.select(['album', 'preview.name',
|
||||
'directory.name',
|
||||
'directory.path']).getMany();
|
||||
}
|
||||
|
||||
public async onGalleryIndexUpdate(): Promise<void> {
|
||||
@ -59,10 +63,11 @@ export class AlbumManager implements IAlbumManager {
|
||||
|
||||
private async updateAlbum(album: SavedSearchEntity): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const preview = await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
|
||||
.getPreview((album as SavedSearchDTO).searchQuery);
|
||||
const preview = await ObjectManagers.getInstance().PreviewManager
|
||||
.getAlbumPreview(album);
|
||||
const count = await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
|
||||
.getCount((album as SavedSearchDTO).searchQuery);
|
||||
|
||||
await connection
|
||||
.createQueryBuilder()
|
||||
.update(AlbumBaseEntity)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {IGalleryManager} from '../interfaces/IGalleryManager';
|
||||
import {DirectoryPathDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import {DirectoryEntity} from './enitites/DirectoryEntity';
|
||||
@ -17,7 +17,7 @@ import {Logger} from '../../../Logger';
|
||||
import {FaceRegionEntry} from './enitites/FaceRegionEntry';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {DuplicatesDTO} from '../../../../common/entities/DuplicatesDTO';
|
||||
import {DatabaseType, ReIndexingSensitivity} from '../../../../common/config/private/PrivateConfig';
|
||||
import {ReIndexingSensitivity} from '../../../../common/config/private/PrivateConfig';
|
||||
|
||||
|
||||
const LOG_TAG = '[GalleryManager]';
|
||||
@ -63,7 +63,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
|
||||
const ret = await ObjectManagers.getInstance().IndexingManager.indexDirectory(relativeDirectoryName);
|
||||
for (const subDir of ret.directories) {
|
||||
if (!subDir.preview) { // if sub directories does not have photos, so cannot show a preview, try get one from DB
|
||||
await this.fillPreviewFromSubDir(connection, subDir);
|
||||
await this.fillPreviewForSubDir(connection, subDir);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
@ -216,31 +216,22 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
|
||||
return await query.getOne();
|
||||
}
|
||||
|
||||
public async fillPreviewForSubDir(connection: Connection, dir: DirectoryEntity): Promise<void> {
|
||||
/**
|
||||
* Sets preview for the directory
|
||||
*/
|
||||
public async fillPreviewForSubDir(connection: Connection, dir: SubDirectoryDTO): Promise<void> {
|
||||
|
||||
dir.media = [];
|
||||
dir.preview = await connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoinAndSelect('media.directory', 'directory')
|
||||
.where('media.directory = :dir', {
|
||||
dir: dir.id
|
||||
})
|
||||
.orderBy('media.metadata.creationDate', 'DESC')
|
||||
.limit(1)
|
||||
.getOne();
|
||||
dir.preview = await ObjectManagers.getInstance().PreviewManager.getPreviewForDirectory(dir);
|
||||
dir.isPartial = true;
|
||||
|
||||
|
||||
if (dir.preview) {
|
||||
dir.preview.directory = dir;
|
||||
dir.preview.readyThumbnails = [];
|
||||
dir.preview.readyIcon = false;
|
||||
} else {
|
||||
await this.fillPreviewFromSubDir(connection, dir);
|
||||
}
|
||||
}
|
||||
|
||||
protected async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<DirectoryEntity> {
|
||||
protected async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<ParentDirectoryDTO> {
|
||||
const query = connection
|
||||
.getRepository(DirectoryEntity)
|
||||
.createQueryBuilder('directory')
|
||||
@ -249,8 +240,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
|
||||
path: directoryParent
|
||||
})
|
||||
.leftJoinAndSelect('directory.directories', 'directories')
|
||||
.leftJoinAndSelect('directory.media', 'media')
|
||||
.orderBy('media.metadata.creationDate', 'DESC');
|
||||
.leftJoinAndSelect('directory.media', 'media');
|
||||
|
||||
if (Config.Client.MetaFile.enabled === true) {
|
||||
query.leftJoinAndSelect('directory.metaFile', 'metaFile');
|
||||
@ -259,33 +249,8 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
|
||||
return await query.getOne();
|
||||
}
|
||||
|
||||
protected async fillPreviewFromSubDir(connection: Connection, dir: SubDirectoryDTO): Promise<void> {
|
||||
dir.media = [];
|
||||
const query = connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoinAndSelect('media.directory', 'directory');
|
||||
|
||||
if (Config.Server.Database.type === DatabaseType.mysql) {
|
||||
query.where('directory.path like :path || \'%\'', {
|
||||
path: (DiskMangerWorker.pathFromParent(dir))
|
||||
});
|
||||
} else {
|
||||
query.where('directory.path GLOB :path', {
|
||||
path: DiskMangerWorker.pathFromParent(dir) + '*'
|
||||
});
|
||||
}
|
||||
dir.preview = await query.orderBy('media.metadata.creationDate', 'DESC')
|
||||
.limit(1)
|
||||
.getOne();
|
||||
|
||||
if (dir.preview) {
|
||||
dir.preview.readyThumbnails = [];
|
||||
dir.preview.readyIcon = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async fillParentDir(connection: Connection, dir: DirectoryEntity): Promise<void> {
|
||||
protected async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise<void> {
|
||||
if (dir.media) {
|
||||
const indexedFaces = await connection.getRepository(FaceRegionEntry)
|
||||
.createQueryBuilder('face')
|
||||
@ -306,11 +271,6 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
|
||||
.filter((fe): boolean => fe.media.id === item.id)
|
||||
.map((f): { name: any; box: any } => ({box: f.box, name: f.person.name}));
|
||||
}
|
||||
if (dir.media.length > 0) {
|
||||
dir.preview = dir.media[0];
|
||||
} else {
|
||||
await this.fillPreviewFromSubDir(connection, dir);
|
||||
}
|
||||
}
|
||||
if (dir.metaFile) {
|
||||
for (const item of dir.metaFile) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {ISearchManager} from '../interfaces/ISearchManager';
|
||||
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {Brackets} from 'typeorm';
|
||||
|
||||
export interface ISQLSearchManager extends ISearchManager {
|
||||
autocomplete(text: string, type: SearchQueryTypes): Promise<AutoCompleteItem[]>;
|
||||
@ -13,6 +13,7 @@ export interface ISQLSearchManager extends ISearchManager {
|
||||
getRandomPhoto(queryFilter: SearchQueryDTO): Promise<PhotoDTO>;
|
||||
|
||||
// "Protected" functions. only called from other Managers, not from middlewares
|
||||
getPreview(query: SearchQueryDTO): Promise<MediaDTO>;
|
||||
getCount(query: SearchQueryDTO): Promise<number>;
|
||||
|
||||
prepareAndBuildWhereQuery(query: SearchQueryDTO, directoryOnly?: boolean): Promise<Brackets>;
|
||||
}
|
||||
|
131
src/backend/model/database/sql/PreviewManager.ts
Normal file
131
src/backend/model/database/sql/PreviewManager.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import {Config} from '../../../../common/config/private/Config';
|
||||
import {Brackets, SelectQueryBuilder, WhereExpression} from 'typeorm';
|
||||
import {MediaEntity} from './enitites/MediaEntity';
|
||||
import {DiskMangerWorker} from '../../threading/DiskMangerWorker';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {DatabaseType} from '../../../../common/config/private/PrivateConfig';
|
||||
import {SortingMethods} from '../../../../common/entities/SortingMethods';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {IPreviewManager, PreviewPhotoDTOWithID} from '../interfaces/IPreviewManager';
|
||||
import {SQLConnection} from './SQLConnection';
|
||||
import {SavedSearchDTO} from '../../../../common/entities/album/SavedSearchDTO';
|
||||
|
||||
|
||||
const LOG_TAG = '[PreviewManager]';
|
||||
|
||||
export class PreviewManager implements IPreviewManager {
|
||||
private static DIRECTORY_SELECT = ['directory.name', 'directory.path'];
|
||||
|
||||
private static setSorting<T>(query: SelectQueryBuilder<T>): SelectQueryBuilder<T> {
|
||||
|
||||
for (const sort of Config.Server.Preview.Sorting) {
|
||||
switch (sort) {
|
||||
case SortingMethods.descDate:
|
||||
query.addOrderBy('media.creationDate', 'DESC');
|
||||
break;
|
||||
case SortingMethods.ascDate:
|
||||
query.addOrderBy('media.creationDate', 'ASC');
|
||||
break;
|
||||
case SortingMethods.descRating:
|
||||
query.addOrderBy('media.rating', 'DESC');
|
||||
break;
|
||||
case SortingMethods.ascRating:
|
||||
query.addOrderBy('media.rating', 'ASC');
|
||||
break;
|
||||
case SortingMethods.descName:
|
||||
query.addOrderBy('media.name', 'ASC');
|
||||
break;
|
||||
case SortingMethods.ascName:
|
||||
query.addOrderBy('media.name', 'ASC');
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public async getAlbumPreview(album: SavedSearchDTO): Promise<PreviewPhotoDTOWithID> {
|
||||
|
||||
const albumQuery = await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager).prepareAndBuildWhereQuery(album.searchQuery);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const previewQuery = async (): Promise<SelectQueryBuilder<MediaEntity>> => {
|
||||
const query = connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.select(['media.name', 'media.id', ...PreviewManager.DIRECTORY_SELECT])
|
||||
.where(albumQuery);
|
||||
PreviewManager.setSorting(query);
|
||||
return query;
|
||||
};
|
||||
|
||||
let previewMedia = null;
|
||||
if (Config.Server.Preview.SearchQuery) {
|
||||
previewMedia = await (await previewQuery())
|
||||
.andWhere(await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
|
||||
.prepareAndBuildWhereQuery(Config.Server.Preview.SearchQuery))
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
|
||||
if (!previewMedia) {
|
||||
previewMedia = await (await previewQuery())
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
return previewMedia || null;
|
||||
}
|
||||
|
||||
public async getPreviewForDirectory(dir: { id: number, name: string, path: string }): Promise<PreviewPhotoDTOWithID> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const previewQuery = (): SelectQueryBuilder<MediaEntity> => {
|
||||
const query = connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.select(['media.name', 'media.id', ...PreviewManager.DIRECTORY_SELECT])
|
||||
.where(new Brackets((q: WhereExpression) => {
|
||||
q.where('media.directory = :dir', {
|
||||
dir: dir.id
|
||||
});
|
||||
if (Config.Server.Database.type === DatabaseType.mysql) {
|
||||
q.orWhere('directory.path like :path || \'%\'', {
|
||||
path: (DiskMangerWorker.pathFromParent(dir))
|
||||
});
|
||||
} else {
|
||||
q.orWhere('directory.path GLOB :path', {
|
||||
path: DiskMangerWorker.pathFromParent(dir) + '*'
|
||||
});
|
||||
}
|
||||
}));
|
||||
if (Config.Server.Database.type === DatabaseType.mysql) {
|
||||
query.orderBy('CHAR_LENGTH(directory.path)', 'DESC'); // shorter the path, its higher up in the hierarchy
|
||||
} else {
|
||||
query.orderBy('LENGTH(directory.path)', 'DESC'); // shorter the path, its higher up in the hierarchy
|
||||
}
|
||||
|
||||
|
||||
PreviewManager.setSorting(query);
|
||||
return query;
|
||||
};
|
||||
|
||||
let previewMedia = null;
|
||||
if (Config.Server.Preview.SearchQuery) {
|
||||
previewMedia = await previewQuery()
|
||||
.andWhere(await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
|
||||
.prepareAndBuildWhereQuery(Config.Server.Preview.SearchQuery))
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
|
||||
if (!previewMedia) {
|
||||
previewMedia = await previewQuery()
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
return previewMedia || null;
|
||||
}
|
||||
|
||||
}
|
@ -31,7 +31,6 @@ import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {DatabaseType} from '../../../../common/config/private/PrivateConfig';
|
||||
import {ISQLGalleryManager} from './IGalleryManager';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {FileEntity} from './enitites/FileEntity';
|
||||
|
||||
@ -231,7 +230,7 @@ export class SearchManager implements ISQLSearchManager {
|
||||
.createQueryBuilder('media')
|
||||
.select(['media', ...this.DIRECTORY_SELECT])
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.where(this.buildWhereQuery(query));
|
||||
.where(await this.prepareAndBuildWhereQuery(query));
|
||||
|
||||
|
||||
if (Config.Server.Database.type === DatabaseType.mysql) {
|
||||
@ -241,188 +240,30 @@ export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
}
|
||||
|
||||
public async getPreview(queryIN: SearchQueryDTO, ): Promise<MediaDTO> {
|
||||
const query = await this.prepareQuery(queryIN);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
return await connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.select(['media', ...this.DIRECTORY_SELECT])
|
||||
.where(this.buildWhereQuery(query))
|
||||
.orderBy('media.metadata.creationDate', 'DESC')
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
|
||||
public async getCount(queryIN: SearchQueryDTO): Promise<number> {
|
||||
const query = await this.prepareQuery(queryIN);
|
||||
public async getCount(query: SearchQueryDTO): Promise<number> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
return await connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.where(this.buildWhereQuery(query))
|
||||
.where(await this.prepareAndBuildWhereQuery(query))
|
||||
.getCount();
|
||||
}
|
||||
|
||||
protected flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO {
|
||||
switch (query.type) {
|
||||
case SearchQueryTypes.AND:
|
||||
case SearchQueryTypes.OR:
|
||||
return {
|
||||
type: query.type,
|
||||
list: (query as SearchListQuery).list.map((q): SearchQueryDTO => this.flattenSameOfQueries(q))
|
||||
} as SearchListQuery;
|
||||
case SearchQueryTypes.SOME_OF:
|
||||
const someOfQ = query as SomeOfSearchQuery;
|
||||
someOfQ.min = someOfQ.min || 1;
|
||||
|
||||
if (someOfQ.min === 1) {
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: (someOfQ as SearchListQuery).list
|
||||
} as ORSearchQuery);
|
||||
}
|
||||
|
||||
if (someOfQ.min === (query as SearchListQuery).list.length) {
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.AND,
|
||||
list: (someOfQ as SearchListQuery).list
|
||||
} as ANDSearchQuery);
|
||||
}
|
||||
|
||||
const getAllCombinations = (num: number, arr: SearchQueryDTO[], start = 0): SearchQueryDTO[] => {
|
||||
if (num <= 0 || num > arr.length || start >= arr.length) {
|
||||
return null;
|
||||
}
|
||||
if (num <= 1) {
|
||||
return arr.slice(start);
|
||||
}
|
||||
if (num === arr.length - start) {
|
||||
return [{
|
||||
type: SearchQueryTypes.AND,
|
||||
list: arr.slice(start)
|
||||
} as ANDSearchQuery];
|
||||
}
|
||||
const ret: ANDSearchQuery[] = [];
|
||||
for (let i = start; i < arr.length; ++i) {
|
||||
const subRes = getAllCombinations(num - 1, arr, i + 1);
|
||||
if (subRes === null) {
|
||||
break;
|
||||
}
|
||||
const and: ANDSearchQuery = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: [
|
||||
arr[i]
|
||||
]
|
||||
};
|
||||
if (subRes.length === 1) {
|
||||
if (subRes[0].type === SearchQueryTypes.AND) {
|
||||
and.list.push(...(subRes[0] as ANDSearchQuery).list);
|
||||
} else {
|
||||
and.list.push(subRes[0]);
|
||||
}
|
||||
} else {
|
||||
and.list.push({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: subRes
|
||||
} as ORSearchQuery);
|
||||
}
|
||||
ret.push(and);
|
||||
|
||||
}
|
||||
|
||||
if (ret.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: getAllCombinations(someOfQ.min, (query as SearchListQuery).list)
|
||||
} as ORSearchQuery);
|
||||
|
||||
}
|
||||
return query;
|
||||
public async prepareAndBuildWhereQuery(queryIN: SearchQueryDTO, directoryOnly = false): Promise<Brackets> {
|
||||
const query = await this.prepareQuery(queryIN);
|
||||
return this.buildWhereQuery(query, directoryOnly);
|
||||
}
|
||||
|
||||
private async prepareQuery(queryIN: SearchQueryDTO): Promise<SearchQueryDTO> {
|
||||
public async prepareQuery(queryIN: SearchQueryDTO): Promise<SearchQueryDTO> {
|
||||
let query: SearchQueryDTO = this.assignQueryIDs(Utils.clone(queryIN)); // assign local ids before flattening SOME_OF queries
|
||||
query = this.flattenSameOfQueries(query);
|
||||
query = await this.getGPSData(query);
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigning IDs to search queries. It is a help / simplification to typeorm,
|
||||
* so less parameters are needed to pass down to SQL.
|
||||
* Witch SOME_OF query the number of WHERE constrains have O(N!) complexity
|
||||
*/
|
||||
private assignQueryIDs(queryIN: SearchQueryDTO, id = {value: 1}): SearchQueryDTO {
|
||||
if ((queryIN as SearchListQuery).list) {
|
||||
(queryIN as SearchListQuery).list.forEach(q => this.assignQueryIDs(q, id));
|
||||
return queryIN;
|
||||
}
|
||||
(queryIN as SearchQueryDTOWithID).queryId = id.value;
|
||||
id.value++;
|
||||
return queryIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only those part of a query tree that only contains directory related search queries
|
||||
*/
|
||||
private filterDirectoryQuery(query: SearchQueryDTO): SearchQueryDTO {
|
||||
switch (query.type) {
|
||||
case SearchQueryTypes.AND:
|
||||
const andRet = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: (query as SearchListQuery).list.map(q => this.filterDirectoryQuery(q))
|
||||
} as ANDSearchQuery;
|
||||
// if any of the queries contain non dir query thw whole and query is a non dir query
|
||||
if (andRet.list.indexOf(null) !== -1) {
|
||||
return null;
|
||||
}
|
||||
return andRet;
|
||||
|
||||
case SearchQueryTypes.OR:
|
||||
const orRet = {
|
||||
type: SearchQueryTypes.OR,
|
||||
list: (query as SearchListQuery).list.map(q => this.filterDirectoryQuery(q)).filter(q => q !== null)
|
||||
} as ORSearchQuery;
|
||||
if (orRet.list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return orRet;
|
||||
|
||||
case SearchQueryTypes.any_text:
|
||||
case SearchQueryTypes.directory:
|
||||
return query;
|
||||
|
||||
case SearchQueryTypes.SOME_OF:
|
||||
throw new Error('"Some of" queries should have been already flattened');
|
||||
}
|
||||
// of none of the above, its not a directory search
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getGPSData(query: SearchQueryDTO): Promise<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 as DistanceSearch).from.text) {
|
||||
(query as DistanceSearch).from.GPSData =
|
||||
await ObjectManagers.getInstance().LocationManager.getGPSData((query as DistanceSearch).from.text);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL Where query from search query
|
||||
* @param query input search query
|
||||
@ -430,7 +271,7 @@ export class SearchManager implements ISQLSearchManager {
|
||||
* @param directoryOnly Only builds directory related queries
|
||||
* @private
|
||||
*/
|
||||
private buildWhereQuery(query: SearchQueryDTO, directoryOnly = false): Brackets {
|
||||
public buildWhereQuery(query: SearchQueryDTO, directoryOnly = false): Brackets {
|
||||
const queryId = (query as SearchQueryDTOWithID).queryId;
|
||||
switch (query.type) {
|
||||
case SearchQueryTypes.AND:
|
||||
@ -724,6 +565,155 @@ export class SearchManager implements ISQLSearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
protected flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO {
|
||||
switch (query.type) {
|
||||
case SearchQueryTypes.AND:
|
||||
case SearchQueryTypes.OR:
|
||||
return {
|
||||
type: query.type,
|
||||
list: (query as SearchListQuery).list.map((q): SearchQueryDTO => this.flattenSameOfQueries(q))
|
||||
} as SearchListQuery;
|
||||
case SearchQueryTypes.SOME_OF:
|
||||
const someOfQ = query as SomeOfSearchQuery;
|
||||
someOfQ.min = someOfQ.min || 1;
|
||||
|
||||
if (someOfQ.min === 1) {
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: (someOfQ as SearchListQuery).list
|
||||
} as ORSearchQuery);
|
||||
}
|
||||
|
||||
if (someOfQ.min === (query as SearchListQuery).list.length) {
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.AND,
|
||||
list: (someOfQ as SearchListQuery).list
|
||||
} as ANDSearchQuery);
|
||||
}
|
||||
|
||||
const getAllCombinations = (num: number, arr: SearchQueryDTO[], start = 0): SearchQueryDTO[] => {
|
||||
if (num <= 0 || num > arr.length || start >= arr.length) {
|
||||
return null;
|
||||
}
|
||||
if (num <= 1) {
|
||||
return arr.slice(start);
|
||||
}
|
||||
if (num === arr.length - start) {
|
||||
return [{
|
||||
type: SearchQueryTypes.AND,
|
||||
list: arr.slice(start)
|
||||
} as ANDSearchQuery];
|
||||
}
|
||||
const ret: ANDSearchQuery[] = [];
|
||||
for (let i = start; i < arr.length; ++i) {
|
||||
const subRes = getAllCombinations(num - 1, arr, i + 1);
|
||||
if (subRes === null) {
|
||||
break;
|
||||
}
|
||||
const and: ANDSearchQuery = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: [
|
||||
arr[i]
|
||||
]
|
||||
};
|
||||
if (subRes.length === 1) {
|
||||
if (subRes[0].type === SearchQueryTypes.AND) {
|
||||
and.list.push(...(subRes[0] as ANDSearchQuery).list);
|
||||
} else {
|
||||
and.list.push(subRes[0]);
|
||||
}
|
||||
} else {
|
||||
and.list.push({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: subRes
|
||||
} as ORSearchQuery);
|
||||
}
|
||||
ret.push(and);
|
||||
|
||||
}
|
||||
|
||||
if (ret.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: getAllCombinations(someOfQ.min, (query as SearchListQuery).list)
|
||||
} as ORSearchQuery);
|
||||
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigning IDs to search queries. It is a help / simplification to typeorm,
|
||||
* so less parameters are needed to pass down to SQL.
|
||||
* Witch SOME_OF query the number of WHERE constrains have O(N!) complexity
|
||||
*/
|
||||
private assignQueryIDs(queryIN: SearchQueryDTO, id = {value: 1}): SearchQueryDTO {
|
||||
if ((queryIN as SearchListQuery).list) {
|
||||
(queryIN as SearchListQuery).list.forEach(q => this.assignQueryIDs(q, id));
|
||||
return queryIN;
|
||||
}
|
||||
(queryIN as SearchQueryDTOWithID).queryId = id.value;
|
||||
id.value++;
|
||||
return queryIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only those part of a query tree that only contains directory related search queries
|
||||
*/
|
||||
private filterDirectoryQuery(query: SearchQueryDTO): SearchQueryDTO {
|
||||
switch (query.type) {
|
||||
case SearchQueryTypes.AND:
|
||||
const andRet = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: (query as SearchListQuery).list.map(q => this.filterDirectoryQuery(q))
|
||||
} as ANDSearchQuery;
|
||||
// if any of the queries contain non dir query thw whole and query is a non dir query
|
||||
if (andRet.list.indexOf(null) !== -1) {
|
||||
return null;
|
||||
}
|
||||
return andRet;
|
||||
|
||||
case SearchQueryTypes.OR:
|
||||
const orRet = {
|
||||
type: SearchQueryTypes.OR,
|
||||
list: (query as SearchListQuery).list.map(q => this.filterDirectoryQuery(q)).filter(q => q !== null)
|
||||
} as ORSearchQuery;
|
||||
if (orRet.list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return orRet;
|
||||
|
||||
case SearchQueryTypes.any_text:
|
||||
case SearchQueryTypes.directory:
|
||||
return query;
|
||||
|
||||
case SearchQueryTypes.SOME_OF:
|
||||
throw new Error('"Some of" queries should have been already flattened');
|
||||
}
|
||||
// of none of the above, its not a directory search
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getGPSData(query: SearchQueryDTO): Promise<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 as DistanceSearch).from.text) {
|
||||
(query as DistanceSearch).from.GPSData =
|
||||
await ObjectManagers.getInstance().LocationManager.getGPSData((query as DistanceSearch).from.text);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private encapsulateAutoComplete(values: string[], type: SearchQueryTypes): Array<AutoCompleteItem> {
|
||||
const res: AutoCompleteItem[] = [];
|
||||
values.forEach((value): void => {
|
||||
|
@ -5,6 +5,8 @@ import {ClientConfig} from '../public/ClientConfig';
|
||||
import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
|
||||
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import {DefaultsJobs} from '../../entities/job/JobDTO';
|
||||
import {SearchQueryDTO} from '../../entities/SearchQueryDTO';
|
||||
import {SortingMethods} from '../../entities/SortingMethods';
|
||||
|
||||
export enum DatabaseType {
|
||||
memory = 1, mysql = 2, sqlite = 3
|
||||
@ -295,6 +297,17 @@ export class ServerPhotoConfig {
|
||||
Converting: PhotoConvertingConfig = new PhotoConvertingConfig();
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerPreviewConfig {
|
||||
@ConfigProperty({type: 'object'})
|
||||
SearchQuery: SearchQueryDTO = null;
|
||||
@ConfigProperty({arrayType: SortingMethods})
|
||||
Sorting: SortingMethods[] = [
|
||||
SortingMethods.descRating,
|
||||
SortingMethods.descDate
|
||||
];
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ServerMediaConfig {
|
||||
@ConfigProperty({description: 'Images are loaded from this folder (read permission required)'})
|
||||
@ -337,6 +350,8 @@ export class ServerConfig {
|
||||
@ConfigProperty()
|
||||
Media: ServerMediaConfig = new ServerMediaConfig();
|
||||
@ConfigProperty()
|
||||
Preview: ServerPreviewConfig = new ServerPreviewConfig();
|
||||
@ConfigProperty()
|
||||
Threading: ServerThreadingConfig = new ServerThreadingConfig();
|
||||
@ConfigProperty()
|
||||
Database: ServerDataBaseConfig = new ServerDataBaseConfig();
|
||||
|
@ -91,7 +91,7 @@ export const DirectoryDTOUtils = {
|
||||
dir.preview.directory = {
|
||||
path: dir.preview.directory.path,
|
||||
name: dir.preview.directory.name,
|
||||
};
|
||||
} as DirectoryPathDTO;
|
||||
|
||||
// make sure that it is not a same object as one of the photo in the media[]
|
||||
// as the next foreach would remove the directory
|
||||
|
@ -25,11 +25,11 @@ class IndexingManagerTest extends IndexingManager {
|
||||
|
||||
class GalleryManagerTest extends GalleryManager {
|
||||
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<DirectoryEntity> {
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<ParentDirectoryDTO> {
|
||||
return super.selectParentDir(connection, directoryName, directoryParent);
|
||||
}
|
||||
|
||||
public async fillParentDir(connection: Connection, dir: DirectoryEntity): Promise<void> {
|
||||
public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise<void> {
|
||||
return super.fillParentDir(connection, dir);
|
||||
}
|
||||
}
|
||||
@ -83,7 +83,7 @@ export class DBTestHelper {
|
||||
};
|
||||
}
|
||||
|
||||
public static async persistTestDir(directory: DirectoryBaseDTO): Promise<DirectoryEntity> {
|
||||
public static async persistTestDir(directory: DirectoryBaseDTO): Promise<ParentDirectoryDTO> {
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
ObjectManagers.getInstance().IndexingManager.indexDirectory = () => Promise.resolve(null);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {DBTestHelper} from '../../../DBTestHelper';
|
||||
import { ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
import {TestHelper} from './TestHelper';
|
||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||
import {PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO';
|
||||
import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO';
|
||||
import {VideoDTO} from '../../../../../src/common/entities/VideoDTO';
|
||||
import {AlbumManager} from '../../../../../src/backend/model/database/sql/AlbumManager';
|
||||
import {SearchQueryTypes, TextSearch} from '../../../../../src/common/entities/SearchQueryDTO';
|
||||
@ -89,7 +89,9 @@ describe('AlbumManager', (sqlHelper: DBTestHelper) => {
|
||||
delete tmpDir.preview;
|
||||
delete tmpDir.metaFile;
|
||||
const ret = Utils.clone(m);
|
||||
delete (ret.metadata as PhotoMetadata).faces;
|
||||
delete ret.id;
|
||||
ret.directory = {path: ret.directory.path, name: ret.directory.name};
|
||||
delete ret.metadata;
|
||||
tmpDir.directories = tmpD;
|
||||
tmpDir.media = tmpM;
|
||||
tmpDir.preview = tmpP;
|
||||
|
@ -19,6 +19,7 @@ import {ProjectPath} from '../../../../../src/backend/ProjectPath';
|
||||
import * as path from 'path';
|
||||
import {DiskManager} from '../../../../../src/backend/model/DiskManger';
|
||||
import {AlbumManager} from '../../../../../src/backend/model/database/sql/AlbumManager';
|
||||
import {SortingMethods} from '../../../../../src/common/entities/SortingMethods';
|
||||
|
||||
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
|
||||
const chai = require('chai');
|
||||
@ -29,11 +30,11 @@ const {expect} = chai;
|
||||
class GalleryManagerTest extends GalleryManager {
|
||||
|
||||
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<DirectoryEntity> {
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<ParentDirectoryDTO> {
|
||||
return super.selectParentDir(connection, directoryName, directoryParent);
|
||||
}
|
||||
|
||||
public async fillParentDir(connection: Connection, dir: DirectoryEntity): Promise<void> {
|
||||
public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise<void> {
|
||||
return super.fillParentDir(connection, dir);
|
||||
}
|
||||
|
||||
@ -69,6 +70,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
|
||||
afterEach(async () => {
|
||||
Config.loadSync();
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating];
|
||||
await sqlHelper.clearDB();
|
||||
});
|
||||
|
||||
@ -82,7 +84,28 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
delete dir.media;
|
||||
};
|
||||
|
||||
const removeIds = (dir: DirectoryBaseDTO) => {
|
||||
const makePreview = (m: MediaDTO) => {
|
||||
delete (m.directory as ParentDirectoryDTO).id;
|
||||
delete m.metadata;
|
||||
return m;
|
||||
};
|
||||
|
||||
const indexifyReturn = (dir: DirectoryBaseDTO): DirectoryBaseDTO => {
|
||||
const d = Utils.clone(dir);
|
||||
|
||||
delete d.preview;
|
||||
if (d.directories) {
|
||||
for (const subD of d.directories) {
|
||||
if (subD.preview) {
|
||||
delete subD.preview.metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return d;
|
||||
};
|
||||
|
||||
const removeIds = (dir: DirectoryBaseDTO): DirectoryBaseDTO => {
|
||||
delete dir.id;
|
||||
dir.media.forEach((media: MediaDTO) => {
|
||||
delete media.id;
|
||||
@ -104,6 +127,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(directory);
|
||||
});
|
||||
}
|
||||
|
||||
return dir;
|
||||
};
|
||||
|
||||
it('should support case sensitive file names', async () => {
|
||||
@ -124,9 +149,9 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
await gm.fillParentDir(conn, selected);
|
||||
|
||||
DirectoryDTOUtils.packDirectory(selected);
|
||||
removeIds(selected);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equal(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(removeIds(selected))))
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
|
||||
@ -153,7 +178,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
setPartial(subDir1);
|
||||
setPartial(subDir2);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
it('should support case sensitive directory path', async () => {
|
||||
@ -182,7 +207,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(selected);
|
||||
setPartial(subDir1);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent1)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent1)));
|
||||
}
|
||||
{
|
||||
const selected = await gm.selectParentDir(conn, parent2.name, parent2.path);
|
||||
@ -192,7 +217,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(selected);
|
||||
setPartial(subDir2);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent2)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent2)));
|
||||
}
|
||||
});
|
||||
|
||||
@ -217,7 +242,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
|
||||
const checkParent = async () => {
|
||||
const selected = await selectDirectory(gm, parent);
|
||||
const cloned = Utils.removeNullOrEmptyObj(Utils.clone(parent));
|
||||
const cloned = Utils.removeNullOrEmptyObj(indexifyReturn(parent));
|
||||
if (cloned.directories) {
|
||||
cloned.directories.forEach(d => setPartial(d));
|
||||
}
|
||||
@ -263,8 +288,11 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
const subDir = TestHelper.getRandomizedDirectoryEntry(null, 'subDir');
|
||||
subDir.path = DiskMangerWorker.pathFromParent(parent);
|
||||
const sp1 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto1', 0);
|
||||
sp1.metadata.rating = 5;
|
||||
const sp2 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto2', 0);
|
||||
|
||||
sp2.metadata.rating = 3;
|
||||
subDir.preview = sp1;
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating];
|
||||
|
||||
DirectoryDTOUtils.packDirectory(subDir);
|
||||
await im.saveToDB(Utils.clone(subDir) as ParentDirectoryDTO);
|
||||
@ -283,7 +311,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(selected);
|
||||
setPartial(subDir);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
|
||||
@ -297,7 +325,11 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
const subDir = TestHelper.getRandomizedDirectoryEntry(null, 'subDir');
|
||||
subDir.path = DiskMangerWorker.pathFromParent(parent);
|
||||
const sp1 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto1', 0);
|
||||
sp1.metadata.rating = 5;
|
||||
const sp2 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto2', 0);
|
||||
sp2.metadata.rating = 3;
|
||||
subDir.preview = sp1;
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating];
|
||||
|
||||
|
||||
DirectoryDTOUtils.packDirectory(subDir);
|
||||
@ -317,7 +349,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(selected);
|
||||
setPartial(subDir);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
it('should save parent directory', async () => {
|
||||
@ -330,8 +362,11 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
const gpx = TestHelper.getRandomizedGPXEntry(parent, 'GPX1');
|
||||
const subDir = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir');
|
||||
const sp1 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto1', 0);
|
||||
sp1.metadata.rating = 5;
|
||||
const sp2 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto2', 0);
|
||||
|
||||
sp2.metadata.rating = 3;
|
||||
subDir.preview = sp1;
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating];
|
||||
|
||||
DirectoryDTOUtils.packDirectory(parent);
|
||||
await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO);
|
||||
@ -344,7 +379,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(selected);
|
||||
setPartial(subDir);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
|
||||
@ -381,7 +416,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
DirectoryDTOUtils.packDirectory(selected);
|
||||
removeIds(selected);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
it('should skip meta files', async () => {
|
||||
@ -404,7 +439,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
DirectoryDTOUtils.packDirectory(selected);
|
||||
removeIds(selected);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
it('should update sub directory', async () => {
|
||||
@ -439,7 +474,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
removeIds(selected);
|
||||
// selected.directories[0].parent = selected;
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(subDir)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(subDir)));
|
||||
});
|
||||
|
||||
it('should avoid race condition', async () => {
|
||||
@ -453,8 +488,11 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
const gpx = TestHelper.getRandomizedGPXEntry(parent, 'GPX1');
|
||||
const subDir = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir');
|
||||
const sp1 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto1', 1);
|
||||
sp1.metadata.rating = 5;
|
||||
const sp2 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto2', 1);
|
||||
|
||||
sp2.metadata.rating = 3;
|
||||
subDir.preview = sp1;
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating];
|
||||
|
||||
DirectoryDTOUtils.packDirectory(parent);
|
||||
const s1 = im.queueForSave(Utils.clone(parent) as ParentDirectoryDTO);
|
||||
@ -473,7 +511,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
delete sp1.metadata.faces;
|
||||
delete sp2.metadata.faces;
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equalInAnyOrder(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
});
|
||||
|
||||
it('should reset DB', async () => {
|
||||
@ -494,7 +532,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
DirectoryDTOUtils.packDirectory(selected);
|
||||
removeIds(selected);
|
||||
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
|
||||
.to.deep.equal(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
|
||||
.to.deep.equal(Utils.removeNullOrEmptyObj(indexifyReturn(parent)));
|
||||
|
||||
await im.resetDB();
|
||||
const selectReset = await gm.selectParentDir(conn, parent.name, parent.path);
|
||||
|
218
test/backend/unit/model/sql/PreviewManager.spec.ts
Normal file
218
test/backend/unit/model/sql/PreviewManager.spec.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import {SearchManager} from '../../../../../src/backend/model/database/sql/SearchManager';
|
||||
import {DBTestHelper} from '../../../DBTestHelper';
|
||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../src/common/entities/SearchQueryDTO';
|
||||
import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager';
|
||||
import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
import {TestHelper} from './TestHelper';
|
||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||
import {GalleryManager} from '../../../../../src/backend/model/database/sql/GalleryManager';
|
||||
import {Connection} from 'typeorm';
|
||||
import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO';
|
||||
import {VideoDTO} from '../../../../../src/common/entities/VideoDTO';
|
||||
import {FileDTO} from '../../../../../src/common/entities/FileDTO';
|
||||
import {PreviewManager} from '../../../../../src/backend/model/database/sql/PreviewManager';
|
||||
import {Config} from '../../../../../src/common/config/private/Config';
|
||||
import {SortingMethods} from '../../../../../src/common/entities/SortingMethods';
|
||||
import {Utils} from '../../../../../src/common/Utils';
|
||||
|
||||
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;
|
||||
declare const before: any;
|
||||
const tmpDescribe = describe;
|
||||
describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test)
|
||||
|
||||
|
||||
class IndexingManagerTest extends IndexingManager {
|
||||
|
||||
public async saveToDB(scannedDirectory: ParentDirectoryDTO): Promise<void> {
|
||||
return super.saveToDB(scannedDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchManagerTest extends SearchManager {
|
||||
|
||||
public flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO {
|
||||
return super.flattenSameOfQueries(query);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GalleryManagerTest extends GalleryManager {
|
||||
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<ParentDirectoryDTO> {
|
||||
return super.selectParentDir(connection, directoryName, directoryParent);
|
||||
}
|
||||
|
||||
public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise<void> {
|
||||
return super.fillParentDir(connection, dir);
|
||||
}
|
||||
}
|
||||
|
||||
describe('PreviewManager', (sqlHelper: DBTestHelper) => {
|
||||
describe = tmpDescribe;
|
||||
/**
|
||||
* dir
|
||||
* |-> subDir
|
||||
* |- pFaceLess
|
||||
* |- v
|
||||
* |- p
|
||||
* |- p2
|
||||
* |- gpx
|
||||
* |-> subDir2
|
||||
* |- p4
|
||||
*/
|
||||
|
||||
let dir: ParentDirectoryDTO;
|
||||
let subDir: SubDirectoryDTO;
|
||||
let subDir2: SubDirectoryDTO;
|
||||
let v: VideoDTO;
|
||||
let p: PhotoDTO;
|
||||
let p2: PhotoDTO;
|
||||
let pFaceLess: PhotoDTO;
|
||||
let p4: PhotoDTO;
|
||||
let gpx: FileDTO;
|
||||
|
||||
|
||||
const setUpTestGallery = async (): Promise<void> => {
|
||||
const directory: ParentDirectoryDTO = TestHelper.getDirectoryEntry();
|
||||
subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace');
|
||||
subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi');
|
||||
p = TestHelper.getPhotoEntry1(subDir);
|
||||
p.metadata.rating = 4;
|
||||
p.metadata.creationDate = 10000;
|
||||
p2 = TestHelper.getPhotoEntry2(subDir);
|
||||
p2.metadata.rating = 4;
|
||||
p2.metadata.creationDate = 20000;
|
||||
v = TestHelper.getVideoEntry1(subDir);
|
||||
v.metadata.creationDate = 500;
|
||||
gpx = TestHelper.getRandomizedGPXEntry(subDir);
|
||||
const pFaceLessTmp = TestHelper.getPhotoEntry3(subDir);
|
||||
pFaceLessTmp.metadata.rating = 0;
|
||||
pFaceLessTmp.metadata.creationDate = 400000;
|
||||
delete pFaceLessTmp.metadata.faces;
|
||||
p4 = TestHelper.getPhotoEntry4(subDir2);
|
||||
p4.metadata.rating = 5;
|
||||
p4.metadata.creationDate = 100;
|
||||
|
||||
dir = await DBTestHelper.persistTestDir(directory);
|
||||
|
||||
subDir = dir.directories[0];
|
||||
subDir2 = dir.directories[1];
|
||||
p = (subDir.media.filter(m => m.name === p.name)[0] as any);
|
||||
p2 = (subDir.media.filter(m => m.name === p2.name)[0] as any);
|
||||
gpx = (subDir.metaFile[0] as any);
|
||||
v = (subDir.media.filter(m => m.name === v.name)[0] as any);
|
||||
pFaceLess = (subDir.media.filter(m => m.name === pFaceLessTmp.name)[0] as any);
|
||||
p4 = (subDir2.media[0] as any);
|
||||
};
|
||||
|
||||
const setUpSqlDB = async () => {
|
||||
await sqlHelper.initDB();
|
||||
await setUpTestGallery();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
};
|
||||
|
||||
|
||||
before(async () => {
|
||||
await setUpSqlDB();
|
||||
});
|
||||
|
||||
|
||||
const previewifyMedia = <T extends FileDTO | PhotoDTO>(m: T): T => {
|
||||
const tmpDir: DirectoryBaseDTO = m.directory as DirectoryBaseDTO;
|
||||
const tmpM = tmpDir.media;
|
||||
const tmpD = tmpDir.directories;
|
||||
const tmpP = tmpDir.preview;
|
||||
const tmpMT = tmpDir.metaFile;
|
||||
delete tmpDir.directories;
|
||||
delete tmpDir.media;
|
||||
delete tmpDir.preview;
|
||||
delete tmpDir.metaFile;
|
||||
const ret = Utils.clone(m);
|
||||
delete (ret.directory as DirectoryBaseDTO).id;
|
||||
delete (ret.directory as DirectoryBaseDTO).lastScanned;
|
||||
delete (ret.directory as DirectoryBaseDTO).lastModified;
|
||||
delete (ret.directory as DirectoryBaseDTO).mediaCount;
|
||||
delete (ret as PhotoDTO).metadata;
|
||||
tmpDir.directories = tmpD;
|
||||
tmpDir.media = tmpM;
|
||||
tmpDir.preview = tmpP;
|
||||
tmpDir.metaFile = tmpMT;
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
after(async () => {
|
||||
await sqlHelper.clearDB();
|
||||
Config.Server.Preview.SearchQuery = null;
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating, SortingMethods.descDate];
|
||||
});
|
||||
|
||||
it('should sort directory preview', async () => {
|
||||
const pm = new PreviewManager();
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating, SortingMethods.descDate];
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(subDir))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descDate];
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(subDir))).to.deep.equalInAnyOrder(previewifyMedia(pFaceLess));
|
||||
Config.Server.Preview.Sorting = [SortingMethods.descRating];
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(dir))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
||||
});
|
||||
|
||||
it('should get preview for directory', async () => {
|
||||
const pm = new PreviewManager();
|
||||
|
||||
Config.Server.Preview.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Boba'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(subDir))).to.deep.equalInAnyOrder(previewifyMedia(p));
|
||||
Config.Server.Preview.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(subDir))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(dir))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
expect(Utils.clone(await pm.getPreviewForDirectory(subDir2))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
||||
|
||||
});
|
||||
|
||||
it('should get preview for saved search', async () => {
|
||||
const pm = new PreviewManager();
|
||||
Config.Server.Preview.SearchQuery = null;
|
||||
expect(Utils.clone(await pm.getAlbumPreview({
|
||||
name: 'test',
|
||||
id: 0,
|
||||
count: 0,
|
||||
locked: false,
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p4));
|
||||
Config.Server.Preview.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Boba'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumPreview({
|
||||
name: 'test',
|
||||
id: 0,
|
||||
count: 0,
|
||||
locked: false,
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p));
|
||||
Config.Server.Preview.SearchQuery = {type: SearchQueryTypes.any_text, text: 'Derem'} as TextSearch;
|
||||
expect(Utils.clone(await pm.getAlbumPreview({
|
||||
name: 'test',
|
||||
id: 0,
|
||||
count: 0,
|
||||
locked: false,
|
||||
searchQuery: {
|
||||
type: SearchQueryTypes.any_text,
|
||||
text: 'sw'
|
||||
} as TextSearch
|
||||
}))).to.deep.equalInAnyOrder(previewifyMedia(p2));
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -66,11 +66,11 @@ class SearchManagerTest extends SearchManager {
|
||||
|
||||
class GalleryManagerTest extends GalleryManager {
|
||||
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<DirectoryEntity> {
|
||||
public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<ParentDirectoryDTO> {
|
||||
return super.selectParentDir(connection, directoryName, directoryParent);
|
||||
}
|
||||
|
||||
public async fillParentDir(connection: Connection, dir: DirectoryEntity): Promise<void> {
|
||||
public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise<void> {
|
||||
return super.fillParentDir(connection, dir);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user