1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2024-11-03 21:04:03 +08:00

Implement MediaPickDTO to allow multiple search queries to send top pick mails. #683

This commit is contained in:
Patrik J. Braun 2023-08-04 00:17:54 +02:00
parent 3e6bb7bff7
commit 794039d824
9 changed files with 204 additions and 116 deletions

View File

@ -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<boolean> {
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<boolean> {
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,

View File

@ -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}

View File

@ -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<string, string | number | string[] | number[]> = {};
config: Record<string, string | number | string[] | number[] | MediaPickDTO[]> = {};
@ConfigProperty()
allowParallelRun: boolean = false;
@ConfigProperty({

View File

@ -0,0 +1,8 @@
import {SearchQueryDTO} from './SearchQueryDTO';
import {SortingMethods} from './SortingMethods';
export interface MediaPickDTO {
searchQuery: SearchQueryDTO;
sortBy: SortingMethods[];
pick: number;
}

View File

@ -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,

View File

@ -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<string, string | number | string[] | number[]>;
config: Record<string, string | number | string[] | number[] | MediaPickDTO[]>;
allowParallelRun: boolean;
trigger:
| NeverJobTrigger

View File

@ -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:

View File

@ -155,7 +155,7 @@
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
<div class="col-md-10">
<div class="input-group">
<div [class.input-group]="'MediaPickDTO-array'!=configEntry.type">
<ng-container [ngSwitch]="configEntry.type">
<div
*ngSwitchCase="'boolean'"
@ -208,44 +208,115 @@
placeholder="Search Query">
</app-gallery-search-field>
<ng-container *ngSwitchCase="'MediaPickDTO-array'">
<ng-container *ngFor="let mp of AsMediaPickDTOArray(schedule.config[configEntry.id]); let j=index">
<ng-container *ngSwitchCase="'sort-array'">
<ng-container *ngFor="let _ of AsSortArray(schedule.config[configEntry.id]); let j=index">
<div class="row col-12 mt-1 m-0 p-0">
<div class="col p-0">
<select
[id]="configEntry.id+'_'+i+'_'+j"
[name]="configEntry.id+'_'+i+'_'+j"
(ngModelChange)="onChange($event)"
class="form-select" [(ngModel)]="AsSortArray(schedule.config[configEntry.id])[j]">
<option *ngFor="let opt of SortingMethods" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
</div>
<ng-container>
<div class="col-auto pe-0">
<button class="btn btn-secondary float-end"
[id]="'list_btn_'+configEntry.id+'_'+i+'_'+j"
[name]="'list_btn_'+configEntry.id+'_'+i+'_'+j"
(click)="removeSorting(schedule.config[configEntry.id],j)"><span
class="oi oi-trash"></span>
</button>
<div class="mb-3 row">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i" i18n>Search Query - {{(j + 1)}}</label>
<div class="col-md-10">
<div class="input-group">
<app-gallery-search-field
[(ngModel)]="mp.searchQuery"
[id]="'sq_'+configEntry.id+'_'+i+'_'+j"
[name]="'sq_'+configEntry.id+'_'+i+'_'+j"
(change)="onChange($event)"
placeholder="Search Query">
</app-gallery-search-field>
</div>
</ng-container>
<small class="form-text text-muted" i18n>
Search query to list photos and videos.
</small>
</div>
</div>
<div class="mb-3 row">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i" i18n>Sort by</label>
<div class="col-md-10">
<div class="input-group">
<ng-container *ngFor="let _ of mp.sortBy; let k=index">
<div class="row col-12 mt-1 m-0 p-0">
<div class="col p-0">
<select
[id]="configEntry.id+'_'+i+'_'+j+'_'+k"
[name]="configEntry.id+'_'+i+'_'+j+'_'+k"
(ngModelChange)="onChange($event)"
class="form-select"
[(ngModel)]="mp.sortBy[k]">
<option *ngFor="let opt of SortingMethods" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
</div>
<div class="col-auto pe-0">
<button class="btn btn-danger float-end"
[class.btn-secondary]="mp.sortBy.length < 2"
[class.btn-danger]="mp.sortBy.length >= 2"
[disabled]="mp.sortBy.length < 2"
[id]="'list_btn_'+configEntry.id+'_'+i+'_'+j+'_'+k"
[name]="'list_btn_'+configEntry.id+'_'+i+'_'+j+'_'+k"
(click)="removeFromArray(mp.sortBy,k)"><span
class="oi oi-trash"></span>
</button>
</div>
</div>
</ng-container>
<div class="col-12 p-0">
<button class="btn btn-primary mt-1 float-end"
[id]="'btn_add_'+configEntry.id+'_'+i+'_'+j"
[name]="'btn_add_'+configEntry.id+'_'+i+'_'+j"
(click)="AddNewSorting(mp.sortBy)" i18n>+ Add
</button>
</div>
</div>
<small class="form-text text-muted" i18n>
Sorts the photos and videos by this.
</small>
</div>
</div>
<div class="mb-3 row">
<label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i" i18n>Pick</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control" [name]="configEntry.id+'_'+i+'_'+j"
[id]="configEntry.id+'_'+i+'_'+j"
(ngModelChange)="onChange($event)"
[(ngModel)]="mp.pick" required>
</div>
<small class="form-text text-muted" i18n>
Number of photos and videos to pick.
</small>
</div>
</div>
<div class="row">
<button class="btn float-end"
[class.btn-secondary]="AsMediaPickDTOArray(schedule.config[configEntry.id]).length < 2"
[class.btn-danger]="AsMediaPickDTOArray(schedule.config[configEntry.id]).length >= 2"
[disabled]="AsMediaPickDTOArray(schedule.config[configEntry.id]).length < 2"
[id]="'list_btn_'+configEntry.id+'_'+i+'_'+j"
[name]="'list_btn_'+configEntry.id+'_'+i+'_'+j"
(click)="removeFromArray(AsMediaPickDTOArray(schedule.config[configEntry.id]),j)"><span
class="oi oi-trash"></span>
</button>
</div>
<hr/>
</ng-container>
<ng-container>
<div class="col-12 p-0">
<button class="btn btn-primary mt-1 float-end"
[id]="'btn_add_'+configEntry.id+'_'+i"
[name]="'btn_add_'+configEntry.id+'_'+i"
(click)="AddNewSorting(schedule.config[configEntry.id])" i18n>+ Add
(click)="AddNewMediaPickDTO(schedule.config[configEntry.id])" i18n>+ Add
</button>
</div>
</ng-container>
</ng-container>
</ng-container>
</div>
<small class="form-text text-muted">

View File

@ -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<string, number[]>, 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
});
}
}