diff --git a/package-lock.json b/package-lock.json index a71cac10..9c2bdedb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "image-size": "1.0.2", "locale": "0.1.0", "node-geocoder": "4.2.0", + "nodemailer": "^6.9.4", "reflect-metadata": "0.1.13", "sharp": "0.31.3", "ts-exif-parser": "0.2.2", @@ -16598,6 +16599,14 @@ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/non-layered-tidy-tree-layout": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", @@ -35797,6 +35806,11 @@ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "dev": true }, + "nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==" + }, "non-layered-tidy-tree-layout": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", diff --git a/package.json b/package.json index 798fc1e4..ec080e78 100644 --- a/package.json +++ b/package.json @@ -46,13 +46,14 @@ "image-size": "1.0.2", "locale": "0.1.0", "node-geocoder": "4.2.0", + "nodemailer": "^6.9.4", "reflect-metadata": "0.1.13", "sharp": "0.31.3", "ts-exif-parser": "0.2.2", "ts-node-iptc": "1.0.11", "typeconfig": "2.1.0", - "xml2js": "0.4.23", - "typeorm": "0.3.12" + "typeorm": "0.3.12", + "xml2js": "0.4.23" }, "devDependencies": { "@angular-builders/custom-webpack": "15.0.0", diff --git a/src/backend/middlewares/GalleryMWs.ts b/src/backend/middlewares/GalleryMWs.ts index 7f1f67dc..bf831776 100644 --- a/src/backend/middlewares/GalleryMWs.ts +++ b/src/backend/middlewares/GalleryMWs.ts @@ -3,9 +3,7 @@ import {promises as fsp} from 'fs'; import * as archiver from 'archiver'; import {NextFunction, Request, Response} from 'express'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; -import { - ParentDirectoryDTO, -} from '../../common/entities/DirectoryDTO'; +import {ParentDirectoryDTO,} from '../../common/entities/DirectoryDTO'; import {ObjectManagers} from '../model/ObjectManagers'; import {ContentWrapper} from '../../common/entities/ConentWrapper'; import {ProjectPath} from '../ProjectPath'; @@ -14,13 +12,11 @@ import {UserDTOUtils} from '../../common/entities/UserDTO'; import {MediaDTO, MediaDTOUtils} from '../../common/entities/MediaDTO'; import {QueryParams} from '../../common/QueryParams'; import {VideoProcessing} from '../model/fileprocessing/VideoProcessing'; -import { - SearchQueryDTO, - SearchQueryTypes, -} from '../../common/entities/SearchQueryDTO'; +import {SearchQueryDTO, SearchQueryTypes,} from '../../common/entities/SearchQueryDTO'; import {LocationLookupException} from '../exceptions/LocationLookupException'; import {SupportedFormats} from '../../common/SupportedFormats'; import {ServerTime} from './ServerTimingMWs'; +import {SortingMethods} from '../../common/entities/SortingMethods'; export class GalleryMWs { @ServerTime('1.db', 'List Directory') @@ -325,16 +321,16 @@ export class GalleryMWs { req.params['searchQueryDTO'] as string ); - const photo = - await ObjectManagers.getInstance().SearchManager.getRandomPhoto(query); - if (!photo) { + const photos = + await ObjectManagers.getInstance().SearchManager.getNMedia(query, [SortingMethods.random], 1, true); + if (!photos || photos.length !== 1) { return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'No photo found')); } req.params['mediaPath'] = path.join( - photo.directory.path, - photo.directory.name, - photo.name + photos[0].directory.path, + photos[0].directory.name, + photos[0].name ); return next(); } catch (e) { diff --git a/src/backend/model/database/SearchManager.ts b/src/backend/model/database/SearchManager.ts index 579d0004..6079940f 100644 --- a/src/backend/model/database/SearchManager.ts +++ b/src/backend/model/database/SearchManager.ts @@ -30,11 +30,11 @@ import { } from '../../../common/entities/SearchQueryDTO'; import {GalleryManager} from './GalleryManager'; import {ObjectManagers} from '../ObjectManagers'; -import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {DatabaseType} from '../../../common/config/private/PrivateConfig'; import {Utils} from '../../../common/Utils'; import {FileEntity} from './enitites/FileEntity'; import {SQL_COLLATE} from './enitites/EntityUtils'; +import {SortingMethods} from '../../../common/entities/SortingMethods'; export class SearchManager { private DIRECTORY_SELECT = [ @@ -348,19 +348,61 @@ export class SearchManager { return result; } - public async getRandomPhoto(query: SearchQueryDTO): Promise { + + private static setSorting( + query: SelectQueryBuilder, + sortings: SortingMethods[] + ): SelectQueryBuilder { + if (!sortings || !Array.isArray(sortings)) { + return query; + } + if (sortings.includes(SortingMethods.random) && sortings.length > 0) { + throw new Error('Error during applying sorting: Can\' randomize and also sort the result. Bad input:' + sortings.map(s => SortingMethods[s]).join(', ')); + } + for (const sort of sortings) { + switch (sort) { + case SortingMethods.descDate: + query.addOrderBy('media.metadata.creationDate', 'DESC'); + break; + case SortingMethods.ascDate: + query.addOrderBy('media.metadata.creationDate', 'ASC'); + break; + case SortingMethods.descRating: + query.addOrderBy('media.metadata.rating', 'DESC'); + break; + case SortingMethods.ascRating: + query.addOrderBy('media.metadata.rating', 'ASC'); + break; + case SortingMethods.descName: + query.addOrderBy('media.name', 'DESC'); + break; + case SortingMethods.ascName: + query.addOrderBy('media.name', 'ASC'); + break; + case SortingMethods.random: + if (Config.Database.type === DatabaseType.mysql) { + query.groupBy('RAND(), media.id'); + } + query.groupBy('RANDOM()'); + break; + } + } + + return query; + } + + public async getNMedia(query: SearchQueryDTO, sortings: SortingMethods[], take: number, photoOnly = false) { const connection = await SQLConnection.getConnection(); const sqlQuery: SelectQueryBuilder = connection - .getRepository(PhotoEntity) + .getRepository(photoOnly ? PhotoEntity : MediaEntity) .createQueryBuilder('media') .select(['media', ...this.DIRECTORY_SELECT]) .innerJoin('media.directory', 'directory') .where(await this.prepareAndBuildWhereQuery(query)); + SearchManager.setSorting(sqlQuery, sortings); + + return sqlQuery.limit(take).getMany(); - if (Config.Database.type === DatabaseType.mysql) { - return await sqlQuery.groupBy('RAND(), media.id').limit(1).getOne(); - } - return await sqlQuery.groupBy('RANDOM()').limit(1).getOne(); } public async getCount(query: SearchQueryDTO): Promise { diff --git a/src/backend/model/jobs/JobRepository.ts b/src/backend/model/jobs/JobRepository.ts index 2be563a6..367d6402 100644 --- a/src/backend/model/jobs/JobRepository.ts +++ b/src/backend/model/jobs/JobRepository.ts @@ -10,6 +10,7 @@ import {PreviewRestJob} from './jobs/PreviewResetJob'; import {GPXCompressionJob} from './jobs/GPXCompressionJob'; import {AlbumRestJob} from './jobs/AlbumResetJob'; import {GPXCompressionResetJob} from './jobs/GPXCompressionResetJob'; +import {TopPickSendJob} from './jobs/TopPickSendJob'; export class JobRepository { private static instance: JobRepository = null; @@ -45,3 +46,4 @@ JobRepository.Instance.register(new GPXCompressionJob()); JobRepository.Instance.register(new TempFolderCleaningJob()); JobRepository.Instance.register(new AlbumRestJob()); JobRepository.Instance.register(new GPXCompressionResetJob()); +JobRepository.Instance.register(new TopPickSendJob()); diff --git a/src/backend/model/jobs/jobs/PreviewFillingJob.ts b/src/backend/model/jobs/jobs/PreviewFillingJob.ts index 333adbcc..9eae9be0 100644 --- a/src/backend/model/jobs/jobs/PreviewFillingJob.ts +++ b/src/backend/model/jobs/jobs/PreviewFillingJob.ts @@ -1,11 +1,6 @@ -import { ObjectManagers } from '../../ObjectManagers'; -import { - ConfigTemplateEntry, - DefaultsJobs, -} from '../../../../common/entities/job/JobDTO'; -import { Job } from './Job'; -import { Config } from '../../../../common/config/private/Config'; -import { DatabaseType } from '../../../../common/config/private/PrivateConfig'; +import {ObjectManagers} from '../../ObjectManagers'; +import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO'; +import {Job} from './Job'; export class PreviewFillingJob extends Job { public readonly Name = DefaultsJobs[DefaultsJobs['Preview Filling']]; @@ -18,7 +13,7 @@ export class PreviewFillingJob extends Job { } protected async init(): Promise { - // abstract function + this.status = 'Persons'; } protected async step(): Promise { diff --git a/src/backend/model/jobs/jobs/TopPickSendJob.ts b/src/backend/model/jobs/jobs/TopPickSendJob.ts new file mode 100644 index 00000000..1ed05111 --- /dev/null +++ b/src/backend/model/jobs/jobs/TopPickSendJob.ts @@ -0,0 +1,74 @@ +import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO'; +import {Job} from './Job'; +import {backendTexts} from '../../../../common/BackendTexts'; +import {SortingMethods} from '../../../../common/entities/SortingMethods'; +import {DatePatternFrequency, DatePatternSearch, SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; +import {ObjectManagers} from '../../ObjectManagers'; +import {PhotoEntity} from '../../database/enitites/PhotoEntity'; + + +export class TopPickSendJob extends Job<{ searchQuery: SearchQueryDTO, sortBy: SortingMethods[], pickAmount: number }> { + public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']]; + public readonly Supported: boolean = true; + public readonly ConfigTemplate: ConfigTemplateEntry[] = [ + { + id: 'searchQuery', + type: 'SearchQuery', + name: backendTexts.searchQuery.name, + description: backendTexts.searchQuery.description, + defaultValue: { + type: SearchQueryTypes.date_pattern, + daysLength: 7, + frequency: DatePatternFrequency.every_year + } as DatePatternSearch, + }, { + id: 'sortby', + type: 'sort-array', + name: backendTexts.sortBy.name, + description: backendTexts.sortBy.description, + defaultValue: [SortingMethods.descRating], + }, { + id: 'pickAmount', + type: 'number', + name: backendTexts.pickAmount.name, + description: backendTexts.pickAmount.description, + defaultValue: 5, + }, + ]; + private status: 'Listing' | 'Sending' = 'Listing'; + private mediaList: PhotoEntity[] = []; + + + protected async init(): Promise { + this.status = 'Listing'; + this.mediaList = []; + this.Progress.Left = 2; + } + + + protected async step(): Promise { + + switch (this.status) { + case 'Listing': + if (!await this.stepListing()) { + this.status = 'Sending'; + } + return true; + case 'Sending': + await this.stepSending(); + } + return false; + } + + private async stepListing(): Promise { + this.Progress.log('Collecting Photos and videos to Send'); + this.Progress.Processed++; + this.mediaList = await ObjectManagers.getInstance().SearchManager.getNMedia(this.config.searchQuery, this.config.sortBy, this.config.pickAmount); + // console.log(this.mediaList); + return false; + } + + private async stepSending(): Promise { + return false; + } +} diff --git a/src/common/BackendTexts.ts b/src/common/BackendTexts.ts index 940d4b22..1503548c 100644 --- a/src/common/BackendTexts.ts +++ b/src/common/BackendTexts.ts @@ -3,4 +3,8 @@ export const backendTexts = { indexedFilesOnly: { name: 10, description: 12 }, sizeToGenerate: { name: 20, description: 22 }, indexChangesOnly: { name: 30, description: 32 }, + searchQuery: { name: 40, description: 42 }, + sortBy: { name: 40, description: 42 }, + pickAmount: { name: 40, description: 42 }, + }; diff --git a/src/common/entities/job/JobDTO.ts b/src/common/entities/job/JobDTO.ts index 6106e586..4d843ee7 100644 --- a/src/common/entities/job/JobDTO.ts +++ b/src/common/entities/job/JobDTO.ts @@ -1,6 +1,6 @@ import {backendText} from '../../BackendTexts'; -export type fieldType = 'string' | 'number' | 'boolean' | 'number-array'; +export type fieldType = 'string' | 'number' | 'boolean' | 'number-array' | 'SearchQuery' | 'sort-array'; export enum DefaultsJobs { Indexing = 1, @@ -13,7 +13,8 @@ export enum DefaultsJobs { 'Preview Reset' = 8, 'GPX Compression' = 9, 'Album Reset' = 10, - 'Delete Compressed GPX' = 11 + 'Delete Compressed GPX' = 11, + 'Top Pick Sending' = 12 } export interface ConfigTemplateEntry {