Added UI for managing tasks

Added ability to schedule tasks based on timestamp

Fixed mismatched types between frontend and openapi yaml

Simplified imports for several backend components
tasks-and-maintenence-page
Isaac Abadi 3 years ago
parent 5b4d4d5f81
commit 091f81bb38

@ -666,11 +666,287 @@ paths:
schema:
$ref: '#/components/schemas/GetDownloadRequest'
description: ''
description: "Gets a single download using its download_id and session_id. session_id is the device fingerprint. If none was provided at the time of download, then set session_id is 'undeclared'."
description: "Gets a single download using its download_id."
security:
- Auth query parameter: []
tags:
- downloader
/api/pauseDownload:
post:
summary: Pauses one download
operationId: post-api-pause-download
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetDownloadRequest'
description: ''
description: "Pause a single download using its download_id."
security:
- Auth query parameter: []
tags:
- downloader
/api/pauseAllDownloads:
post:
tags:
- downloader
summary: Pauses all downloads
operationId: post-api-pause-all-downloads
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/resumeDownload:
post:
summary: Resume one download
operationId: post-api-resume-download
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetDownloadRequest'
description: ''
description: "Resume a single download using its download_id."
security:
- Auth query parameter: []
tags:
- downloader
/api/resumeAllDownloads:
post:
tags:
- downloader
summary: Resumes all downloads
operationId: post-api-resume-all-downloads
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/restartDownload:
post:
summary: Restart one download
operationId: post-api-restart-download
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetDownloadRequest'
description: ''
description: "Restart a single download using its download_id."
security:
- Auth query parameter: []
tags:
- downloader
/api/cancelDownload:
post:
summary: Cancel one download
operationId: post-api-cancel-download
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetDownloadRequest'
description: ''
description: "Cancel a single download using its download_id."
security:
- Auth query parameter: []
tags:
- downloader
/api/clearDownload:
post:
summary: Clear one download
operationId: post-api-clear-download
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetDownloadRequest'
description: ''
description: "Clears a single download from the downloaded list using its download_id."
security:
- Auth query parameter: []
tags:
- downloader
/api/clearFinishedDownloads:
post:
tags:
- downloader
summary: Clear finished downloads
operationId: post-api-clear-finished-downloads
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/getTask:
post:
summary: Get info for one task
operationId: post-api-get-task
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GetTaskResponse'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetTaskRequest'
description: ''
description: "Gets a single task using its key."
security:
- Auth query parameter: []
tags:
- tasks
/api/getTasks:
post:
tags:
- tasks
summary: Get tasks
operationId: post-api-get-tasks
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GetAllTasksResponse'
security:
- Auth query parameter: []
/api/runTask:
post:
summary: Runs one task
operationId: post-api-run-task
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetTaskRequest'
/api/confirmTask:
post:
summary: Confirms a task
operationId: post-api-confirm-task
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetTaskRequest'
/api/cancelTask:
post:
summary: Cancels a task
operationId: post-api-cancel-task
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GetTaskRequest'
/api/updateTaskSchedule:
post:
summary: Updates task schedule
operationId: post-api-update-task-schedule
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateTaskScheduleRequest'
/api/auth/login:
post:
summary: Login
@ -1231,6 +1507,35 @@ components:
type: array
items:
$ref: '#/components/schemas/Download'
GetTaskRequest:
type: object
properties:
task_key:
type: string
required:
- task_key
UpdateTaskScheduleRequest:
type: object
properties:
task_key:
type: string
new_schedule:
$ref: '#/components/schemas/Schedule'
required:
- task_key
- new_schedule
GetTaskResponse:
type: object
properties:
task:
$ref: '#/components/schemas/Task'
GetAllTasksResponse:
type: object
properties:
tasks:
type: array
items:
$ref: '#/components/schemas/Task'
GetMp3sResponse:
required:
- mp3s
@ -1506,6 +1811,10 @@ components:
type: string
playlist_id:
type: string
url:
type: string
type:
$ref: '#/components/schemas/FileType'
DownloadArchiveRequest:
required:
- sub
@ -1967,6 +2276,58 @@ components:
type: string
sub_name:
type: string
Task:
required:
- key
- last_ran
- last_confirmed
- running
- confirming
- data
- error
- schedule
type: object
properties:
key:
type: string
last_ran:
type: number
last_confirmed:
type: number
running:
type: boolean
confirming:
type: boolean
data:
type: object
error:
type: string
schedule:
type: object
Schedule:
required:
- type
- data
type: object
properties:
type:
type: string
enum:
- timestamp
- recurring
data:
type: object
properties:
dayOfWeek:
type: array
items:
type: number
hour:
type: number
minute:
type: number
timestamp:
type: number
SubscriptionRequestData:
required:
- id

@ -28,6 +28,7 @@ const youtubedl = require('youtube-dl');
const logger = require('./logger');
const config_api = require('./config.js');
const downloader_api = require('./downloader');
const tasks_api = require('./tasks');
const subscriptions_api = require('./subscriptions');
const categories_api = require('./categories');
const twitch_api = require('./twitch');
@ -60,9 +61,6 @@ const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
config_api.initialize();
db_api.initialize(db, users_db);
auth_api.initialize(db_api);
downloader_api.initialize(db_api);
subscriptions_api.initialize(db_api, downloader_api);
categories_api.initialize(db_api);
// Set some defaults
db.defaults(
@ -1878,6 +1876,54 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => {
res.send({success: success});
});
// tasks
app.post('/api/getTasks', optionalJwt, async (req, res) => {
const tasks = await db_api.getRecords('tasks');
for (let task of tasks) {
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime();
}
res.send({tasks: tasks});
});
app.post('/api/getTask', optionalJwt, async (req, res) => {
const task_key = req.body.task_key;
const task = await db_api.getRecord('tasks', {key: task_key});
if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task_key]['job'].nextInvocation().getTime();
res.send({task: task});
});
app.post('/api/runTask', optionalJwt, async (req, res) => {
const task_key = req.body.task_key;
const task = await db_api.getRecord('tasks', {key: task_key});
let success = true;
if (task['running'] || task['confirming']) success = false;
else await tasks_api.executeRun(task_key);
res.send({success: success});
});
app.post('/api/confirmTask', optionalJwt, async (req, res) => {
const task_key = req.body.task_key;
const task = await db_api.getRecord('tasks', {key: task_key});
let success = true;
if (task['running'] || task['confirming'] || !task['data']) success = false;
else await tasks_api.executeConfirm(task_key);
res.send({success: success});
});
app.post('/api/updateTaskSchedule', optionalJwt, async (req, res) => {
const task_key = req.body.task_key;
const new_schedule = req.body.new_schedule;
await tasks_api.updateTaskSchedule(task_key, new_schedule);
res.send({success: true});
});
// logs management
app.post('/api/logs', optionalJwt, async function(req, res) {

@ -1,6 +1,7 @@
const config_api = require('../config');
const consts = require('../consts');
const logger = require('../logger');
const db_api = require('../db');
const jwt = require('jsonwebtoken');
const { uuid } = require('uuidv4');
@ -12,15 +13,12 @@ var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
// other required vars
let db_api = null;
let SERVER_SECRET = null;
let JWT_EXPIRATION = null;
let opts = null;
let saltRounds = null;
exports.initialize = function(db_api) {
setDB(db_api);
exports.initialize = function() {
/*************************
* Authentication module
************************/
@ -51,10 +49,6 @@ exports.initialize = function(db_api) {
}));
}
function setDB(input_db_api) {
db_api = input_db_api;
}
exports.passport = require('passport');
exports.passport.serializeUser(function(user, done) {

@ -1,14 +1,6 @@
const utils = require('./utils');
const logger = require('./logger');
var db_api = null;
function setDB(input_db_api) { db_api = input_db_api }
function initialize(input_db_api) {
setDB(input_db_api);
}
const db_api = require('./db');
/*
Categories:
@ -137,7 +129,6 @@ function applyCategoryRules(file_json, rules, category_name) {
// }
module.exports = {
initialize: initialize,
categorize: categorize,
getCategories: getCategories,
getCategoriesAsPlaylists: getCategoriesAsPlaylists

@ -14,26 +14,19 @@ const twitch_api = require('./twitch');
const { create } = require('xmlbuilder2');
const categories_api = require('./categories');
const utils = require('./utils');
let db_api = null;
const db_api = require('./db');
const mutex = new Mutex();
let should_check_downloads = true;
const archivePath = path.join(__dirname, 'appdata', 'archives');
function setDB(input_db_api) { db_api = input_db_api }
exports.initialize = (input_db_api) => {
setDB(input_db_api);
categories_api.initialize(db_api);
if (db_api.database_initialized) {
setupDownloads();
} else {
db_api.database_initialized_bs.subscribe(init => {
if (init) setupDownloads();
});
}
if (db_api.database_initialized) {
setupDownloads();
} else {
db_api.database_initialized_bs.subscribe(init => {
if (init) setupDownloads();
});
}
exports.createDownload = async (url, type, options, user_uid = null, sub_id = null, sub_name = null) => {

@ -8,15 +8,8 @@ const logger = require('./logger');
const debugMode = process.env.YTDL_MODE === 'debug';
let db_api = null;
let downloader_api = null;
function setDB(input_db_api) { db_api = input_db_api }
function initialize(input_db_api, input_downloader_api) {
setDB(input_db_api);
downloader_api = input_downloader_api;
}
const db_api = require('./db');
const downloader_api = require('./downloader');
async function subscribe(sub, user_uid = null) {
const result_obj = {
@ -542,7 +535,6 @@ module.exports = {
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
initialize : initialize,
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
}

@ -31,7 +31,21 @@ const TASKS = {
}
function scheduleJob(task_key, schedule) {
return scheduler.scheduleJob(schedule, async () => {
// 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.`);
@ -43,15 +57,24 @@ function scheduleJob(task_key, schedule) {
});
}
exports.initialize = async () => {
if (db_api.database_initialized) {
setupTasks();
} else {
db_api.database_initialized_bs.subscribe(init => {
if (init) setupTasks();
});
}
const 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 into table if missing
// 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,
@ -85,12 +108,15 @@ exports.executeTask = async (task_key) => {
}
exports.executeRun = async (task_key) => {
logger.verbose(`Running task ${task_key}`);
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: data, 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;
}
@ -98,10 +124,12 @@ exports.executeConfirm = async (task_key) => {
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});
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();

@ -44,6 +44,7 @@ export type { GetAllDownloadsRequest } from './models/GetAllDownloadsRequest';
export type { GetAllDownloadsResponse } from './models/GetAllDownloadsResponse';
export type { GetAllFilesResponse } from './models/GetAllFilesResponse';
export type { GetAllSubscriptionsResponse } from './models/GetAllSubscriptionsResponse';
export type { GetAllTasksResponse } from './models/GetAllTasksResponse';
export type { GetDownloadRequest } from './models/GetDownloadRequest';
export type { GetDownloadResponse } from './models/GetDownloadResponse';
export type { GetFileFormatsRequest } from './models/GetFileFormatsRequest';
@ -63,6 +64,8 @@ export type { GetPlaylistsResponse } from './models/GetPlaylistsResponse';
export type { GetRolesResponse } from './models/GetRolesResponse';
export type { GetSubscriptionRequest } from './models/GetSubscriptionRequest';
export type { GetSubscriptionResponse } from './models/GetSubscriptionResponse';
export type { GetTaskRequest } from './models/GetTaskRequest';
export type { GetTaskResponse } from './models/GetTaskResponse';
export type { GetUsersResponse } from './models/GetUsersResponse';
export type { IncrementViewCountRequest } from './models/IncrementViewCountRequest';
export type { inline_response_200_15 } from './models/inline_response_200_15';
@ -71,6 +74,7 @@ export type { LoginResponse } from './models/LoginResponse';
export type { Playlist } from './models/Playlist';
export type { RegisterRequest } from './models/RegisterRequest';
export type { RegisterResponse } from './models/RegisterResponse';
export { Schedule } from './models/Schedule';
export type { SetConfigRequest } from './models/SetConfigRequest';
export type { SharingToggle } from './models/SharingToggle';
export type { SubscribeRequest } from './models/SubscribeRequest';
@ -79,6 +83,7 @@ export type { Subscription } from './models/Subscription';
export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
export type { SuccessObject } from './models/SuccessObject';
export type { TableInfo } from './models/TableInfo';
export type { Task } from './models/Task';
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
export type { TransferDBRequest } from './models/TransferDBRequest';
@ -93,6 +98,7 @@ export type { UpdateConcurrentStreamResponse } from './models/UpdateConcurrentSt
export type { UpdatePlaylistRequest } from './models/UpdatePlaylistRequest';
export type { UpdaterStatus } from './models/UpdaterStatus';
export type { UpdateServerRequest } from './models/UpdateServerRequest';
export type { UpdateTaskScheduleRequest } from './models/UpdateTaskScheduleRequest';
export type { UpdateUserRequest } from './models/UpdateUserRequest';
export type { User } from './models/User';
export { UserPermission } from './models/UserPermission';

@ -2,6 +2,7 @@
/* tslint:disable */
/* eslint-disable */
import { FileType } from './FileType';
export interface DownloadFileRequest {
uid?: string;
@ -9,5 +10,5 @@ export interface DownloadFileRequest {
sub_id?: string;
playlist_id?: string;
url?: string;
type?: string;
type?: FileType;
}

@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Task } from './Task';
export interface GetAllTasksResponse {
tasks?: Array<Task>;
}

@ -15,8 +15,5 @@ export interface GetFullTwitchChatRequest {
* User UID
*/
uuid?: string;
/**
* Subscription
*/
sub?: Subscription;
}

@ -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,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,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { Schedule } from './Schedule';
export interface UpdateTaskScheduleRequest {
task_key: string;
new_schedule: Schedule;
}

@ -8,6 +8,7 @@ import { PostsService } from './posts.services';
import { LoginComponent } from './components/login/login.component';
import { DownloadsComponent } from './components/downloads/downloads.component';
import { SettingsComponent } from './settings/settings.component';
import { TasksComponent } from './components/tasks/tasks.component';
const routes: Routes = [
{ path: 'home', component: MainComponent, canActivate: [PostsService] },
@ -17,6 +18,7 @@ const routes: Routes = [
{ path: 'settings', component: SettingsComponent, canActivate: [PostsService] },
{ path: 'login', component: LoginComponent },
{ path: 'downloads', component: DownloadsComponent, canActivate: [PostsService] },
{ path: 'tasks', component: TasksComponent, canActivate: [PostsService] },
{ path: '', redirectTo: '/home', pathMatch: 'full' }
];

@ -44,6 +44,7 @@
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
<ng-container *ngIf="postsService.config && postsService.hasPermission('settings')">
<mat-divider></mat-divider>
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>

@ -28,6 +28,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { TextFieldModule } from '@angular/cdk/text-field';
@ -87,6 +88,8 @@ import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.co
import { H401Interceptor } from './http.interceptor';
import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component';
import { SkipAdButtonComponent } from './components/skip-ad-button/skip-ad-button.component';
import { TasksComponent } from './components/tasks/tasks.component';
import { UpdateTaskScheduleDialogComponent } from './dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
registerLocaleData(es, 'es');
@ -135,7 +138,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
TwitchChatComponent,
SeeMoreComponent,
ConcurrentStreamComponent,
SkipAdButtonComponent
SkipAdButtonComponent,
TasksComponent,
UpdateTaskScheduleDialogComponent
],
imports: [
CommonModule,
@ -171,6 +176,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatPaginatorModule,
MatSortModule,
MatTableModule,
MatDatepickerModule,
MatChipsModule,
DragDropModule,
ClipboardModule,

@ -0,0 +1,77 @@
<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">
<ng-container i18n="Scheduled">Scheduled for</ng-container>&nbsp;
{{element.next_invocation | date: 'short'}}<mat-icon style="font-size: 16px; text-align: center;" *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>
<ng-container *ngIf="element.data?.uids?.length > 0">
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
<ng-container *ngIf="element.key == 'missing_files_check'" i18n="Clear missing files from DB">Clear missing files from DB:</ng-container>
<ng-container *ngIf="element.key == 'duplicate_files_check'" i18n="Clear duplicate files from DB">Clear duplicate files from DB:</ng-container>&nbsp;{{element.data.uids.length}}
</button>
</ng-container>
<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>
<button (click)="scheduleTask(element)" mat-icon-button matTooltip="Schedule" i18n-matTooltip="Schedule"><mat-icon>schedule</mat-icon></button>
</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>
</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,109 @@
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 { 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;
@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();
});
}
confirmTask(task_key: string): void {
this.postsService.confirmTask(task_key).subscribe(res => {
this.getTasks();
});
}
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);
});
}
});
}
}
export interface Task {
key: string;
title: string;
last_ran: number;
last_confirmed: number;
running: boolean;
confirming: boolean;
data: unknown;
}

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

@ -318,7 +318,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
const type = this.playlist[0].type;
const url = this.playlist[0].url;
this.downloading = true;
this.postsService.downloadFileFromServer(this.uid, this.uuid, this.sub_id, url, type).subscribe(res => {
this.postsService.downloadFileFromServer(this.uid, this.uuid, this.sub_id, url, type as FileType).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, filename + ext);

@ -90,6 +90,9 @@ import {
DBInfoResponse,
GetFileFormatsRequest,
GetFileFormatsResponse,
GetTaskRequest,
GetTaskResponse,
UpdateTaskScheduleRequest,
} from '../api-types';
import { isoLangs } from './settings/locales_list';
import { Title } from '@angular/platform-browser';
@ -351,7 +354,7 @@ export class PostsService implements CanActivate {
return this.http.post<GetAllFilesResponse>(this.path + 'getAllFiles', {sort: sort, range: range, text_search: text_search, file_type_filter: file_type_filter}, this.httpOptions);
}
downloadFileFromServer(uid: string, uuid: string = null, sub_id: string = null, url: string = null, type: string = null) {
downloadFileFromServer(uid: string, uuid: string = null, sub_id: string = null, url: string = null, type: FileType = null) {
const body: DownloadFileRequest = {
uid: uid,
uuid: uuid,
@ -544,38 +547,67 @@ export class PostsService implements CanActivate {
return this.http.post<GetDownloadResponse>(this.path + 'download', body, this.httpOptions);
}
pauseDownload(download_uid) {
return this.http.post<SuccessObject>(this.path + 'pauseDownload', {download_uid: download_uid}, this.httpOptions);
pauseDownload(download_uid: string) {
const body: GetDownloadRequest = {download_uid: download_uid};
return this.http.post<SuccessObject>(this.path + 'pauseDownload', body, this.httpOptions);
}
pauseAllDownloads() {
return this.http.post<SuccessObject>(this.path + 'pauseAllDownloads', {}, this.httpOptions);
}
resumeDownload(download_uid) {
return this.http.post<SuccessObject>(this.path + 'resumeDownload', {download_uid: download_uid}, this.httpOptions);
resumeDownload(download_uid: string) {
const body: GetDownloadRequest = {download_uid: download_uid};
return this.http.post<SuccessObject>(this.path + 'resumeDownload', body, this.httpOptions);
}
resumeAllDownloads() {
return this.http.post<SuccessObject>(this.path + 'resumeAllDownloads', {}, this.httpOptions);
}
restartDownload(download_uid) {
return this.http.post<SuccessObject>(this.path + 'restartDownload', {download_uid: download_uid}, this.httpOptions);
restartDownload(download_uid: string) {
const body: GetDownloadRequest = {download_uid: download_uid};
return this.http.post<SuccessObject>(this.path + 'restartDownload', body, this.httpOptions);
}
cancelDownload(download_uid) {
return this.http.post<SuccessObject>(this.path + 'cancelDownload', {download_uid: download_uid}, this.httpOptions);
cancelDownload(download_uid: string) {
const body: GetDownloadRequest = {download_uid: download_uid};
return this.http.post<SuccessObject>(this.path + 'cancelDownload', body, this.httpOptions);
}
clearDownload(download_uid) {
return this.http.post<SuccessObject>(this.path + 'clearDownload', {download_uid: download_uid}, this.httpOptions);
clearDownload(download_uid: string) {
const body: GetDownloadRequest = {download_uid: download_uid};
return this.http.post<SuccessObject>(this.path + 'clearDownload', body, this.httpOptions);
}
clearFinishedDownloads() {
return this.http.post<SuccessObject>(this.path + 'clearFinishedDownloads', {}, this.httpOptions);
}
getTasks() {
return this.http.post<SuccessObject>(this.path + 'getTasks', {}, this.httpOptions);
}
getTask(task_key) {
const body: GetTaskRequest = {task_key: task_key};
return this.http.post<GetTaskResponse>(this.path + 'getTask', body, this.httpOptions);
}
runTask(task_key) {
const body: GetTaskRequest = {task_key: task_key};
return this.http.post<SuccessObject>(this.path + 'runTask', body, this.httpOptions);
}
confirmTask(task_key) {
const body: GetTaskRequest = {task_key: task_key};
return this.http.post<SuccessObject>(this.path + 'confirmTask', body, this.httpOptions);
}
updateTaskSchedule(task_key, schedule) {
const body: UpdateTaskScheduleRequest = {task_key: task_key, new_schedule: schedule};
return this.http.post<SuccessObject>(this.path + 'updateTaskSchedule', body, this.httpOptions);
}
getVersionInfo() {
return this.http.get<VersionInfoResponse>(this.path + 'versionInfo', this.httpOptions);
}

Loading…
Cancel
Save