diff --git a/Public API v1.yaml b/Public API v1.yaml index ad43d48..fc63ef8 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -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 diff --git a/backend/app.js b/backend/app.js index b12d502..8aa2696 100644 --- a/backend/app.js +++ b/backend/app.js @@ -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) { diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index 7aad070..de54a0b 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -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) { diff --git a/backend/categories.js b/backend/categories.js index 269ae9c..2236d9f 100644 --- a/backend/categories.js +++ b/backend/categories.js @@ -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 diff --git a/backend/downloader.js b/backend/downloader.js index e151f87..f139a1f 100644 --- a/backend/downloader.js +++ b/backend/downloader.js @@ -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) => { diff --git a/backend/subscriptions.js b/backend/subscriptions.js index 869cf76..1b35a4f 100644 --- a/backend/subscriptions.js +++ b/backend/subscriptions.js @@ -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 } diff --git a/backend/tasks.js b/backend/tasks.js index 701cbff..c66b23a 100644 --- a/backend/tasks.js +++ b/backend/tasks.js @@ -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(); diff --git a/src/api-types/index.ts b/src/api-types/index.ts index c08044a..93d611f 100644 --- a/src/api-types/index.ts +++ b/src/api-types/index.ts @@ -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'; diff --git a/src/api-types/models/DownloadFileRequest.ts b/src/api-types/models/DownloadFileRequest.ts index 31ba393..a874a8c 100644 --- a/src/api-types/models/DownloadFileRequest.ts +++ b/src/api-types/models/DownloadFileRequest.ts @@ -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; } \ No newline at end of file diff --git a/src/api-types/models/GetAllTasksResponse.ts b/src/api-types/models/GetAllTasksResponse.ts new file mode 100644 index 0000000..221d44a --- /dev/null +++ b/src/api-types/models/GetAllTasksResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Task } from './Task'; + +export interface GetAllTasksResponse { + tasks?: Array; +} \ No newline at end of file diff --git a/src/api-types/models/GetFullTwitchChatRequest.ts b/src/api-types/models/GetFullTwitchChatRequest.ts index 6d6a3a6..64a043d 100644 --- a/src/api-types/models/GetFullTwitchChatRequest.ts +++ b/src/api-types/models/GetFullTwitchChatRequest.ts @@ -15,8 +15,5 @@ export interface GetFullTwitchChatRequest { * User UID */ uuid?: string; - /** - * Subscription - */ sub?: Subscription; } \ No newline at end of file diff --git a/src/api-types/models/GetTaskRequest.ts b/src/api-types/models/GetTaskRequest.ts new file mode 100644 index 0000000..655a69f --- /dev/null +++ b/src/api-types/models/GetTaskRequest.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface GetTaskRequest { + task_key: string; +} \ No newline at end of file diff --git a/src/api-types/models/GetTaskResponse.ts b/src/api-types/models/GetTaskResponse.ts new file mode 100644 index 0000000..7f11c6e --- /dev/null +++ b/src/api-types/models/GetTaskResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Task } from './Task'; + +export interface GetTaskResponse { + task?: Task; +} \ No newline at end of file diff --git a/src/api-types/models/Schedule.ts b/src/api-types/models/Schedule.ts new file mode 100644 index 0000000..452202d --- /dev/null +++ b/src/api-types/models/Schedule.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + + +export interface Schedule { + type: Schedule.type; + data: { +dayOfWeek?: Array, +hour?: number, +minute?: number, +timestamp?: number, +}; +} + +export namespace Schedule { + + export enum type { + TIMESTAMP = 'timestamp', + RECURRING = 'recurring', + } + + +} \ No newline at end of file diff --git a/src/api-types/models/Task.ts b/src/api-types/models/Task.ts new file mode 100644 index 0000000..95c864a --- /dev/null +++ b/src/api-types/models/Task.ts @@ -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; +} \ No newline at end of file diff --git a/src/api-types/models/UpdateTaskScheduleRequest.ts b/src/api-types/models/UpdateTaskScheduleRequest.ts new file mode 100644 index 0000000..b9e61d6 --- /dev/null +++ b/src/api-types/models/UpdateTaskScheduleRequest.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import { Schedule } from './Schedule'; + +export interface UpdateTaskScheduleRequest { + task_key: string; + new_schedule: Schedule; +} \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 8620fcb..ab9c609 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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' } ]; diff --git a/src/app/app.component.html b/src/app/app.component.html index df61ad2..f1270c7 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -44,6 +44,7 @@ Login Subscriptions Downloads + Tasks Settings diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7b26e63..0da23ee 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/components/tasks/tasks.component.html b/src/app/components/tasks/tasks.component.html new file mode 100644 index 0000000..556ded4 --- /dev/null +++ b/src/app/components/tasks/tasks.component.html @@ -0,0 +1,77 @@ +
+
+ + + + Title + + + {{element.title}} + + + + + + + Last ran + + {{element.last_ran*1000 | date: 'short'}} + N/A + + + + + + Last confirmed + + {{element.last_confirmed*1000 | date: 'short'}} + N/A + + + + + + Status + + + + Scheduled for  + {{element.next_invocation | date: 'short'}}repeat + + + Not scheduled + + + + + + + Actions + +
+ + + + + +
+
+
+ + + +
+ + + +
+
+ +
+

No tasks available!

+
\ No newline at end of file diff --git a/src/app/components/tasks/tasks.component.scss b/src/app/components/tasks/tasks.component.scss new file mode 100644 index 0000000..ed84df9 --- /dev/null +++ b/src/app/components/tasks/tasks.component.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/components/tasks/tasks.component.spec.ts b/src/app/components/tasks/tasks.component.spec.ts new file mode 100644 index 0000000..d4ab7a5 --- /dev/null +++ b/src/app/components/tasks/tasks.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TasksComponent } from './tasks.component'; + +describe('TasksComponent', () => { + let component: TasksComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TasksComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TasksComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/tasks/tasks.component.ts b/src/app/components/tasks/tasks.component.ts new file mode 100644 index 0000000..a294b5b --- /dev/null +++ b/src/app/components/tasks/tasks.component.ts @@ -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(this.tasks); + } else { + this.tasks = res['tasks']; + this.dataSource = new MatTableDataSource(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; +} \ No newline at end of file diff --git a/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html new file mode 100644 index 0000000..7782e15 --- /dev/null +++ b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.html @@ -0,0 +1,53 @@ +

Update task schedule

+ + +
+
+
+ Enabled +
+
+ Recurring +
+
+ + + Weekly + Daily + + +
+
+ + Choose a date + + + + +
+
+ + + M + T + W + T + F + S + S + +
+
+ + Time + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.scss b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.spec.ts b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.spec.ts new file mode 100644 index 0000000..fe52670 --- /dev/null +++ b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UpdateTaskScheduleDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UpdateTaskScheduleDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts new file mode 100644 index 0000000..ef61cf4 --- /dev/null +++ b/src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts @@ -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, 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); + } +} diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 5fe3936..1a4e0ca 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -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); diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index f157d47..923210a 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -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(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(this.path + 'download', body, this.httpOptions); } - pauseDownload(download_uid) { - return this.http.post(this.path + 'pauseDownload', {download_uid: download_uid}, this.httpOptions); + pauseDownload(download_uid: string) { + const body: GetDownloadRequest = {download_uid: download_uid}; + return this.http.post(this.path + 'pauseDownload', body, this.httpOptions); } pauseAllDownloads() { return this.http.post(this.path + 'pauseAllDownloads', {}, this.httpOptions); } - resumeDownload(download_uid) { - return this.http.post(this.path + 'resumeDownload', {download_uid: download_uid}, this.httpOptions); + resumeDownload(download_uid: string) { + const body: GetDownloadRequest = {download_uid: download_uid}; + return this.http.post(this.path + 'resumeDownload', body, this.httpOptions); } resumeAllDownloads() { return this.http.post(this.path + 'resumeAllDownloads', {}, this.httpOptions); } - restartDownload(download_uid) { - return this.http.post(this.path + 'restartDownload', {download_uid: download_uid}, this.httpOptions); + restartDownload(download_uid: string) { + const body: GetDownloadRequest = {download_uid: download_uid}; + return this.http.post(this.path + 'restartDownload', body, this.httpOptions); } - cancelDownload(download_uid) { - return this.http.post(this.path + 'cancelDownload', {download_uid: download_uid}, this.httpOptions); + cancelDownload(download_uid: string) { + const body: GetDownloadRequest = {download_uid: download_uid}; + return this.http.post(this.path + 'cancelDownload', body, this.httpOptions); } - clearDownload(download_uid) { - return this.http.post(this.path + 'clearDownload', {download_uid: download_uid}, this.httpOptions); + clearDownload(download_uid: string) { + const body: GetDownloadRequest = {download_uid: download_uid}; + return this.http.post(this.path + 'clearDownload', body, this.httpOptions); } clearFinishedDownloads() { return this.http.post(this.path + 'clearFinishedDownloads', {}, this.httpOptions); } + getTasks() { + return this.http.post(this.path + 'getTasks', {}, this.httpOptions); + } + + getTask(task_key) { + const body: GetTaskRequest = {task_key: task_key}; + return this.http.post(this.path + 'getTask', body, this.httpOptions); + } + + runTask(task_key) { + const body: GetTaskRequest = {task_key: task_key}; + return this.http.post(this.path + 'runTask', body, this.httpOptions); + } + + confirmTask(task_key) { + const body: GetTaskRequest = {task_key: task_key}; + return this.http.post(this.path + 'confirmTask', body, this.httpOptions); + } + + updateTaskSchedule(task_key, schedule) { + const body: UpdateTaskScheduleRequest = {task_key: task_key, new_schedule: schedule}; + return this.http.post(this.path + 'updateTaskSchedule', body, this.httpOptions); + } + getVersionInfo() { return this.http.get(this.path + 'versionInfo', this.httpOptions); }