diff --git a/package-lock.json b/package-lock.json index a71cac10..119a4518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,11 +23,12 @@ "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", + "typeconfig": "2.1.2", "typeorm": "0.3.12", "xml2js": "0.4.23" }, @@ -75,6 +76,7 @@ "@types/leaflet.markercluster": "1.5.1", "@types/node": "18.15.0", "@types/node-geocoder": "4.2.0", + "@types/nodemailer": "6.4.9", "@types/sharp": "0.31.1", "@types/xml2js": "0.4.11", "@typescript-eslint/eslint-plugin": "5.54.1", @@ -4891,6 +4893,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz", + "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -16598,6 +16609,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", @@ -21670,9 +21689,9 @@ } }, "node_modules/typeconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.0.tgz", - "integrity": "sha512-2Sn2lB8nG9lOvy2jY/4U0HCkqJqc7Fpf8uF5hXaB/+YVnjexX05bfOxUpxIB0fh+Qob7TrVkHxt/2R3aacj8Cw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.2.tgz", + "integrity": "sha512-4Zw3q9LmJ3OeVFMxpvbRYpcrR5mG7TTZUr+bYPepXvWCuqLC4fXLD+cjS1hl8e1oSmum6Xfl4vheFx33VevlqA==", "dependencies": { "minimist": "1.2.8" } @@ -26709,6 +26728,15 @@ } } }, + "@types/nodemailer": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz", + "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -35797,6 +35825,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", @@ -39736,9 +39769,9 @@ } }, "typeconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.0.tgz", - "integrity": "sha512-2Sn2lB8nG9lOvy2jY/4U0HCkqJqc7Fpf8uF5hXaB/+YVnjexX05bfOxUpxIB0fh+Qob7TrVkHxt/2R3aacj8Cw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.2.tgz", + "integrity": "sha512-4Zw3q9LmJ3OeVFMxpvbRYpcrR5mG7TTZUr+bYPepXvWCuqLC4fXLD+cjS1hl8e1oSmum6Xfl4vheFx33VevlqA==", "requires": { "minimist": "1.2.8" } diff --git a/package.json b/package.json index 798fc1e4..997e9338 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" + "typeconfig": "2.1.2", + "typeorm": "0.3.12", + "xml2js": "0.4.23" }, "devDependencies": { "@angular-builders/custom-webpack": "15.0.0", @@ -95,6 +96,7 @@ "@types/leaflet.markercluster": "1.5.1", "@types/node": "18.15.0", "@types/node-geocoder": "4.2.0", + "@types/nodemailer": "6.4.9", "@types/sharp": "0.31.1", "@types/xml2js": "0.4.11", "@typescript-eslint/eslint-plugin": "5.54.1", diff --git a/src/backend/Environment.ts b/src/backend/Environment.ts new file mode 100644 index 00000000..9325a3a3 --- /dev/null +++ b/src/backend/Environment.ts @@ -0,0 +1,5 @@ +/** + * Keeps the environment context + * Only use it in the Config constructor + */ +export const ServerEnvironment: { sendMailAvailable?: boolean } = {}; 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/middlewares/RenderingMWs.ts b/src/backend/middlewares/RenderingMWs.ts index b1edfea0..27e89cac 100644 --- a/src/backend/middlewares/RenderingMWs.ts +++ b/src/backend/middlewares/RenderingMWs.ts @@ -9,6 +9,7 @@ import {SharingDTO} from '../../common/entities/SharingDTO'; import {Utils} from '../../common/Utils'; import {LoggerRouter} from '../routes/LoggerRouter'; import {TAGS} from '../../common/config/public/ClientConfig'; +import {ConfigDiagnostics} from '../model/diagnostics/ConfigDiagnostics'; const forcedDebug = process.env['NODE_ENV'] === 'debug'; diff --git a/src/backend/middlewares/admin/SettingsMWs.ts b/src/backend/middlewares/admin/SettingsMWs.ts index 2a8ef6a4..a099f022 100644 --- a/src/backend/middlewares/admin/SettingsMWs.ts +++ b/src/backend/middlewares/admin/SettingsMWs.ts @@ -27,7 +27,6 @@ export class SettingsMWs { try { let settings = req.body.settings; // Top level settings JSON const settingsPath: string = req.body.settingsPath; // Name of the top level settings - const transformer = await Config.original(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/src/backend/model/database/SearchManager.ts b/src/backend/model/database/SearchManager.ts index 579d0004..5e60b33c 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,62 @@ 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 > 1) { + 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'); + } else { + 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/diagnostics/ConfigDiagnostics.ts b/src/backend/model/diagnostics/ConfigDiagnostics.ts index be3079c7..caa530f3 100644 --- a/src/backend/model/diagnostics/ConfigDiagnostics.ts +++ b/src/backend/model/diagnostics/ConfigDiagnostics.ts @@ -26,6 +26,9 @@ import { import {SearchQueryParser} from '../../../common/SearchQueryParser'; import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO'; import {Utils} from '../../../common/Utils'; +import {createTransport} from 'nodemailer'; +import {EmailMessagingType, MessagingConfig} from '../../../common/config/private/MessagingConfig'; +import {ServerEnvironment} from '../../Environment'; const LOG_TAG = '[ConfigDiagnostics]'; @@ -79,6 +82,14 @@ export class ConfigDiagnostics { } } + private static async testEmailMessagingConfig(Messaging: MessagingConfig, config: PrivateConfigClass): Promise { + Logger.debug(LOG_TAG, 'Testing EmailMessaging config'); + + if (Messaging.Email.type === EmailMessagingType.sendmail && ServerEnvironment.sendMailAvailable === false) { + throw new Error('sendmail e-mail sending method is not supported as the sendmail application cannot be found in the OS.'); + } + } + static testVideoConfig(videoConfig: ServerVideoConfig, config: PrivateConfigClass): Promise { Logger.debug(LOG_TAG, 'Testing video config with ffmpeg test'); @@ -285,15 +296,49 @@ export class ConfigDiagnostics { await ConfigDiagnostics.testSharingConfig(config.Sharing, config); await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config); await ConfigDiagnostics.testMapConfig(config.Map); + await ConfigDiagnostics.testEmailMessagingConfig(config.Messaging, config); } + static async checkAndSetEnvironment(): Promise { + Logger.debug(LOG_TAG, 'Checking sendmail availability'); + const transporter = createTransport({ + sendmail: true, + }); + try { + ServerEnvironment.sendMailAvailable = await transporter.verify(); + } catch (e) { + ServerEnvironment.sendMailAvailable = false; + } + Config.Environment.sendMailAvailable = ServerEnvironment.sendMailAvailable; + if (!ServerEnvironment.sendMailAvailable) { + Config.Messaging.Email.type = EmailMessagingType.SMTP; + Logger.info(LOG_TAG, 'Sendmail is not available on the OS. You will need to use an SMTP server if you wish the app to send mails.'); + } + } + static async runDiagnostics(): Promise { if (process.env['NODE_ENV'] === 'debug') { NotificationManager.warning('You are running the application with NODE_ENV=debug. This exposes a lot of debug information that can be a security vulnerability. Set NODE_ENV=production, when you finished debugging.'); } + try { + await ConfigDiagnostics.checkAndSetEnvironment(); + } catch (ex) { + const err: Error = ex; + NotificationManager.error( + 'Error during checking environment', + err.toString() + ); + Logger.error( + LOG_TAG, + 'Error during checking environment', + err.toString() + ); + process.exit(1); + } + try { await ConfigDiagnostics.testDatabase(Config.Database); } catch (ex) { @@ -525,5 +570,23 @@ export class ConfigDiagnostics { ); Config.Map.mapProvider = MapProviders.OpenStreetMap; } + try { + await ConfigDiagnostics.testEmailMessagingConfig(Config.Messaging, Config); + } catch (ex) { + const err: Error = ex; + NotificationManager.warning( + 'Setting to SMTP method.', + err.toString() + ); + Logger.warn( + LOG_TAG, + 'Setting to SMTP method.', + err.toString() + ); + Config.Messaging.Email.type = EmailMessagingType.SMTP; + } + + } + } 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/Job.ts b/src/backend/model/jobs/jobs/Job.ts index adf77084..3ed5e745 100644 --- a/src/backend/model/jobs/jobs/Job.ts +++ b/src/backend/model/jobs/jobs/Job.ts @@ -154,6 +154,7 @@ export abstract class Job = Record> i this.run(); } catch (e) { Logger.error(LOG_TAG, e); + this.Progress.State = JobProgressStates.failed; } }); } 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..094927b5 --- /dev/null +++ b/src/backend/model/jobs/jobs/TopPickSendJob.ts @@ -0,0 +1,116 @@ +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'; +import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger'; + + +export class TopPickSendJob extends Job<{ + searchQuery: SearchQueryDTO, + sortBy: SortingMethods[], + pickAmount: number, + emailTo: string, + emailFrom: string, + emailSubject: string, + emailText: string, +}> { + 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, + }, { + id: 'emailTo', + type: 'email', + name: backendTexts.emailTo.name, + description: backendTexts.emailTo.description, + defaultValue: '', + }, { + id: 'emailFrom', + type: 'email', + name: backendTexts.emailFrom.name, + description: backendTexts.emailFrom.description, + defaultValue: 'norelpy@pigallery2.com', + }, { + id: 'emailSubject', + type: 'string', + name: backendTexts.emailSubject.name, + description: backendTexts.emailSubject.description, + defaultValue: 'Latest photos for you', + }, { + id: 'emailText', + type: 'string', + name: backendTexts.emailText.name, + description: backendTexts.emailText.description, + defaultValue: 'I hand picked these photos just for you:', + }, + ]; + 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 { + this.Progress.log('Sending emails'); + const messenger = new EmailMediaMessenger(); + await messenger.sendMedia({ + from: this.config.emailFrom, + to: this.config.emailTo, + subject: this.config.emailSubject, + text: this.config.emailText + }, this.mediaList); + this.Progress.Processed++; + return false; + } +} diff --git a/src/backend/model/mediamessengers/EmailMediaMessenger.ts b/src/backend/model/mediamessengers/EmailMediaMessenger.ts new file mode 100644 index 00000000..a951750e --- /dev/null +++ b/src/backend/model/mediamessengers/EmailMediaMessenger.ts @@ -0,0 +1,94 @@ +import {createTransport, Transporter} from 'nodemailer'; +import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO'; +import {Config} from '../../../common/config/private/Config'; +import {EmailMessagingType} from '../../../common/config/private/MessagingConfig'; +import {PhotoProcessing} from '../fileprocessing/PhotoProcessing'; +import {ThumbnailSourceType} from '../threading/PhotoWorker'; +import {ProjectPath} from '../../ProjectPath'; +import * as path from 'path'; +import {PhotoMetadata} from '../../../common/entities/PhotoDTO'; +import {Utils} from '../../../common/Utils'; + +export class EmailMediaMessenger { + transporter: Transporter; + + constructor() { + if (Config.Messaging.Email.type === EmailMessagingType.sendmail) { + this.transporter = createTransport({ + sendmail: true + }); + } else { + this.transporter = createTransport({ + host: Config.Messaging.Email.smtp.host, + port: Config.Messaging.Email.smtp.port, + secure: Config.Messaging.Email.smtp.secure, + requireTLS: Config.Messaging.Email.smtp.requireTLS, + auth: { + user: Config.Messaging.Email.smtp.user, + pass: Config.Messaging.Email.smtp.password + } + }); + } + + } + + private async getThumbnail(m: MediaDTO) { + return await PhotoProcessing.generateThumbnail( + path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name), + Config.Media.Thumbnail.thumbnailSizes[0], + MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video, + false + ); + } + + public async sendMedia(mailSettings: { + from: string, + to: string, + subject: string, + text: string + }, media: MediaDTO[]) { + + const attachments = []; + const htmlStart = '

' + Config.Server.applicationTitle + '

\n' + + '

' + mailSettings.text + '

\n' + + '\n' + + ' \n'; + const htmlEnd = ' \n' + + ' \n' + + '
'; + let htmlMiddle = ''; + for (let i = 0; i < media.length; ++i) { + const thPath = await this.getThumbnail(media[i]); + const linkUrl = Utils.concatUrls(Config.Server.publicUrl, '/gallery/', path.join(media[i].directory.path, media[i].directory.name)); + const location = (media[0].metadata as PhotoMetadata).positionData?.country ? + (media[0].metadata as PhotoMetadata).positionData?.country : + ((media[0].metadata as PhotoMetadata).positionData?.city ? + (media[0].metadata as PhotoMetadata).positionData?.city : ''); + const caption = (new Date(media[0].metadata.creationDate)).getFullYear() + (location ? ', ' + location : ''); + attachments.push({ + filename: media[i].name, + path: thPath, + cid: 'img' + i + }); + if (i % 2 == 0) { + htmlMiddle += ''; + } + htmlMiddle += '\n' + + ' ' + media[i].name + '\n' + + caption + + ' \n'; + + if (i % 2 == 1 || i === media.length - 1) { + htmlMiddle += ''; + } + } + + return await this.transporter.sendMail({ + from: mailSettings.from, + to: mailSettings.to, + subject: mailSettings.subject, + html: htmlStart + htmlMiddle + htmlEnd, + attachments: attachments + }); + } +} diff --git a/src/common/BackendTexts.ts b/src/common/BackendTexts.ts index 940d4b22..c29bf961 100644 --- a/src/common/BackendTexts.ts +++ b/src/common/BackendTexts.ts @@ -1,6 +1,14 @@ export type backendText = number; export const backendTexts = { - indexedFilesOnly: { name: 10, description: 12 }, - sizeToGenerate: { name: 20, description: 22 }, - indexChangesOnly: { name: 30, description: 32 }, + indexedFilesOnly: {name: 10, description: 12}, + sizeToGenerate: {name: 20, description: 22}, + indexChangesOnly: {name: 30, description: 32}, + searchQuery: {name: 40, description: 42}, + sortBy: {name: 50, description: 52}, + pickAmount: {name: 60, description: 62}, + emailTo: {name: 70, description: 72}, + emailFrom: {name: 80, description: 82}, + emailSubject: {name: 90, description: 92}, + emailText: {name: 100, description: 102} + }; diff --git a/src/common/config/private/Config.ts b/src/common/config/private/Config.ts index bf517148..4a5a02ea 100644 --- a/src/common/config/private/Config.ts +++ b/src/common/config/private/Config.ts @@ -6,6 +6,8 @@ import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node'; import {IConfigClass} from 'typeconfig/common'; import {PasswordHelper} from '../../../backend/model/PasswordHelper'; import {TAGS} from '../public/ClientConfig'; +import {ServerEnvironment} from '../../../backend/Environment'; +import {EmailMessagingType} from './MessagingConfig'; declare const process: any; @@ -16,7 +18,8 @@ const isTesting = ['afterEach', 'after', 'beforeEach', 'before', 'describe', 'it .every((fn) => (global as any)[fn] instanceof Function); @ConfigClass & ServerConfig>({ - configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/assets/config.json'), + configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/tmp/config.json'), + crateConfigPathIfNotExists: isTesting, saveIfNotExist: true, attachDescription: true, enumsAsString: true, @@ -79,6 +82,12 @@ export class PrivateConfigClass extends ServerConfig { require('../../../../package.json').buildCommitHash; this.Environment.upTime = upTime; this.Environment.isDocker = !!process.env.PI_DOCKER; + if (typeof ServerEnvironment.sendMailAvailable !== 'undefined') { + this.Environment.sendMailAvailable = ServerEnvironment.sendMailAvailable; + if (!this.Environment.sendMailAvailable) { //onNewValue is not yet available as a callback + this.Messaging.Email.type = EmailMessagingType.SMTP; + } + } } async original(): Promise { diff --git a/src/common/config/private/MessagingConfig.ts b/src/common/config/private/MessagingConfig.ts new file mode 100644 index 00000000..219c5eea --- /dev/null +++ b/src/common/config/private/MessagingConfig.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-inferrable-types */ +import {SubConfigClass} from '../../../../node_modules/typeconfig/src/decorators/class/SubConfigClass'; +import {ConfigPriority, TAGS} from '../public/ClientConfig'; +import {ConfigProperty} from '../../../../node_modules/typeconfig/src/decorators/property/ConfigPropoerty'; +import {ServerConfig} from './PrivateConfig'; +declare let $localize: (s: TemplateStringsArray) => string; + +if (typeof $localize === 'undefined') { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.$localize = (s) => s; +} +export enum EmailMessagingType { + sendmail = 1, + SMTP = 2, +} + +@SubConfigClass({softReadonly: true}) +export class EmailSMTPMessagingConfig { + @ConfigProperty({ + tags: { + name: $localize`Host`, + priority: ConfigPriority.advanced, + hint: 'smtp.example.com' + }, + description: $localize`SMTP host server` + }) + host: string = ''; + + @ConfigProperty({ + tags: { + name: $localize`Port`, + priority: ConfigPriority.advanced, + }, + description: $localize`SMTP server's port` + }) + port: number = 587; + + @ConfigProperty({ + tags: { + name: $localize`isSecure`, + priority: ConfigPriority.advanced, + }, + description: $localize`Is the connection secure. See https://nodemailer.com/smtp/#tls-options for more details` + }) + secure: boolean = false; + + @ConfigProperty({ + tags: { + name: $localize`TLS required`, + priority: ConfigPriority.advanced, + }, + description: $localize`if this is true and secure is false then Nodemailer (used library in the background) tries to use STARTTLS. See https://nodemailer.com/smtp/#tls-options for more details` + }) + requireTLS: boolean = true; + + + @ConfigProperty({ + tags: { + name: $localize`User`, + priority: ConfigPriority.advanced, + }, + description: $localize`User to connect to the SMTP server.` + }) + user: string = ''; + + + @ConfigProperty({ + tags: { + name: $localize`Password`, + priority: ConfigPriority.advanced, + }, + type: 'password', + description: $localize`Password to connect to the SMTP server.` + }) + password: string = ''; + +} + +@SubConfigClass({softReadonly: true}) +export class EmailMessagingConfig { + @ConfigProperty({ + type: EmailMessagingType, + tags: + { + name: $localize`Sending method`, + priority: ConfigPriority.advanced, + uiDisabled: (sc: EmailMessagingConfig, c: ServerConfig) => !c.Environment.sendMailAvailable + } as TAGS, + description: $localize`Sendmail uses the built in unix binary if available. STMP connects to any STMP server of your choice.` + }) + type: EmailMessagingType = EmailMessagingType.sendmail; + + @ConfigProperty({ + tags: + { + name: $localize`SMTP`, + relevant: (c: any) => c.type === EmailMessagingType.SMTP, + } + }) + smtp?: EmailSMTPMessagingConfig = new EmailSMTPMessagingConfig(); + +} + +@SubConfigClass({softReadonly: true}) +export class MessagingConfig { + @ConfigProperty({ + tags: + { + name: $localize`Email`, + }, + description: $localize`The app uses Nodemailer in the background for sending e-mails. Refer to https://nodemailer.com/usage/ if some options are not clear.` + }) + Email: EmailMessagingConfig = new EmailMessagingConfig(); +} diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index 8b924325..e9b60a19 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -30,6 +30,7 @@ import {DefaultsJobs} from '../../entities/job/JobDTO'; import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO'; import {SortingMethods} from '../../entities/SortingMethods'; import {UserRoles} from '../../entities/UserDTO'; +import {EmailMessagingType, MessagingConfig} from './MessagingConfig'; declare let $localize: (s: TemplateStringsArray) => string; @@ -395,7 +396,7 @@ export class ServerMetaFileConfig extends ClientMetaFileConfig { uiJob: [{ job: DefaultsJobs[DefaultsJobs['GPX Compression']], relevant: (c) => c.MetaFile.GPXCompressing.enabled - },{ + }, { job: DefaultsJobs[DefaultsJobs['Delete Compressed GPX']], relevant: (c) => c.MetaFile.GPXCompressing.enabled }] @@ -903,6 +904,7 @@ export class ServerPreviewConfig { @SubConfigClass({softReadonly: true}) export class ServerMediaConfig extends ClientMediaConfig { @ConfigProperty({ + tags: { name: $localize`Images folder`, priority: ConfigPriority.basic, @@ -1049,8 +1051,19 @@ export class ServerEnvironmentConfig { buildCommitHash: string | undefined; @ConfigProperty({volatile: true}) isDocker: boolean | undefined; + @ConfigProperty({ + volatile: true, + onNewValue: (value, config) => { + if (value === false) { + config.Messaging.Email.type = EmailMessagingType.SMTP; + } + }, + description: 'App updates on start-up if sendmail binary is available' + }) + sendMailAvailable: boolean | undefined; } + @SubConfigClass({softReadonly: true}) export class ServerConfig extends ClientConfig { @@ -1144,6 +1157,16 @@ export class ServerConfig extends ClientConfig { }) Duplicates: ServerDuplicatesConfig = new ServerDuplicatesConfig(); + @ConfigProperty({ + tags: { + name: $localize`Messaging`, + uiIcon: 'chat', + githubIssue: 683 + } as TAGS, + description: $localize`The App can send messages (like photos on the same day a year ago. aka: "Top Pick"). Here you can configure the delivery method.` + }) + Messaging: MessagingConfig = new MessagingConfig(); + @ConfigProperty({ tags: { name: $localize`Jobs`, diff --git a/src/common/entities/job/JobDTO.ts b/src/common/entities/job/JobDTO.ts index 6106e586..f06fe435 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' | 'email' | '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 { diff --git a/src/common/entities/job/JobProgressDTO.ts b/src/common/entities/job/JobProgressDTO.ts index 7aecb71b..a0a1ed71 100644 --- a/src/common/entities/job/JobProgressDTO.ts +++ b/src/common/entities/job/JobProgressDTO.ts @@ -4,6 +4,7 @@ export enum JobProgressStates { interrupted = 3, canceled = 4, finished = 5, + failed = 6, } export interface JobProgressLogDTO { diff --git a/src/frontend/app/model/backendtext.service.spec.ts b/src/frontend/app/model/backendtext.service.spec.ts index 09fd03c0..c95c1fae 100644 --- a/src/frontend/app/model/backendtext.service.spec.ts +++ b/src/frontend/app/model/backendtext.service.spec.ts @@ -1,6 +1,6 @@ -import { inject, TestBed } from '@angular/core/testing'; -import { BackendtextService } from './backendtext.service'; -import { backendTexts } from '../../../common/BackendTexts'; +import {inject, TestBed} from '@angular/core/testing'; +import {BackendtextService} from './backendtext.service'; +import {backendTexts} from '../../../common/BackendTexts'; describe('BackendTextService', () => { beforeEach(() => { @@ -9,7 +9,7 @@ describe('BackendTextService', () => { }); }); - it('should call UserDTO service login', inject( + it('should have valid text for all keys', inject( [BackendtextService], (backendTextService: BackendtextService) => { const getTexts = (obj: any) => { @@ -18,7 +18,7 @@ describe('BackendTextService', () => { getTexts(obj[key]); continue; } - expect(backendTextService.get(obj[key])).not.toBe(null); + expect(backendTextService.get(obj[key])).not.toEqual(null, 'Error for key: ' + obj[key] +', ' + key); } }; getTexts(backendTexts); diff --git a/src/frontend/app/model/backendtext.service.ts b/src/frontend/app/model/backendtext.service.ts index 738d6e6f..fc5d66a9 100644 --- a/src/frontend/app/model/backendtext.service.ts +++ b/src/frontend/app/model/backendtext.service.ts @@ -19,6 +19,34 @@ export class BackendtextService { return $localize`Index changes only`; case backendTexts.indexChangesOnly.description: return $localize`Only indexes a folder if it got changed.`; + case backendTexts.searchQuery.name: + return $localize`Search query`; + case backendTexts.searchQuery.description: + return $localize`Search query to list photos and videos.`; + case backendTexts.sortBy.name: + return $localize`Sorting`; + case backendTexts.sortBy.description: + return $localize`Sorts the photos and videos by this.`; + case backendTexts.pickAmount.name: + return $localize`Pick`; + case backendTexts.pickAmount.description: + return $localize`Number of photos and videos to pick.`; + case backendTexts.emailTo.name: + return $localize`E-mail to`; + case backendTexts.emailTo.description: + return $localize`E-mail address of the recipient.`; + case backendTexts.emailFrom.name: + return $localize`E-mail From`; + case backendTexts.emailFrom.description: + return $localize`E-mail sender address.`; + case backendTexts.emailSubject.name: + return $localize`Subject`; + case backendTexts.emailSubject.description: + return $localize`E-mail subject.`; + case backendTexts.emailText.name: + return $localize`Message`; + case backendTexts.emailText.description: + return $localize`E-mail text.`; default: return null; } diff --git a/src/frontend/app/ui/settings/template/template.component.html b/src/frontend/app/ui/settings/template/template.component.html index e35b8a50..08e88fa5 100644 --- a/src/frontend/app/ui/settings/template/template.component.html +++ b/src/frontend/app/ui/settings/template/template.component.html @@ -71,6 +71,10 @@ let-confPath="confPath"> diff --git a/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts b/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts index e8a1a52b..40f62d3d 100644 --- a/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts +++ b/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts @@ -115,6 +115,8 @@ export class JobProgressComponent implements OnDestroy, OnChanges { return $localize`interrupted`; case JobProgressStates.finished: return $localize`finished`; + case JobProgressStates.failed: + return $localize`failed`; default: return 'unknown state'; } diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.css b/src/frontend/app/ui/settings/workflow/workflow.component.css index e69de29b..26e62ba9 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.css +++ b/src/frontend/app/ui/settings/workflow/workflow.component.css @@ -0,0 +1,3 @@ +app-gallery-search-field { + width: 100%; +} diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.html b/src/frontend/app/ui/settings/workflow/workflow.component.html index be043e5b..7983f4a9 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.html +++ b/src/frontend/app/ui/settings/workflow/workflow.component.html @@ -1,6 +1,7 @@
-
+
@@ -175,6 +176,14 @@ [(ngModel)]="schedule.config[configEntry.id]" required> + + + + + + + + + + + +
+
+ + + +
+ +
+ +
+
+
+
+ +
+ +
+
+
diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.ts b/src/frontend/app/ui/settings/workflow/workflow.component.ts index 0e5ed79f..51ae5dc5 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.ts +++ b/src/frontend/app/ui/settings/workflow/workflow.component.ts @@ -19,6 +19,8 @@ import { ScheduledJobTriggerConfig } from '../../../../../common/config/private/PrivateConfig'; import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms'; +import {enumToTranslatedArray} from '../../EnumTranslations'; +import {SortingMethods} from '../../../../../common/entities/SortingMethods'; @Component({ selector: 'app-settings-workflow', @@ -61,6 +63,9 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni allowParallelRun: false, }; + SortingMethods = enumToTranslatedArray(SortingMethods); + + error: string; constructor( @@ -278,4 +283,17 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni this.onTouched = fn; } + + + AsSortArray(configElement: string | number | string[] | number[]): SortingMethods[] { + return configElement as SortingMethods[]; + } + + removeSorting(configElement: string | number | string[] | number[], j: number): void { + (configElement as SortingMethods[]).splice(j); + } + + AddNewSorting(configElement: string | number | string[] | number[]): void { + (configElement as SortingMethods[]).push(SortingMethods.ascDate) + } } diff --git a/test/backend/integration/routers/admin/SettingsRouter.ts b/test/backend/integration/routers/admin/SettingsRouter.ts index 649e5e7d..6f65578f 100644 --- a/test/backend/integration/routers/admin/SettingsRouter.ts +++ b/test/backend/integration/routers/admin/SettingsRouter.ts @@ -1,12 +1,12 @@ import * as path from 'path'; import * as fs from 'fs'; import {Config} from '../../../../../src/common/config/private/Config'; -import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection'; import {Server} from '../../../../../src/backend/server'; import {DatabaseType, ServerConfig} from '../../../../../src/common/config/private/PrivateConfig'; import {ProjectPath} from '../../../../../src/backend/ProjectPath'; import {TAGS} from '../../../../../src/common/config/public/ClientConfig'; import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; +import {UserRoles} from '../../../../../src/common/entities/UserDTO'; process.env.NODE_ENV = 'test'; const chai: any = require('chai'); @@ -34,9 +34,8 @@ describe('SettingsRouter', () => { describe('/GET settings', () => { it('it should GET the settings', async () => { Config.Users.authenticationRequired = false; + Config.Users.unAuthenticatedUserRole = UserRoles.Admin; const originalSettings = await Config.original(); - // originalSettings.Server.sessionSecret = null; - // originalSettings.Users.enforcedUsers = null; const srv = new Server(); await srv.onStarted.wait(); const result = await chai.request(srv.App) @@ -46,7 +45,9 @@ describe('SettingsRouter', () => { result.body.should.be.a('object'); should.equal(result.body.error, null); (result.body.result as ServerConfig).Environment.upTime = null; + (result.body.result as ServerConfig).Environment.sendMailAvailable = null; originalSettings.Environment.upTime = null; + originalSettings.Environment.sendMailAvailable = null; result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({ attachState: true, attachVolatile: true, diff --git a/test/backend/unit/middlewares/admin/SettingsMWs.ts b/test/backend/unit/middlewares/admin/SettingsMWs.ts index f7bd0973..db0b689a 100644 --- a/test/backend/unit/middlewares/admin/SettingsMWs.ts +++ b/test/backend/unit/middlewares/admin/SettingsMWs.ts @@ -6,6 +6,11 @@ import {SettingsMWs} from '../../../../../src/backend/middlewares/admin/Settings import {ServerUserConfig} from '../../../../../src/common/config/private/PrivateConfig'; import {Config} from '../../../../../src/common/config/private/Config'; import {UserRoles} from '../../../../../src/common/entities/UserDTO'; +import {ConfigClassBuilder} from '../../../../../node_modules/typeconfig/node'; +import {ServerEnvironment} from '../../../../../src/backend/Environment'; +import {EmailMessagingType} from '../../../../../src/common/config/private/MessagingConfig'; +import * as fs from 'fs'; +import * as path from 'path'; declare const describe: any; @@ -14,11 +19,16 @@ declare const beforeEach: any; describe('Settings middleware', () => { - beforeEach(() => { - ObjectManagers.reset(); + const tempDir = path.join(__dirname, '../../../tmp'); + beforeEach(async () => { + await ObjectManagers.reset(); + await fs.promises.rm(tempDir, {recursive: true, force: true}); }); it('should save empty enforced users settings', (done: (err?: any) => void) => { + ServerEnvironment.sendMailAvailable = false; + Config.Environment.sendMailAvailable = false; + Config.Messaging.Email.type = EmailMessagingType.SMTP; const req: any = { session: {}, sessionOptions: {}, @@ -26,15 +36,17 @@ describe('Settings middleware', () => { params: {}, body: { settingsPath: 'Users', - settings: new ServerUserConfig() + settings: ConfigClassBuilder.attachPrivateInterface(new ServerUserConfig()).toJSON() } }; req.body.settings.enforcedUsers = []; const next: any = (err: ErrorDTO) => { try { + expect(err).to.be.undefined; expect(Config.Users.enforcedUsers.length).to.be.equal(0); done(); } catch (err) { + console.error(err); done(err); } }; @@ -43,6 +55,10 @@ describe('Settings middleware', () => { }); it('should save enforced users settings', (done: (err?: any) => void) => { + + ServerEnvironment.sendMailAvailable = false; + Config.Environment.sendMailAvailable = false; + Config.Messaging.Email.type = EmailMessagingType.SMTP; const req: any = { session: {}, sessionOptions: {}, @@ -66,11 +82,17 @@ describe('Settings middleware', () => { expect(Config.Users.enforcedUsers[0].name).to.be.equal('Apple'); expect(Config.Users.enforcedUsers.length).to.be.equal(1); Config.original().then((cfg) => { - expect(cfg.Users.enforcedUsers.length).to.be.equal(1); - expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple'); - done(); + try { + expect(cfg.Users.enforcedUsers.length).to.be.equal(1); + expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple'); + done(); + } catch (err) { + console.error(err); + done(err); + } }).catch(done); } catch (err) { + console.error(err); done(err); } }; diff --git a/test/backend/unit/model/sql/IndexingManager.spec.ts b/test/backend/unit/model/sql/IndexingManager.spec.ts index 08358206..a79d3a09 100644 --- a/test/backend/unit/model/sql/IndexingManager.spec.ts +++ b/test/backend/unit/model/sql/IndexingManager.spec.ts @@ -711,7 +711,6 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const albums = await am.getAlbums(); expect(albums[0].preview).to.be.an('object'); delete albums[0].preview; - console.log(albums); expect(albums).to.be.deep.equal([ { id: 1, diff --git a/test/backend/unit/model/sql/SearchManager.spec.ts b/test/backend/unit/model/sql/SearchManager.spec.ts index 61db7c3c..3837aaee 100644 --- a/test/backend/unit/model/sql/SearchManager.spec.ts +++ b/test/backend/unit/model/sql/SearchManager.spec.ts @@ -35,6 +35,7 @@ import {AutoCompleteItem} from '../../../../../src/common/entities/AutoCompleteI import {Config} from '../../../../../src/common/config/private/Config'; import {SearchQueryParser} from '../../../../../src/common/SearchQueryParser'; import {FileDTO} from '../../../../../src/common/entities/FileDTO'; +import {SortingMethods} from '../../../../../src/common/entities/SortingMethods'; // eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); @@ -1405,14 +1406,14 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { } as TextSearch; // eslint-disable-next-line - expect(await sm.getRandomPhoto(query)).to.not.exist; + expect(await sm.getNMedia(query, [SortingMethods.random], 1, true)).to.deep.equalInAnyOrder([]); query = ({ text: 'wookiees', matchType: TextSearchQueryMatchTypes.exact_match, type: SearchQueryTypes.keyword } as TextSearch); - expect(Utils.clone(await sm.getRandomPhoto(query))).to.deep.equalInAnyOrder(searchifyMedia(pFaceLess)); + expect(Utils.clone(await sm.getNMedia(query, [SortingMethods.random], 1, true))).to.deep.equalInAnyOrder([searchifyMedia(pFaceLess)]); }); });