Merge pull request #554 from Tzahi12345/tasks-and-maintenence-page
Tasks and maintenence pagepull/555/head
commit
8314ab8fce
@ -0,0 +1,195 @@
|
|||||||
|
const db_api = require('./db');
|
||||||
|
const youtubedl_api = require('./youtube-dl');
|
||||||
|
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const scheduler = require('node-schedule');
|
||||||
|
|
||||||
|
const TASKS = {
|
||||||
|
backup_local_db: {
|
||||||
|
run: db_api.backupDB,
|
||||||
|
title: 'Backup DB',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
missing_files_check: {
|
||||||
|
run: checkForMissingFiles,
|
||||||
|
confirm: deleteMissingFiles,
|
||||||
|
title: 'Missing files check',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
missing_db_records: {
|
||||||
|
run: db_api.importUnregisteredFiles,
|
||||||
|
title: 'Import missing DB records',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
duplicate_files_check: {
|
||||||
|
run: checkForDuplicateFiles,
|
||||||
|
confirm: removeDuplicates,
|
||||||
|
title: 'Find duplicate files in DB',
|
||||||
|
job: null
|
||||||
|
},
|
||||||
|
youtubedl_update_check: {
|
||||||
|
run: youtubedl_api.checkForYoutubeDLUpdate,
|
||||||
|
confirm: youtubedl_api.updateYoutubeDL,
|
||||||
|
title: 'Update youtube-dl',
|
||||||
|
job: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleJob(task_key, schedule) {
|
||||||
|
// schedule has to be converted from our format to one node-schedule can consume
|
||||||
|
let converted_schedule = null;
|
||||||
|
if (schedule['type'] === 'timestamp') {
|
||||||
|
converted_schedule = new Date(schedule['data']['timestamp']);
|
||||||
|
} else if (schedule['type'] === 'recurring') {
|
||||||
|
const dayOfWeek = schedule['data']['dayOfWeek'] ? schedule['data']['dayOfWeek'] : null;
|
||||||
|
const hour = schedule['data']['hour'] ? schedule['data']['hour'] : null;
|
||||||
|
const minute = schedule['data']['minute'] ? schedule['data']['minute'] : null;
|
||||||
|
converted_schedule = new scheduler.RecurrenceRule(null, null, null, dayOfWeek, hour, minute);
|
||||||
|
} else {
|
||||||
|
logger.error(`Failed to schedule job '${task_key}' as the type '${schedule['type']}' is invalid.`)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduler.scheduleJob(converted_schedule, async () => {
|
||||||
|
const task_state = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
if (task_state['running'] || task_state['confirming']) {
|
||||||
|
logger.verbose(`Skipping running task ${task_state['key']} as it is already in progress.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove schedule if it's a one-time task
|
||||||
|
if (task_state['schedule']['type'] !== 'recurring') await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
|
||||||
|
// we're just "running" the task, any confirmation should be user-initiated
|
||||||
|
exports.executeRun(task_key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_api.database_initialized) {
|
||||||
|
exports.setupTasks();
|
||||||
|
} else {
|
||||||
|
db_api.database_initialized_bs.subscribe(init => {
|
||||||
|
if (init) exports.setupTasks();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setupTasks = async () => {
|
||||||
|
const tasks_keys = Object.keys(TASKS);
|
||||||
|
for (let i = 0; i < tasks_keys.length; i++) {
|
||||||
|
const task_key = tasks_keys[i];
|
||||||
|
const task_in_db = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
if (!task_in_db) {
|
||||||
|
// insert task metadata into table if missing
|
||||||
|
await db_api.insertRecordIntoTable('tasks', {
|
||||||
|
key: task_key,
|
||||||
|
title: TASKS[task_key]['title'],
|
||||||
|
last_ran: null,
|
||||||
|
last_confirmed: null,
|
||||||
|
running: false,
|
||||||
|
confirming: false,
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
schedule: null,
|
||||||
|
options: {}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// reset task if necessary
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {running: false, confirming: false});
|
||||||
|
|
||||||
|
// schedule task and save job
|
||||||
|
if (task_in_db['schedule']) {
|
||||||
|
// prevent timestamp schedules from being set to the past
|
||||||
|
if (task_in_db['schedule']['type'] === 'timestamp' && task_in_db['schedule']['data']['timestamp'] < Date.now()) {
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {schedule: null});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TASKS[task_key]['job'] = scheduleJob(task_key, task_in_db['schedule']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.executeTask = async (task_key) => {
|
||||||
|
if (!TASKS[task_key]) {
|
||||||
|
logger.error(`Task ${task_key} does not exist!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.verbose(`Executing task ${task_key}`);
|
||||||
|
await exports.executeRun(task_key);
|
||||||
|
if (!TASKS[task_key]['confirm']) return;
|
||||||
|
await exports.executeConfirm(task_key);
|
||||||
|
logger.verbose(`Finished executing ${task_key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.executeRun = async (task_key) => {
|
||||||
|
logger.verbose(`Running task ${task_key}`);
|
||||||
|
// 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();
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {data: TASKS[task_key]['confirm'] ? data : null, last_ran: Date.now()/1000, running: false});
|
||||||
|
logger.verbose(`Finished running task ${task_key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.executeConfirm = async (task_key) => {
|
||||||
|
logger.verbose(`Confirming task ${task_key}`);
|
||||||
|
if (!TASKS[task_key]['confirm']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {confirming: true});
|
||||||
|
const task_obj = await db_api.getRecord('tasks', {key: task_key});
|
||||||
|
const data = task_obj['data'];
|
||||||
|
await TASKS[task_key].confirm(data);
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {confirming: false, last_confirmed: Date.now()/1000, data: null});
|
||||||
|
logger.verbose(`Finished confirming task ${task_key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateTaskSchedule = async (task_key, schedule) => {
|
||||||
|
logger.verbose(`Updating schedule for task ${task_key}`);
|
||||||
|
await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule});
|
||||||
|
if (TASKS[task_key]['job']) {
|
||||||
|
TASKS[task_key]['job'].cancel();
|
||||||
|
}
|
||||||
|
if (schedule) {
|
||||||
|
TASKS[task_key]['job'] = scheduleJob(task_key, schedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing files check
|
||||||
|
|
||||||
|
async function checkForMissingFiles() {
|
||||||
|
const missing_files = [];
|
||||||
|
const all_files = await db_api.getRecords('files');
|
||||||
|
for (let i = 0; i < all_files.length; i++) {
|
||||||
|
const file_to_check = all_files[i];
|
||||||
|
const file_exists = fs.existsSync(file_to_check['path']);
|
||||||
|
if (!file_exists) missing_files.push(file_to_check['uid']);
|
||||||
|
}
|
||||||
|
return {uids: missing_files};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMissingFiles(data) {
|
||||||
|
const uids = data['uids'];
|
||||||
|
for (let i = 0; i < uids.length; i++) {
|
||||||
|
const uid = uids[i];
|
||||||
|
await db_api.removeRecord('files', {uid: uid});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate files check
|
||||||
|
|
||||||
|
async function checkForDuplicateFiles() {
|
||||||
|
const duplicate_files = await db_api.findDuplicatesByKey('files', 'path');
|
||||||
|
const duplicate_uids = duplicate_files.map(duplicate_file => duplicate_file['uid']);
|
||||||
|
if (duplicate_uids && duplicate_uids.length > 0) {
|
||||||
|
return {uids: duplicate_uids};
|
||||||
|
}
|
||||||
|
return {uids: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeDuplicates(data) {
|
||||||
|
for (let i = 0; i < data['uids'].length; i++) {
|
||||||
|
await db_api.removeRecord('files', {uid: data['uids'][i]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.TASKS = TASKS;
|
@ -0,0 +1,127 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const logger = require('./logger');
|
||||||
|
const utils = require('./utils');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
const config_api = require('./config.js');
|
||||||
|
|
||||||
|
const is_windows = process.platform === 'win32';
|
||||||
|
|
||||||
|
const download_sources = {
|
||||||
|
'youtube-dl': {
|
||||||
|
'tags_url': 'https://api.github.com/repos/ytdl-org/youtube-dl/tags',
|
||||||
|
'func': downloadLatestYoutubeDLBinary
|
||||||
|
},
|
||||||
|
'youtube-dlc': {
|
||||||
|
'tags_url': 'https://api.github.com/repos/blackjack4494/yt-dlc/tags',
|
||||||
|
'func': downloadLatestYoutubeDLCBinary
|
||||||
|
},
|
||||||
|
'yt-dlp': {
|
||||||
|
'tags_url': 'https://api.github.com/repos/yt-dlp/yt-dlp/tags',
|
||||||
|
'func': downloadLatestYoutubeDLPBinary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.checkForYoutubeDLUpdate = async () => {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
const tags_url = download_sources[default_downloader]['tags_url'];
|
||||||
|
// get current version
|
||||||
|
let current_app_details_exists = fs.existsSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
if (!current_app_details_exists) {
|
||||||
|
logger.warn(`Failed to get youtube-dl binary details at location '${CONSTS.DETAILS_BIN_PATH}'. Generating file...`);
|
||||||
|
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, {"version":"2020.00.00", "downloader": default_downloader});
|
||||||
|
}
|
||||||
|
let current_app_details = JSON.parse(fs.readFileSync(CONSTS.DETAILS_BIN_PATH));
|
||||||
|
let current_version = current_app_details['version'];
|
||||||
|
let current_downloader = current_app_details['downloader'];
|
||||||
|
let stored_binary_path = current_app_details['path'];
|
||||||
|
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
|
||||||
|
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${CONSTS.DETAILS_BIN_PATH}, attempting to guess actual path...`);
|
||||||
|
const guessed_base_path = 'node_modules/youtube-dl/bin/';
|
||||||
|
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
|
||||||
|
if (fs.existsSync(guessed_file_path)) {
|
||||||
|
stored_binary_path = guessed_file_path;
|
||||||
|
// logger.info('INFO: Guess successful! Update process continuing...')
|
||||||
|
} else {
|
||||||
|
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// got version, now let's check the latest version from the youtube-dl API
|
||||||
|
|
||||||
|
|
||||||
|
fetch(tags_url, {method: 'Get'})
|
||||||
|
.then(async res => res.json())
|
||||||
|
.then(async (json) => {
|
||||||
|
// check if the versions are different
|
||||||
|
if (!json || !json[0]) {
|
||||||
|
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latest_update_version = json[0]['name'];
|
||||||
|
if (current_version !== latest_update_version || default_downloader !== current_downloader) {
|
||||||
|
// versions different or different downloader is being used, download new update
|
||||||
|
resolve(latest_update_version);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||||
|
logger.error(err);
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.updateYoutubeDL = async (latest_update_version) => {
|
||||||
|
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||||
|
await download_sources[default_downloader]['func'](latest_update_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLBinary(new_version) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/ytdl-org/youtube-dl/releases/latest/download/youtube-dl${file_ext}`;
|
||||||
|
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||||
|
|
||||||
|
await utils.fetchFile(download_url, output_path, `youtube-dl ${new_version}`);
|
||||||
|
|
||||||
|
updateDetailsJSON(new_version, 'youtube-dl');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLCBinary(new_version) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
||||||
|
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||||
|
|
||||||
|
await utils.fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
||||||
|
|
||||||
|
updateDetailsJSON(new_version, 'youtube-dlc');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLatestYoutubeDLPBinary(new_version) {
|
||||||
|
const file_ext = is_windows ? '.exe' : '';
|
||||||
|
|
||||||
|
const download_url = `https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp${file_ext}`;
|
||||||
|
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||||
|
|
||||||
|
await utils.fetchFile(download_url, output_path, `yt-dlp ${new_version}`);
|
||||||
|
|
||||||
|
updateDetailsJSON(new_version, 'yt-dlp');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDetailsJSON(new_version, downloader) {
|
||||||
|
const details_json = fs.readJSONSync(CONSTS.DETAILS_BIN_PATH);
|
||||||
|
if (new_version) details_json['version'] = new_version;
|
||||||
|
details_json['downloader'] = downloader;
|
||||||
|
fs.writeJSONSync(CONSTS.DETAILS_BIN_PATH, details_json);
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface DBBackup {
|
||||||
|
name: string;
|
||||||
|
timestamp: number;
|
||||||
|
size: number;
|
||||||
|
source: DBBackup.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace DBBackup {
|
||||||
|
|
||||||
|
export enum source {
|
||||||
|
LOCAL = 'local',
|
||||||
|
REMOTE = 'remote',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Task } from './Task';
|
||||||
|
|
||||||
|
export interface GetAllTasksResponse {
|
||||||
|
tasks?: Array<Task>;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { DBBackup } from './DBBackup';
|
||||||
|
|
||||||
|
export interface GetDBBackupsResponse {
|
||||||
|
tasks?: Array<DBBackup>;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface GetTaskRequest {
|
||||||
|
task_key: string;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Task } from './Task';
|
||||||
|
|
||||||
|
export interface GetTaskResponse {
|
||||||
|
task?: Task;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface RestoreDBBackupRequest {
|
||||||
|
file_name: string;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface Schedule {
|
||||||
|
type: Schedule.type;
|
||||||
|
data: {
|
||||||
|
dayOfWeek?: Array<number>,
|
||||||
|
hour?: number,
|
||||||
|
minute?: number,
|
||||||
|
timestamp?: number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Schedule {
|
||||||
|
|
||||||
|
export enum type {
|
||||||
|
TIMESTAMP = 'timestamp',
|
||||||
|
RECURRING = 'recurring',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
key: string;
|
||||||
|
last_ran: number;
|
||||||
|
last_confirmed: number;
|
||||||
|
running: boolean;
|
||||||
|
confirming: boolean;
|
||||||
|
data: any;
|
||||||
|
error: string;
|
||||||
|
schedule: any;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
||||||
|
export interface UpdateTaskDataRequest {
|
||||||
|
task_key: string;
|
||||||
|
new_data: any;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { Schedule } from './Schedule';
|
||||||
|
|
||||||
|
export interface UpdateTaskScheduleRequest {
|
||||||
|
task_key: string;
|
||||||
|
new_schedule: Schedule;
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
<div [hidden]="!(tasks && tasks.length > 0)">
|
||||||
|
<div style="overflow: hidden;" [ngClass]="'mat-elevation-z8'">
|
||||||
|
<mat-table style="overflow: hidden" matSort [dataSource]="dataSource">
|
||||||
|
<!-- Title Column -->
|
||||||
|
<ng-container matColumnDef="title">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let element">
|
||||||
|
<span class="one-line" [matTooltip]="element.title ? element.title : null">
|
||||||
|
{{element.title}}
|
||||||
|
</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Last Ran Column -->
|
||||||
|
<ng-container matColumnDef="last_ran">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last ran">Last ran</ng-container> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let element">
|
||||||
|
<ng-container *ngIf="element.last_ran">{{element.last_ran*1000 | date: 'short'}}</ng-container>
|
||||||
|
<ng-container i18n="N/A" *ngIf="!element.last_ran">N/A</ng-container>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Last Confirmed Column -->
|
||||||
|
<ng-container matColumnDef="last_confirmed">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last confirmed">Last confirmed</ng-container> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let element">
|
||||||
|
<ng-container *ngIf="element.last_confirmed">{{element.last_confirmed*1000 | date: 'short'}}</ng-container>
|
||||||
|
<ng-container i18n="N/A" *ngIf="!element.last_confirmed">N/A</ng-container>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<ng-container matColumnDef="status">
|
||||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Status">Status</ng-container> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let element">
|
||||||
|
<span *ngIf="element.running || element.confirming"><mat-spinner matTooltip="Busy" i18n-matTooltip="Busy" [diameter]="25"></mat-spinner></span>
|
||||||
|
<span *ngIf="!(element.running || element.confirming) && element.schedule" style="display: flex">
|
||||||
|
<ng-container i18n="Scheduled">Scheduled for</ng-container>
|
||||||
|
{{element.next_invocation | date: 'short'}}<mat-icon style="font-size: 16px; display: inline-flex; align-items: center; padding-left: 5px;" *ngIf="element.schedule.type === 'recurring'">repeat</mat-icon>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!(element.running || element.confirming) && !element.schedule">
|
||||||
|
<ng-container i18n="Not scheduled">Not scheduled</ng-container>
|
||||||
|
</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Actions Column -->
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let element">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div *ngIf="element.data?.uids?.length > 0 || (!element.data?.uids && element.data)" class="col-12 mt-2" style="display: flex; justify-content: center;">
|
||||||
|
<ng-container>
|
||||||
|
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
|
||||||
|
<ng-container *ngIf="element.key == 'missing_files_check'">
|
||||||
|
<ng-container i18n="Clear missing files from DB">Clear missing files from DB:</ng-container>{{element.data.uids.length}}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="element.key == 'duplicate_files_check'">
|
||||||
|
<ng-container i18n="Clear duplicate files from DB">Clear duplicate files from DB:</ng-container> {{element.data.uids.length}}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="element.key == 'youtubedl_update_check'">
|
||||||
|
<ng-container i18n="Update binary to">Update binary to:</ng-container> {{element.data}}
|
||||||
|
</ng-container>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<button (click)="runTask(element.key)" [disabled]="element.running || element.confirming" mat-icon-button matTooltip="Run" i18n-matTooltip="Run"><mat-icon>play_arrow</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<button (click)="scheduleTask(element)" mat-icon-button matTooltip="Schedule" i18n-matTooltip="Schedule"><mat-icon>schedule</mat-icon></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator [pageSizeOptions]="[5, 10, 20]"
|
||||||
|
showFirstLastButtons
|
||||||
|
aria-label="Select page of tasks">
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="openRestoreDBBackupDialog()" i18n="Restore DB from backup button">Restore DB from backup</button>
|
||||||
|
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="resetTasks()" color="warn" i18n="Reset tasks button">Reset tasks</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="(!tasks || tasks.length === 0) && tasks_retrieved">
|
||||||
|
<h4 style="text-align: center; margin-top: 10px;" i18n="No tasks label">No tasks available!</h4>
|
||||||
|
</div>
|
@ -0,0 +1,32 @@
|
|||||||
|
mat-header-cell, mat-cell {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.one-line {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloads-action-button-div {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-top {
|
||||||
|
border-radius: 16px 16px 0px 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-bottom {
|
||||||
|
border-radius: 0px 0px 16px 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 16px 16px 16px 16px !important;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TasksComponent } from './tasks.component';
|
||||||
|
|
||||||
|
describe('TasksComponent', () => {
|
||||||
|
let component: TasksComponent;
|
||||||
|
let fixture: ComponentFixture<TasksComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ TasksComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TasksComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,164 @@
|
|||||||
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
|
||||||
|
import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component';
|
||||||
|
import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tasks',
|
||||||
|
templateUrl: './tasks.component.html',
|
||||||
|
styleUrls: ['./tasks.component.scss']
|
||||||
|
})
|
||||||
|
export class TasksComponent implements OnInit {
|
||||||
|
|
||||||
|
interval_id = null;
|
||||||
|
tasks_check_interval = 1500;
|
||||||
|
tasks = null;
|
||||||
|
tasks_retrieved = false;
|
||||||
|
|
||||||
|
displayedColumns: string[] = ['title', 'last_ran', 'last_confirmed', 'status', 'actions'];
|
||||||
|
dataSource = null;
|
||||||
|
|
||||||
|
db_backups = [];
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
constructor(private postsService: PostsService, private dialog: MatDialog) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.postsService.initialized) {
|
||||||
|
this.getTasksRecurring();
|
||||||
|
} else {
|
||||||
|
this.postsService.service_initialized.subscribe(init => {
|
||||||
|
if (init) {
|
||||||
|
this.getTasksRecurring();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.interval_id) { clearInterval(this.interval_id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
getTasksRecurring(): void {
|
||||||
|
this.getTasks();
|
||||||
|
this.interval_id = setInterval(() => {
|
||||||
|
this.getTasks();
|
||||||
|
}, this.tasks_check_interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTasks(): void {
|
||||||
|
this.postsService.getTasks().subscribe(res => {
|
||||||
|
if (this.tasks) {
|
||||||
|
if (JSON.stringify(this.tasks) === JSON.stringify(res['tasks'])) return;
|
||||||
|
for (const task of res['tasks']) {
|
||||||
|
const task_index = this.tasks.map(t => t.key).indexOf(task['key']);
|
||||||
|
this.tasks[task_index] = task;
|
||||||
|
}
|
||||||
|
this.dataSource = new MatTableDataSource<Task>(this.tasks);
|
||||||
|
} else {
|
||||||
|
this.tasks = res['tasks'];
|
||||||
|
this.dataSource = new MatTableDataSource<Task>(this.tasks);
|
||||||
|
this.dataSource.paginator = this.paginator;
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
runTask(task_key: string): void {
|
||||||
|
this.postsService.runTask(task_key).subscribe(res => {
|
||||||
|
this.getTasks();
|
||||||
|
this.getDBBackups();
|
||||||
|
if (res['success']) this.postsService.openSnackBar($localize`Successfully ran task!`);
|
||||||
|
else this.postsService.openSnackBar($localize`Failed to run task!`);
|
||||||
|
}, err => {
|
||||||
|
this.postsService.openSnackBar($localize`Failed to run task!`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmTask(task_key: string): void {
|
||||||
|
this.postsService.confirmTask(task_key).subscribe(res => {
|
||||||
|
this.getTasks();
|
||||||
|
if (res['success']) this.postsService.openSnackBar($localize`Successfully confirmed task!`);
|
||||||
|
else this.postsService.openSnackBar($localize`Failed to confirm task!`);
|
||||||
|
}, err => {
|
||||||
|
this.postsService.openSnackBar($localize`Failed to confirm task!`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleTask(task: any): void {
|
||||||
|
// open dialog
|
||||||
|
const dialogRef = this.dialog.open(UpdateTaskScheduleDialogComponent, {
|
||||||
|
data: {
|
||||||
|
task: task
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe(schedule => {
|
||||||
|
if (schedule || schedule === null) {
|
||||||
|
this.postsService.updateTaskSchedule(task['key'], schedule).subscribe(res => {
|
||||||
|
this.getTasks();
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDBBackups(): void {
|
||||||
|
this.postsService.getDBBackups().subscribe(res => {
|
||||||
|
this.db_backups = res['db_backups'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openRestoreDBBackupDialog(): void {
|
||||||
|
this.dialog.open(RestoreDbDialogComponent, {
|
||||||
|
data: {
|
||||||
|
db_backups: this.db_backups
|
||||||
|
},
|
||||||
|
width: '80vw'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTasks(): void {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||||
|
data: {
|
||||||
|
dialogTitle: $localize`Reset tasks`,
|
||||||
|
dialogText: $localize`Would you like to reset your tasks? All your schedules will be removed as well.`,
|
||||||
|
submitText: $localize`Reset`,
|
||||||
|
warnSubmitColor: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.postsService.resetTasks().subscribe(res => {
|
||||||
|
if (res['success']) {
|
||||||
|
this.postsService.openSnackBar($localize`Tasks successfully reset!`);
|
||||||
|
} else {
|
||||||
|
this.postsService.openSnackBar($localize`Failed to reset tasks!`);
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.postsService.openSnackBar($localize`Failed to reset tasks!`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
last_ran: number;
|
||||||
|
last_confirmed: number;
|
||||||
|
running: boolean;
|
||||||
|
confirming: boolean;
|
||||||
|
data: unknown;
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
<h4 mat-dialog-title><ng-container i18n="Restore DB from backup">Restore DB from backup</ng-container></h4>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<mat-selection-list [multiple]="false" [(ngModel)]="selected_backup">
|
||||||
|
<mat-list-option *ngFor="let db_backup of db_backups" [value]="db_backup.name" [matTooltip]="db_backup.name">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
{{db_backup.timestamp*1000 | date: 'short'}}
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
{{(db_backup.size/1000).toFixed(2)}} kB
|
||||||
|
</div>
|
||||||
|
<div class="col-4" style="text-transform: capitalize;">
|
||||||
|
{{db_backup.source}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-list-option>
|
||||||
|
</mat-selection-list>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button mat-dialog-close><ng-container i18n="Restore DB cancel button">Cancel</ng-container></button>
|
||||||
|
<button mat-button [disabled]="restoring" (click)="restoreClicked()" [disabled]="!selected_backup || selected_backup.length !== 1"><ng-container i18n="Restore button">Restore</ng-container></button>
|
||||||
|
<div class="mat-spinner" *ngIf="restoring">
|
||||||
|
<mat-spinner [diameter]="25"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</mat-dialog-actions>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RestoreDbDialogComponent } from './restore-db-dialog.component';
|
||||||
|
|
||||||
|
describe('RestoreDbDialogComponent', () => {
|
||||||
|
let component: RestoreDbDialogComponent;
|
||||||
|
let fixture: ComponentFixture<RestoreDbDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ RestoreDbDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(RestoreDbDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-restore-db-dialog',
|
||||||
|
templateUrl: './restore-db-dialog.component.html',
|
||||||
|
styleUrls: ['./restore-db-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class RestoreDbDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
db_backups = [];
|
||||||
|
selected_backup = null;
|
||||||
|
restoring = false;
|
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialogRef: MatDialogRef<RestoreDbDialogComponent>, private postsService: PostsService) {
|
||||||
|
if (this.data?.db_backups) {
|
||||||
|
this.db_backups = this.data.db_backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getDBBackups();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
getDBBackups(): void {
|
||||||
|
this.postsService.getDBBackups().subscribe(res => {
|
||||||
|
this.db_backups = res['db_backups'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreClicked(): void {
|
||||||
|
this.restoring = true;
|
||||||
|
if (this.selected_backup.length !== 1) return;
|
||||||
|
this.postsService.restoreDBBackup(this.selected_backup[0]).subscribe(res => {
|
||||||
|
this.restoring = false;
|
||||||
|
if (res['success']) {
|
||||||
|
this.postsService.openSnackBar('Database successfully restored!');
|
||||||
|
this.dialogRef.close();
|
||||||
|
} else {
|
||||||
|
this.postsService.openSnackBar('Failed to restore database! See logs for more info.');
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
this.restoring = false;
|
||||||
|
this.postsService.openSnackBar('Failed to restore database! See browser console for more info.');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<h4 mat-dialog-title><ng-container i18n="Update task schedule">Update task schedule</ng-container></h4>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<mat-checkbox [(ngModel)]="enabled"><ng-container i18n="Enabled">Enabled</ng-container></mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<mat-checkbox [(ngModel)]="recurring" [disabled]="!enabled"><ng-container i18n="Recurring">Recurring</ng-container></mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-2" *ngIf="recurring">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-select placeholder="Interval" [(ngModel)]="interval" [disabled]="!enabled">
|
||||||
|
<mat-option value="weekly">Weekly</mat-option>
|
||||||
|
<mat-option value="daily">Daily</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!recurring" class="col-12 mt-2">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Choose a date</mat-label>
|
||||||
|
<input [(ngModel)]="date" [min]="today" matInput [matDatepicker]="picker" [disabled]="!enabled">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #picker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="recurring && interval === 'weekly'" class="col-12 mt-2">
|
||||||
|
<mat-button-toggle-group [(ngModel)]="days_of_week" [multiple]="true" [disabled]="!enabled" aria-label="Week day">
|
||||||
|
<!-- TODO: support translation -->
|
||||||
|
<mat-button-toggle [value]="0">M</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="1">T</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="2">W</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="3">T</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="4">F</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="5">S</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="6">S</mat-button-toggle>
|
||||||
|
</mat-button-toggle-group>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Time</mat-label>
|
||||||
|
<input type="time" matInput [(ngModel)]="time" [disabled]="!enabled">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button mat-dialog-close><ng-container i18n="Update task schedule cancel button">Cancel</ng-container></button>
|
||||||
|
<button mat-button (click)="updateTaskSchedule()"><ng-container i18n="Update button">Update</ng-container></button>
|
||||||
|
</mat-dialog-actions>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UpdateTaskScheduleDialogComponent } from './update-task-schedule-dialog.component';
|
||||||
|
|
||||||
|
describe('UpdateTaskScheduleDialogComponent', () => {
|
||||||
|
let component: UpdateTaskScheduleDialogComponent;
|
||||||
|
let fixture: ComponentFixture<UpdateTaskScheduleDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ UpdateTaskScheduleDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(UpdateTaskScheduleDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { Schedule } from 'api-types';
|
||||||
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-update-task-schedule-dialog',
|
||||||
|
templateUrl: './update-task-schedule-dialog.component.html',
|
||||||
|
styleUrls: ['./update-task-schedule-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class UpdateTaskScheduleDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
enabled = true;
|
||||||
|
recurring = false;
|
||||||
|
days_of_week = [];
|
||||||
|
interval = 'daily';
|
||||||
|
time = null;
|
||||||
|
date = null;
|
||||||
|
today = new Date();
|
||||||
|
|
||||||
|
constructor(@Inject(MAT_DIALOG_DATA) public data: any, private dialogRef: MatDialogRef<UpdateTaskScheduleDialogComponent>, private postsService: PostsService) {
|
||||||
|
this.processTask(this.data.task);
|
||||||
|
this.postsService.getTask(this.data.task.key).subscribe(res => {
|
||||||
|
this.processTask(res['task']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
processTask(task) {
|
||||||
|
if (!task['schedule']) {
|
||||||
|
this.enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule: Schedule = task['schedule'];
|
||||||
|
|
||||||
|
this.recurring = schedule['type'] === Schedule.type.RECURRING;
|
||||||
|
|
||||||
|
if (this.recurring) {
|
||||||
|
this.time = `${schedule['data']['hour']}:${schedule['data']['minute']}`;
|
||||||
|
|
||||||
|
if (schedule['data']['dayOfWeek']) {
|
||||||
|
this.days_of_week = schedule['data']['dayOfWeek'];
|
||||||
|
this.interval = 'weekly';
|
||||||
|
} else {
|
||||||
|
this.interval = 'daily';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const schedule_date = new Date(schedule['data']['timestamp']);
|
||||||
|
this.time = `${schedule_date.getHours()}:${schedule_date.getMinutes()}`
|
||||||
|
this.date = schedule_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTaskSchedule(): void {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.dialogRef.close(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.time) {
|
||||||
|
// needs time!
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = parseInt(this.time.split(':')[0]);
|
||||||
|
const minutes = parseInt(this.time.split(':')[1]);
|
||||||
|
|
||||||
|
const schedule: Schedule = {type: this.recurring ? Schedule.type.RECURRING : Schedule.type.TIMESTAMP, data: null};
|
||||||
|
if (this.recurring) {
|
||||||
|
schedule['data'] = {hour: hours, minute: minutes};
|
||||||
|
if (this.interval === 'weekly') {
|
||||||
|
schedule['data']['dayOfWeek'] = this.days_of_week;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.date.setHours(hours, minutes);
|
||||||
|
console.log(this.date);
|
||||||
|
schedule['data'] = {timestamp: this.date.getTime()};
|
||||||
|
}
|
||||||
|
this.dialogRef.close(schedule);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue