From 794039d824acc6a5b3b158113a08768e26d87656 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 4 Aug 2023 00:17:54 +0200 Subject: [PATCH] Implement MediaPickDTO to allow multiple search queries to send top pick mails. #683 --- src/backend/model/jobs/jobs/TopPickSendJob.ts | 58 +++++---- src/common/BackendTexts.ts | 4 +- src/common/config/private/PrivateConfig.ts | 3 +- src/common/entities/MediaPickDTO.ts | 8 ++ src/common/entities/job/JobDTO.ts | 2 +- src/common/entities/job/JobScheduleDTO.ts | 4 +- src/frontend/app/model/backendtext.service.ts | 16 +-- .../settings/workflow/workflow.component.html | 123 ++++++++++++++---- .../settings/workflow/workflow.component.ts | 102 ++++++++------- 9 files changed, 204 insertions(+), 116 deletions(-) create mode 100644 src/common/entities/MediaPickDTO.ts diff --git a/src/backend/model/jobs/jobs/TopPickSendJob.ts b/src/backend/model/jobs/jobs/TopPickSendJob.ts index bd47a567..b964d45a 100644 --- a/src/backend/model/jobs/jobs/TopPickSendJob.ts +++ b/src/backend/model/jobs/jobs/TopPickSendJob.ts @@ -2,16 +2,15 @@ import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/jo 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 {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; import {ObjectManagers} from '../../ObjectManagers'; import {PhotoEntity} from '../../database/enitites/PhotoEntity'; import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger'; +import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO'; export class TopPickSendJob extends Job<{ - searchQuery: SearchQueryDTO, - sortBy: SortingMethods[], - pickAmount: number, + mediaPick: MediaPickDTO[], emailTo: string, emailFrom: string, emailSubject: string, @@ -21,27 +20,19 @@ export class TopPickSendJob extends Job<{ 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, SortingMethods.descPersonCount], - }, { - id: 'pickAmount', - type: 'number', - name: backendTexts.pickAmount.name, - description: backendTexts.pickAmount.description, - defaultValue: 5, + id: 'mediaPick', + type: 'MediaPickDTO-array', + name: backendTexts.mediaPick.name, + description: backendTexts.mediaPick.description, + defaultValue: [{ + searchQuery: { + type: SearchQueryTypes.date_pattern, + daysLength: 7, + frequency: DatePatternFrequency.every_year + } as DatePatternSearch, + sortBy: [SortingMethods.descRating, SortingMethods.descPersonCount], + pick: 5 + }] as MediaPickDTO[], }, { id: 'emailTo', type: 'string-array', @@ -88,15 +79,26 @@ export class TopPickSendJob extends Job<{ } private async stepListing(): Promise { - this.Progress.log('Collecting Photos and videos to Send'); + this.Progress.log('Collecting Photos and videos to Send.'); + this.mediaList = []; + for (let i = 0; i < this.config.mediaPick.length; ++i) { + const media = await ObjectManagers.getInstance().SearchManager.getNMedia(this.config.mediaPick[i].searchQuery, this.config.mediaPick[i].sortBy, this.config.mediaPick[i].pick); + this.Progress.log('Find ' + media.length + ' photos and videos from ' + (i + 1) + '. load'); + this.mediaList = this.mediaList.concat(media); + } + 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'); + if (this.mediaList.length <= 0) { + this.Progress.log('No photos found skipping e-mail sending.'); + this.Progress.Skipped++; + return false; + } + this.Progress.log('Sending emails of ' + this.mediaList.length + ' photos.'); const messenger = new EmailMediaMessenger(); await messenger.sendMedia({ to: this.config.emailTo, diff --git a/src/common/BackendTexts.ts b/src/common/BackendTexts.ts index f3ada675..966d198b 100644 --- a/src/common/BackendTexts.ts +++ b/src/common/BackendTexts.ts @@ -3,9 +3,7 @@ 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: 50, description: 52}, - pickAmount: {name: 60, description: 62}, + mediaPick: {name: 40, description: 42}, emailTo: {name: 70, description: 72}, emailSubject: {name: 90, description: 92}, emailText: {name: 100, description: 102} diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index 0b14f8f1..a3497083 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -31,6 +31,7 @@ import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/Sear import {SortingMethods} from '../../entities/SortingMethods'; import {UserRoles} from '../../entities/UserDTO'; import {EmailMessagingType, MessagingConfig} from './MessagingConfig'; +import {MediaPickDTO} from '../../entities/MediaPickDTO'; declare let $localize: (s: TemplateStringsArray) => string; @@ -584,7 +585,7 @@ export class JobScheduleConfig implements JobScheduleDTO { @ConfigProperty() jobName: string; @ConfigProperty() - config: Record = {}; + config: Record = {}; @ConfigProperty() allowParallelRun: boolean = false; @ConfigProperty({ diff --git a/src/common/entities/MediaPickDTO.ts b/src/common/entities/MediaPickDTO.ts new file mode 100644 index 00000000..5a466534 --- /dev/null +++ b/src/common/entities/MediaPickDTO.ts @@ -0,0 +1,8 @@ +import {SearchQueryDTO} from './SearchQueryDTO'; +import {SortingMethods} from './SortingMethods'; + +export interface MediaPickDTO { + searchQuery: SearchQueryDTO; + sortBy: SortingMethods[]; + pick: number; +} diff --git a/src/common/entities/job/JobDTO.ts b/src/common/entities/job/JobDTO.ts index 4760a2be..29159c03 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' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'SearchQuery' | 'sort-array'; +export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array'; export enum DefaultsJobs { Indexing = 1, diff --git a/src/common/entities/job/JobScheduleDTO.ts b/src/common/entities/job/JobScheduleDTO.ts index 4ccede90..6026d587 100644 --- a/src/common/entities/job/JobScheduleDTO.ts +++ b/src/common/entities/job/JobScheduleDTO.ts @@ -1,4 +1,6 @@ /* eslint-disable no-case-declarations */ +import {MediaPickDTO} from '../MediaPickDTO'; + export enum JobTriggerType { never = 1, scheduled = 2, @@ -33,7 +35,7 @@ export interface AfterJobTrigger extends JobTrigger { export interface JobScheduleDTO { name: string; jobName: string; - config: Record; + config: Record; allowParallelRun: boolean; trigger: | NeverJobTrigger diff --git a/src/frontend/app/model/backendtext.service.ts b/src/frontend/app/model/backendtext.service.ts index 5bc6e6bf..7d35da13 100644 --- a/src/frontend/app/model/backendtext.service.ts +++ b/src/frontend/app/model/backendtext.service.ts @@ -19,18 +19,10 @@ 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.mediaPick.name: + return $localize`Media selectors`; + case backendTexts.mediaPick.description: + return $localize`Set these search queries to find photos and videos to email.`; case backendTexts.emailTo.name: return $localize`E-mail to`; case backendTexts.emailTo.description: diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.html b/src/frontend/app/ui/settings/workflow/workflow.component.html index 118fb46c..86c2a7d2 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.html +++ b/src/frontend/app/ui/settings/workflow/workflow.component.html @@ -155,7 +155,7 @@
-
+
+ + - - -
-
- - - -
- -
- +
+ +
+
+ +
- + + Search query to list photos and videos. + +
+
+ +
+
+ +
+
+ + + +
+
+ +
+
+
+
+ +
+
+ + Sorts the photos and videos by this. + +
+
+
+ +
+
+ +
+ + Number of photos and videos to pick. + +
+
+ +
+ +
+
+
+ +
diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.ts b/src/frontend/app/ui/settings/workflow/workflow.component.ts index 75356334..ce95c7e1 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.ts +++ b/src/frontend/app/ui/settings/workflow/workflow.component.ts @@ -21,6 +21,8 @@ import { import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms'; import {enumToTranslatedArray} from '../../EnumTranslations'; import {SortingMethods} from '../../../../../common/entities/SortingMethods'; +import {MediaPickDTO} from '../../../../../common/entities/MediaPickDTO'; +import {SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO'; @Component({ selector: 'app-settings-workflow', @@ -69,9 +71,9 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni error: string; constructor( - public settingsService: SettingsService, - public jobsService: ScheduledJobsService, - public backendTextService: BackendtextService, + public settingsService: SettingsService, + public jobsService: ScheduledJobsService, + public backendTextService: BackendtextService, ) { this.JobTriggerTypeMap = [ {key: JobTriggerType.after, value: $localize`after`}, @@ -113,27 +115,27 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni remove(schedule: JobScheduleDTO): void { this.schedules.splice( - this.schedules.indexOf(schedule), - 1 + this.schedules.indexOf(schedule), + 1 ); } jobTypeChanged(schedule: JobScheduleDTO): void { const job = this.jobsService.availableJobs.value.find( - (t) => t.Name === schedule.jobName + (t) => t.Name === schedule.jobName ); schedule.config = schedule.config || {}; if (job.ConfigTemplate) { job.ConfigTemplate.forEach( - (ct) => (schedule.config[ct.id] = ct.defaultValue) + (ct) => (schedule.config[ct.id] = ct.defaultValue) ); } } jobTriggerTypeChanged( - triggerType: JobTriggerType, - schedule: JobScheduleDTO + triggerType: JobTriggerType, + schedule: JobScheduleDTO ): void { switch (triggerType) { case JobTriggerType.never: @@ -163,7 +165,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni value = value.replace(new RegExp(',', 'g'), ';'); value = value.replace(new RegExp(' ', 'g'), ';'); configElement[id] = value - .split(';').filter((i: string) => i != ''); + .split(';').filter((i: string) => i != ''); } getArray(configElement: Record, id: string): string { @@ -174,46 +176,46 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni value = value.replace(new RegExp(',', 'g'), ';'); value = value.replace(new RegExp(' ', 'g'), ';'); configElement[id] = value - .split(';') - .map((s: string) => parseInt(s, 10)) - .filter((i: number) => !isNaN(i) && i > 0); + .split(';') + .map((s: string) => parseInt(s, 10)) + .filter((i: number) => !isNaN(i) && i > 0); } public shouldIdent(curr: JobScheduleDTO, prev: JobScheduleDTO): boolean { return ( - curr && - curr.trigger.type === JobTriggerType.after && - prev && - prev.name === curr.trigger.afterScheduleName + curr && + curr.trigger.type === JobTriggerType.after && + prev && + prev.name === curr.trigger.afterScheduleName ); } public sortedSchedules(): JobScheduleDTO[] { return (this.schedules || []) - .slice() - .sort((a: JobScheduleDTO, b: JobScheduleDTO) => { - return ( - this.getNextRunningDate(a, this.schedules) - - this.getNextRunningDate(b, this.schedules) - ); - }); + .slice() + .sort((a: JobScheduleDTO, b: JobScheduleDTO) => { + return ( + this.getNextRunningDate(a, this.schedules) - + this.getNextRunningDate(b, this.schedules) + ); + }); } prepareNewJob(): void { const jobName = this.jobsService.availableJobs.value[0].Name; this.newSchedule = new JobScheduleConfig('new job', - jobName, - new NeverJobTriggerConfig()); + jobName, + new NeverJobTriggerConfig()); // setup job specific config const job = this.jobsService.availableJobs.value.find( - (t) => t.Name === jobName + (t) => t.Name === jobName ); this.newSchedule.config = this.newSchedule.config || {}; if (job.ConfigTemplate) { job.ConfigTemplate.forEach( - (ct) => (this.newSchedule.config[ct.id] = ct.defaultValue) + (ct) => (this.newSchedule.config[ct.id] = ct.defaultValue) ); } this.jobModalQL.first.show(); @@ -223,12 +225,12 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni // make unique job name const jobName = this.newSchedule.jobName; const count = this.schedules.filter( - (s: JobScheduleDTO) => s.jobName === jobName + (s: JobScheduleDTO) => s.jobName === jobName ).length; this.newSchedule.name = - count === 0 - ? jobName - : this.backendTextService.getJobName(jobName) + ' ' + (count + 1); + count === 0 + ? jobName + : this.backendTextService.getJobName(jobName) + ' ' + (count + 1); this.schedules.push(this.newSchedule); this.jobModalQL.first.hide(); @@ -240,24 +242,24 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni } private getNextRunningDate( - sch: JobScheduleDTO, - list: JobScheduleDTO[], - depth = 0 + sch: JobScheduleDTO, + list: JobScheduleDTO[], + depth = 0 ): number { if (depth > list.length) { return 0; } if (sch.trigger.type === JobTriggerType.never) { return ( - list - .map((s) => s.name) - .sort() - .indexOf(sch.name) * -1 + list + .map((s) => s.name) + .sort() + .indexOf(sch.name) * -1 ); } if (sch.trigger.type === JobTriggerType.after) { const parent = list.find( - (s) => s.name === (sch.trigger as AfterJobTrigger).afterScheduleName + (s) => s.name === (sch.trigger as AfterJobTrigger).afterScheduleName ); if (parent) { return this.getNextRunningDate(parent, list, depth + 1) + 0.001; @@ -292,15 +294,27 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni } - AsSortArray(configElement: string | number | string[] | number[]): SortingMethods[] { + AsSortArray(configElement: string | number | string[] | number[] | MediaPickDTO[]): SortingMethods[] { return configElement as SortingMethods[]; } - removeSorting(configElement: string | number | string[] | number[], j: number): void { - (configElement as SortingMethods[]).splice(j); + AsMediaPickDTOArray(configElement: string | number | string[] | number[] | MediaPickDTO[]): MediaPickDTO[] { + return configElement as MediaPickDTO[]; } - AddNewSorting(configElement: string | number | string[] | number[]): void { + removeFromArray(configElement: any[], i: number): void { + configElement.splice(i, 1); + } + + AddNewSorting(configElement: string | number | string[] | number[] | MediaPickDTO[]): void { (configElement as SortingMethods[]).push(SortingMethods.ascDate); } + + AddNewMediaPickDTO(configElement: string | number | string[] | number[] | MediaPickDTO[]): void { + (configElement as MediaPickDTO[]).push({ + searchQuery: {type: SearchQueryTypes.any_text, text: ''} as TextSearch, + sortBy: [SortingMethods.descRating], + pick: 5 + }); + } }