feat(jobs): allow modifying job schedules (#1440)

* feat(jobs): backend implementation

* feat(jobs): initial frontend implementation

* feat(jobs): store job settings as Record

* feat(jobs): use heroicons/react instead of inline svgs

* feat(jobs): use presets instead of cron expressions

* feat(jobs): ran `yarn i18n:extract`

* feat(jobs): suggested changes

- use job ids in settings
- add intervalDuration to jobs to allow choosing only minutes or hours for the job schedule
- move job schedule defaults to settings.json
- better TS types for jobs in settings cache component
- make suggested changes to wording
- plural form for label when job schedule can be defined in minutes
- add fixed job interval duration
- add predefined interval choices for minutes and hours
- add new schema for job to overseerr api

* feat(jobs): required change for CI to not fail

* feat(jobs): suggested changes

* fix(jobs): revert offending type refactor
This commit is contained in:
Danshil Kokil Mungur
2021-10-15 16:23:39 +04:00
committed by GitHub
parent 5683f55ebf
commit 82614ca441
6 changed files with 310 additions and 66 deletions

View File

@@ -1,15 +1,17 @@
import schedule from 'node-schedule';
import logger from '../logger';
import downloadTracker from '../lib/downloadtracker';
import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex';
import { radarrScanner } from '../lib/scanners/radarr';
import { sonarrScanner } from '../lib/scanners/sonarr';
import { getSettings, JobId } from '../lib/settings';
import logger from '../logger';
interface ScheduledJob {
id: string;
id: JobId;
job: schedule.Job;
name: string;
type: 'process' | 'command';
interval: 'short' | 'long' | 'fixed';
running?: () => boolean;
cancelFn?: () => void;
}
@@ -17,12 +19,15 @@ interface ScheduledJob {
export const scheduledJobs: ScheduledJob[] = [];
export const startJobs = (): void => {
const jobs = getSettings().jobs;
// Run recently added plex scan every 5 minutes
scheduledJobs.push({
id: 'plex-recently-added-scan',
name: 'Plex Recently Added Scan',
type: 'process',
job: schedule.scheduleJob('0 */5 * * * *', () => {
interval: 'short',
job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Recently Added Scan', {
label: 'Jobs',
});
@@ -37,7 +42,8 @@ export const startJobs = (): void => {
id: 'plex-full-scan',
name: 'Plex Full Library Scan',
type: 'process',
job: schedule.scheduleJob('0 0 3 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Full Library Scan', {
label: 'Jobs',
});
@@ -52,7 +58,8 @@ export const startJobs = (): void => {
id: 'radarr-scan',
name: 'Radarr Scan',
type: 'process',
job: schedule.scheduleJob('0 0 4 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
radarrScanner.run();
}),
@@ -65,7 +72,8 @@ export const startJobs = (): void => {
id: 'sonarr-scan',
name: 'Sonarr Scan',
type: 'process',
job: schedule.scheduleJob('0 30 4 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
sonarrScanner.run();
}),
@@ -73,23 +81,27 @@ export const startJobs = (): void => {
cancelFn: () => sonarrScanner.cancel(),
});
// Run download sync
// Run download sync every minute
scheduledJobs.push({
id: 'download-sync',
name: 'Download Sync',
type: 'command',
job: schedule.scheduleJob('0 * * * * *', () => {
logger.debug('Starting scheduled job: Download Sync', { label: 'Jobs' });
interval: 'fixed',
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
logger.debug('Starting scheduled job: Download Sync', {
label: 'Jobs',
});
downloadTracker.updateDownloads();
}),
});
// Reset download sync
// Reset download sync everyday at 01:00 am
scheduledJobs.push({
id: 'download-sync-reset',
name: 'Download Sync Reset',
type: 'command',
job: schedule.scheduleJob('0 0 1 * * *', () => {
interval: 'long',
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
logger.info('Starting scheduled job: Download Sync Reset', {
label: 'Jobs',
});

View File

@@ -215,6 +215,18 @@ interface NotificationSettings {
agents: NotificationAgents;
}
interface JobSettings {
schedule: string;
}
export type JobId =
| 'plex-recently-added-scan'
| 'plex-full-scan'
| 'radarr-scan'
| 'sonarr-scan'
| 'download-sync'
| 'download-sync-reset';
interface AllSettings {
clientId: string;
vapidPublic: string;
@@ -225,6 +237,7 @@ interface AllSettings {
sonarr: SonarrSettings[];
public: PublicSettings;
notifications: NotificationSettings;
jobs: Record<JobId, JobSettings>;
}
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
@@ -346,6 +359,26 @@ class Settings {
},
},
},
jobs: {
'plex-recently-added-scan': {
schedule: '0 */5 * * * *',
},
'plex-full-scan': {
schedule: '0 0 3 * * *',
},
'radarr-scan': {
schedule: '0 0 4 * * *',
},
'sonarr-scan': {
schedule: '0 30 4 * * *',
},
'download-sync': {
schedule: '0 * * * * *',
},
'download-sync-reset': {
schedule: '0 0 1 * * *',
},
},
};
if (initialSettings) {
this.data = merge(this.data, initialSettings);
@@ -428,6 +461,14 @@ class Settings {
this.data.notifications = data;
}
get jobs(): Record<JobId, JobSettings> {
return this.data.jobs;
}
set jobs(data: Record<JobId, JobSettings>) {
this.data.jobs = data;
}
get clientId(): string {
if (!this.data.clientId) {
this.data.clientId = randomUUID();

View File

@@ -2,6 +2,7 @@ import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import fs from 'fs';
import { merge, omit } from 'lodash';
import { rescheduleJob } from 'node-schedule';
import path from 'path';
import { getRepository } from 'typeorm';
import { URL } from 'url';
@@ -49,7 +50,7 @@ settingsRoutes.get('/main', (req, res, next) => {
const settings = getSettings();
if (!req.user) {
return next({ status: 500, message: 'User missing from request' });
return next({ status: 400, message: 'User missing from request' });
}
res.status(200).json(filteredMainSettings(req.user, settings.main));
@@ -310,6 +311,7 @@ settingsRoutes.get('/jobs', (_req, res) => {
id: job.id,
name: job.name,
type: job.type,
interval: job.interval,
nextExecutionTime: job.job.nextInvocation(),
running: job.running ? job.running() : false,
}))
@@ -329,6 +331,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
id: scheduledJob.id,
name: scheduledJob.name,
type: scheduledJob.type,
interval: scheduledJob.interval,
nextExecutionTime: scheduledJob.job.nextInvocation(),
running: scheduledJob.running ? scheduledJob.running() : false,
});
@@ -353,12 +356,45 @@ settingsRoutes.post<{ jobId: string }>(
id: scheduledJob.id,
name: scheduledJob.name,
type: scheduledJob.type,
interval: scheduledJob.interval,
nextExecutionTime: scheduledJob.job.nextInvocation(),
running: scheduledJob.running ? scheduledJob.running() : false,
});
}
);
settingsRoutes.post<{ jobId: string }>(
'/jobs/:jobId/schedule',
(req, res, next) => {
const scheduledJob = scheduledJobs.find(
(job) => job.id === req.params.jobId
);
if (!scheduledJob) {
return next({ status: 404, message: 'Job not found' });
}
const result = rescheduleJob(scheduledJob.job, req.body.schedule);
const settings = getSettings();
if (result) {
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
settings.save();
return res.status(200).json({
id: scheduledJob.id,
name: scheduledJob.name,
type: scheduledJob.type,
interval: scheduledJob.interval,
nextExecutionTime: scheduledJob.job.nextInvocation(),
running: scheduledJob.running ? scheduledJob.running() : false,
});
} else {
return next({ status: 400, message: 'Invalid job schedule' });
}
}
);
settingsRoutes.get('/cache', (req, res) => {
const caches = cacheManager.getAllCaches();