diff --git a/Public API v1.yaml b/Public API v1.yaml index 55d4b3d..22b57da 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -1673,6 +1673,16 @@ components: required: - task_key - new_data + UpdateTaskOptionsRequest: + type: object + properties: + task_key: + type: string + new_options: + type: object + required: + - task_key + - new_options GetTaskResponse: type: object properties: @@ -2549,6 +2559,8 @@ components: type: string schedule: type: object + options: + type: object Schedule: required: - type diff --git a/backend/app.js b/backend/app.js index 560f13a..7ada2d6 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1814,6 +1814,15 @@ app.post('/api/updateTaskData', optionalJwt, async (req, res) => { res.send({success: success}); }); +app.post('/api/updateTaskOptions', optionalJwt, async (req, res) => { + const task_key = req.body.task_key; + const new_options = req.body.new_options; + + const success = await db_api.updateRecord('tasks', {key: task_key}, {options: new_options}); + + res.send({success: success}); +}); + app.post('/api/getDBBackups', optionalJwt, async (req, res) => { const backup_dir = path.join('appdata', 'db_backup'); fs.ensureDirSync(backup_dir); diff --git a/backend/db.js b/backend/db.js index f9400eb..f3c1948 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1095,6 +1095,14 @@ exports.applyFilterLocalDB = (db_path, filter_obj, operation) => { filtered &= (record[filter_prop].search(new RegExp(filter_prop_value['$regex'], filter_prop_value['$options'])) !== -1); } else if ('$ne' in filter_prop_value) { filtered &= filter_prop in record && record[filter_prop] !== filter_prop_value['$ne']; + } else if ('$lt' in filter_prop_value) { + filtered &= filter_prop in record && record[filter_prop] < filter_prop_value['$lt']; + } else if ('$gt' in filter_prop_value) { + filtered &= filter_prop in record && record[filter_prop] > filter_prop_value['$gt']; + } else if ('$lte' in filter_prop_value) { + filtered &= filter_prop in record && record[filter_prop] <= filter_prop_value['$lt']; + } else if ('$gte' in filter_prop_value) { + filtered &= filter_prop in record && record[filter_prop] >= filter_prop_value['$gt']; } } else { // handle case of nested property check diff --git a/backend/tasks.js b/backend/tasks.js index dc91053..427cdfd 100644 --- a/backend/tasks.js +++ b/backend/tasks.js @@ -34,6 +34,12 @@ const TASKS = { confirm: youtubedl_api.updateYoutubeDL, title: 'Update youtube-dl', job: null + }, + delete_old_files: { + run: checkForAutoDeleteFiles, + confirm: autoDeleteFiles, + title: 'Delete old files', + job: null } } @@ -124,6 +130,7 @@ exports.executeTask = async (task_key) => { exports.executeRun = async (task_key) => { logger.verbose(`Running task ${task_key}`); + await db_api.updateRecord('tasks', {key: task_key}, {error: null}) // don't set running to true when backup up DB as it will be stick "running" if restored if (task_key !== 'backup_local_db') await db_api.updateRecord('tasks', {key: task_key}, {running: true}); const data = await TASKS[task_key].run(); @@ -131,10 +138,15 @@ exports.executeRun = async (task_key) => { logger.verbose(`Finished running task ${task_key}`); const task_obj = await db_api.getRecord('tasks', {key: task_key}); await notifications_api.sendTaskNotification(task_obj, false); + + if (task_obj['options'] && task_obj['options']['auto_confirm']) { + exports.executeConfirm(task_key); + } } exports.executeConfirm = async (task_key) => { logger.verbose(`Confirming task ${task_key}`); + await db_api.updateRecord('tasks', {key: task_key}, {error: null}) if (!TASKS[task_key]['confirm']) { return null; } @@ -197,4 +209,29 @@ async function removeDuplicates(data) { } } +// auto delete files + +async function checkForAutoDeleteFiles() { + const task_obj = await db_api.getRecord('tasks', {key: 'delete_old_files'}); + if (!task_obj['options'] || !task_obj['options']['threshold_days']) { + const error_message = 'Failed to do delete check because no limit was set!'; + logger.error(error_message); + await db_api.updateRecord('tasks', {key: 'delete_old_files'}, {error: error_message}) + return null; + } + const delete_older_than_timestamp = Date.now() - task_obj['options']['threshold_days']*86400*1000; + const uids = (await db_api.getRecords('files', {registered: {$lt: delete_older_than_timestamp}})).map(file => file.uid); + return {uids: uids}; +} + +async function autoDeleteFiles(data) { + if (data['uids']) { + logger.info(`Removing ${data['uids'].length} old files!`); + for (let i = 0; i < data['uids'].length; i++) { + const file_to_remove = data['uids'][i]; + await db_api.deleteFile(file_to_remove); + } + } +} + exports.TASKS = TASKS; \ No newline at end of file diff --git a/src/api-types/index.ts b/src/api-types/index.ts index 5819a8c..0fa0c93 100644 --- a/src/api-types/index.ts +++ b/src/api-types/index.ts @@ -114,6 +114,7 @@ export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest'; export type { UpdaterStatus } from './models/UpdaterStatus'; export type { UpdateServerRequest } from './models/UpdateServerRequest'; export type { UpdateTaskDataRequest } from './models/UpdateTaskDataRequest'; +export type { UpdateTaskOptionsRequest } from './models/UpdateTaskOptionsRequest'; export type { UpdateTaskScheduleRequest } from './models/UpdateTaskScheduleRequest'; export type { UpdateUserRequest } from './models/UpdateUserRequest'; export type { User } from './models/User'; diff --git a/src/api-types/models/Task.ts b/src/api-types/models/Task.ts index e6f68be..4b85095 100644 --- a/src/api-types/models/Task.ts +++ b/src/api-types/models/Task.ts @@ -12,4 +12,5 @@ export type Task = { data: any; error: string; schedule: any; + options?: any; }; \ No newline at end of file diff --git a/src/api-types/models/UpdateTaskOptionsRequest.ts b/src/api-types/models/UpdateTaskOptionsRequest.ts new file mode 100644 index 0000000..47cde2d --- /dev/null +++ b/src/api-types/models/UpdateTaskOptionsRequest.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type UpdateTaskOptionsRequest = { + task_key: string; + new_options: any; +}; \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d0b205e..36bad89 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -90,6 +90,7 @@ import { UpdateTaskScheduleDialogComponent } from './dialogs/update-task-schedul import { RestoreDbDialogComponent } from './dialogs/restore-db-dialog/restore-db-dialog.component'; import { NotificationsComponent } from './components/notifications/notifications.component'; import { NotificationsListComponent } from './components/notifications-list/notifications-list.component'; +import { TaskSettingsComponent } from './components/task-settings/task-settings.component'; registerLocaleData(es, 'es'); @@ -137,7 +138,8 @@ registerLocaleData(es, 'es'); UpdateTaskScheduleDialogComponent, RestoreDbDialogComponent, NotificationsComponent, - NotificationsListComponent + NotificationsListComponent, + TaskSettingsComponent ], imports: [ CommonModule, diff --git a/src/app/components/task-settings/task-settings.component.html b/src/app/components/task-settings/task-settings.component.html new file mode 100644 index 0000000..6639bbf --- /dev/null +++ b/src/app/components/task-settings/task-settings.component.html @@ -0,0 +1,23 @@ +

Task settings - {{task.title}}

+ + +
+ + Delete files older than + + days + +
+ +
+ Do not ask for confirmation +
+
+ + + + + \ No newline at end of file diff --git a/src/app/components/task-settings/task-settings.component.scss b/src/app/components/task-settings/task-settings.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/task-settings/task-settings.component.spec.ts b/src/app/components/task-settings/task-settings.component.spec.ts new file mode 100644 index 0000000..7af2cc5 --- /dev/null +++ b/src/app/components/task-settings/task-settings.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TaskSettingsComponent } from './task-settings.component'; + +describe('TaskSettingsComponent', () => { + let component: TaskSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TaskSettingsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TaskSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/task-settings/task-settings.component.ts b/src/app/components/task-settings/task-settings.component.ts new file mode 100644 index 0000000..f96b57e --- /dev/null +++ b/src/app/components/task-settings/task-settings.component.ts @@ -0,0 +1,46 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Task } from 'api-types'; +import { PostsService } from 'app/posts.services'; + +@Component({ + selector: 'app-task-settings', + templateUrl: './task-settings.component.html', + styleUrls: ['./task-settings.component.scss'] +}) +export class TaskSettingsComponent { + task_key: string; + new_options = {}; + task: Task = null; + + constructor(private postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: {task: Task}) { + this.task_key = this.data.task.key; + this.task = this.data.task; + if (!this.task.options) { + this.task.options = {}; + } + } + + ngOnInit(): void { + this.getSettings(); + } + + getSettings(): void { + this.postsService.getTask(this.task_key).subscribe(res => { + this.task = res['task']; + this.new_options = JSON.parse(JSON.stringify(this.task['options'])) || {}; + }); + } + + saveSettings(): void { + this.postsService.updateTaskOptions(this.task_key, this.new_options).subscribe(() => { + this.getSettings(); + }, () => { + this.getSettings(); + }); + } + + optionsChanged(): boolean { + return JSON.stringify(this.new_options) !== JSON.stringify(this.task.options); + } +} diff --git a/src/app/components/tasks/tasks.component.html b/src/app/components/tasks/tasks.component.html index 662e3be..4571f57 100644 --- a/src/app/components/tasks/tasks.component.html +++ b/src/app/components/tasks/tasks.component.html @@ -62,6 +62,9 @@ Update binary to: {{element.data}} + + Delete old files: {{element.data.uids.length}} + @@ -71,6 +74,12 @@
+
+ +
+
+ +
@@ -80,7 +89,7 @@ - diff --git a/src/app/components/tasks/tasks.component.scss b/src/app/components/tasks/tasks.component.scss index ed84df9..46fe007 100644 --- a/src/app/components/tasks/tasks.component.scss +++ b/src/app/components/tasks/tasks.component.scss @@ -29,4 +29,8 @@ mat-header-cell, mat-cell { .rounded { border-radius: 16px 16px 16px 16px !important; +} + +::ng-deep mat-row { + height: fit-content !important; } \ No newline at end of file diff --git a/src/app/components/tasks/tasks.component.ts b/src/app/components/tasks/tasks.component.ts index c4dd2a1..d462429 100644 --- a/src/app/components/tasks/tasks.component.ts +++ b/src/app/components/tasks/tasks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, EventEmitter, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; @@ -8,6 +8,8 @@ import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore- import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component'; import { PostsService } from 'app/posts.services'; import { Task } from 'api-types'; +import { TaskSettingsComponent } from '../task-settings/task-settings.component'; +import { Clipboard } from '@angular/cdk/clipboard'; @Component({ selector: 'app-tasks', @@ -29,7 +31,7 @@ export class TasksComponent implements OnInit { @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - constructor(private postsService: PostsService, private dialog: MatDialog) { } + constructor(private postsService: PostsService, private dialog: MatDialog, private clipboard: Clipboard) { } ngOnInit(): void { if (this.postsService.initialized) { @@ -117,6 +119,14 @@ export class TasksComponent implements OnInit { }); } + openTaskSettings(task: Task): void { + this.dialog.open(TaskSettingsComponent, { + data: { + task: task + } + }); + } + getDBBackups(): void { this.postsService.getDBBackups().subscribe(res => { this.db_backups = res['db_backups']; @@ -157,4 +167,24 @@ export class TasksComponent implements OnInit { }); } + showError(task: Task): void { + const copyToClipboardEmitter = new EventEmitter(); + this.dialog.open(ConfirmDialogComponent, { + data: { + dialogTitle: $localize`Error for: ${task['title']}`, + dialogText: task['error'], + submitText: $localize`Copy to clipboard`, + cancelText: $localize`Close`, + closeOnSubmit: false, + onlyEmitOnDone: true, + doneEmitter: copyToClipboardEmitter + } + }); + copyToClipboardEmitter.subscribe((done: boolean) => { + if (done) { + this.postsService.openSnackBar($localize`Copied to clipboard!`); + this.clipboard.copy(task['error']); + } + }); + } } diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 4c5eef5..7b7be93 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -104,7 +104,8 @@ import { GetAllTasksResponse, DeleteNotificationRequest, SetNotificationsToReadRequest, - GetNotificationsResponse + GetNotificationsResponse, + UpdateTaskOptionsRequest } from '../api-types'; import { isoLangs } from './settings/locales_list'; import { Title } from '@angular/platform-browser'; @@ -647,6 +648,11 @@ export class PostsService implements CanActivate { return this.http.post(this.path + 'updateTaskData', body, this.httpOptions); } + updateTaskOptions(task_key: string, options: any) { + const body: UpdateTaskOptionsRequest = {task_key: task_key, new_options: options}; + return this.http.post(this.path + 'updateTaskOptions', body, this.httpOptions); + } + getDBBackups() { return this.http.post(this.path + 'getDBBackups', {}, this.httpOptions); }