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:
committed by
GitHub
parent
5683f55ebf
commit
82614ca441
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user