From 2f1d500500cdf06adc5eddac567353d4e40e8a2c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 26 Jan 2023 19:40:47 -0500 Subject: [PATCH] feat: added editable download sync schedule --- server/job/schedule.ts | 18 ++++---- src/components/CollectionDetails/index.tsx | 31 +++++--------- src/components/MovieDetails/index.tsx | 20 ++++----- src/components/RequestCard/index.tsx | 20 ++++----- .../RequestList/RequestItem/index.tsx | 21 ++++------ .../Settings/SettingsJobsCache/index.tsx | 41 ++++++++++++++++--- src/components/TvDetails/index.tsx | 20 ++++----- src/i18n/locale/en.json | 1 + src/utils/refreshIntervalHelper.ts | 18 ++++++++ 9 files changed, 107 insertions(+), 83 deletions(-) create mode 100644 src/utils/refreshIntervalHelper.ts diff --git a/server/job/schedule.ts b/server/job/schedule.ts index 725e67b577..2a50217675 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -14,7 +14,7 @@ interface ScheduledJob { job: schedule.Job; name: string; type: 'process' | 'command'; - interval: 'short' | 'long' | 'fixed'; + interval: 'seconds' | 'minutes' | 'hours' | 'fixed'; cronSchedule: string; running?: () => boolean; cancelFn?: () => void; @@ -30,7 +30,7 @@ export const startJobs = (): void => { id: 'plex-recently-added-scan', name: 'Plex Recently Added Scan', type: 'process', - interval: 'short', + interval: 'minutes', cronSchedule: jobs['plex-recently-added-scan'].schedule, job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => { logger.info('Starting scheduled job: Plex Recently Added Scan', { @@ -47,7 +47,7 @@ export const startJobs = (): void => { id: 'plex-full-scan', name: 'Plex Full Library Scan', type: 'process', - interval: 'long', + interval: 'hours', cronSchedule: jobs['plex-full-scan'].schedule, job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => { logger.info('Starting scheduled job: Plex Full Library Scan', { @@ -64,7 +64,7 @@ export const startJobs = (): void => { id: 'plex-watchlist-sync', name: 'Plex Watchlist Sync', type: 'process', - interval: 'short', + interval: 'minutes', cronSchedule: jobs['plex-watchlist-sync'].schedule, job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => { logger.info('Starting scheduled job: Plex Watchlist Sync', { @@ -79,7 +79,7 @@ export const startJobs = (): void => { id: 'radarr-scan', name: 'Radarr Scan', type: 'process', - interval: 'long', + interval: 'hours', cronSchedule: jobs['radarr-scan'].schedule, job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => { logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' }); @@ -94,7 +94,7 @@ export const startJobs = (): void => { id: 'sonarr-scan', name: 'Sonarr Scan', type: 'process', - interval: 'long', + interval: 'hours', cronSchedule: jobs['sonarr-scan'].schedule, job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => { logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' }); @@ -109,7 +109,7 @@ export const startJobs = (): void => { id: 'download-sync', name: 'Download Sync', type: 'command', - interval: 'fixed', + interval: 'seconds', cronSchedule: jobs['download-sync'].schedule, job: schedule.scheduleJob(jobs['download-sync'].schedule, () => { logger.debug('Starting scheduled job: Download Sync', { @@ -124,7 +124,7 @@ export const startJobs = (): void => { id: 'download-sync-reset', name: 'Download Sync Reset', type: 'command', - interval: 'long', + interval: 'hours', cronSchedule: jobs['download-sync-reset'].schedule, job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => { logger.info('Starting scheduled job: Download Sync Reset', { @@ -139,7 +139,7 @@ export const startJobs = (): void => { id: 'image-cache-cleanup', name: 'Image Cache Cleanup', type: 'process', - interval: 'long', + interval: 'minutes', cronSchedule: jobs['image-cache-cleanup'].schedule, job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => { logger.info('Starting scheduled job: Image Cache Cleanup', { diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index da28a11dea..34b379e24e 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -10,6 +10,7 @@ import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; +import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { MediaStatus } from '@server/constants/media'; import type { Collection } from '@server/models/Collection'; @@ -39,8 +40,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { const [requestModal, setRequestModal] = useState(false); const [is4k, setIs4k] = useState(false); - const refreshIntervalChecker = (data: Collection | undefined) => { - const [tempDownloadStatus, tempDownloadStatus4k] = [ + const returnCollectionDownloadItems = (data: Collection | undefined) => { + const [downloadStatus, downloadStatus4k] = [ data?.parts.flatMap((item) => item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] ), @@ -49,14 +50,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { ), ]; - if ( - (tempDownloadStatus ?? []).length > 0 || - (tempDownloadStatus4k ?? []).length > 0 - ) { - return 5000; - } else { - return 0; - } + return { downloadStatus, downloadStatus4k }; }; const { @@ -66,22 +60,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { } = useSWR(`/api/v1/collection/${router.query.collectionId}`, { fallbackData: collection, revalidateOnMount: true, - refreshInterval: refreshIntervalChecker(collection), + refreshInterval: refreshIntervalHelper( + returnCollectionDownloadItems(collection), + 15000 + ), }); const { data: genres } = useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`); const [downloadStatus, downloadStatus4k] = useMemo(() => { - return [ - data?.parts.flatMap((item) => - item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] - ), - data?.parts.flatMap((item) => - item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : [] - ), - ]; - }, [data?.parts]); + const downloadItems = returnCollectionDownloadItems(data); + return [downloadItems.downloadStatus, downloadItems.downloadStatus4k]; + }, [data]); const [titles, titles4k] = useMemo(() => { return [ diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index f7535e0b3b..1b142d4de8 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -26,6 +26,7 @@ import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import { sortCrewPriority } from '@app/utils/creditHelpers'; +import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { ArrowRightCircleIcon, CloudIcon, @@ -104,24 +105,19 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { const [showMoreStudios, setShowMoreStudios] = useState(false); const [showIssueModal, setShowIssueModal] = useState(false); - const refreshIntervalChecker = (data: MovieDetailsType | undefined) => { - if ( - (data?.mediaInfo?.downloadStatus ?? []).length > 0 || - (data?.mediaInfo?.downloadStatus4k ?? []).length > 0 - ) { - return 5000; - } else { - return 0; - } - }; - const { data, error, mutate: revalidate, } = useSWR(`/api/v1/movie/${router.query.movieId}`, { fallbackData: movie, - refreshInterval: refreshIntervalChecker(movie), + refreshInterval: refreshIntervalHelper( + { + downloadStatus: movie?.mediaInfo?.downloadStatus, + downloadStatus4k: movie?.mediaInfo?.downloadStatus4k, + }, + 15000 + ), }); const { data: ratingData } = useSWR( diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 6f0c5e3998..44abd555a8 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; +import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { withProperties } from '@app/utils/typeHelpers'; import { ArrowPathIcon, @@ -221,17 +222,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { ? `/api/v1/movie/${request.media.tmdbId}` : `/api/v1/tv/${request.media.tmdbId}`; - const refreshIntervalChecker = (data: MediaRequest) => { - if ( - (data.media.downloadStatus ?? []).length > 0 || - (data.media.downloadStatus4k ?? []).length > 0 - ) { - return 5000; - } else { - return 0; - } - }; - const { data: title, error } = useSWR( inView ? `${url}` : null ); @@ -241,7 +231,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { mutate: revalidate, } = useSWR(`/api/v1/request/${request.id}`, { fallbackData: request, - refreshInterval: refreshIntervalChecker(request), + refreshInterval: refreshIntervalHelper( + { + downloadStatus: request.media.downloadStatus, + downloadStatus4k: request.media.downloadStatus4k, + }, + 15000 + ), }); const { plexUrl, plexUrl4k } = useDeepLinks({ diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 6aa3449afb..a42483abe4 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; +import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { ArrowPathIcon, CheckIcon, @@ -286,18 +287,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { request.type === 'movie' ? `/api/v1/movie/${request.media.tmdbId}` : `/api/v1/tv/${request.media.tmdbId}`; - - const refreshIntervalChecker = (data: MediaRequest) => { - if ( - (data.media.downloadStatus ?? []).length > 0 || - (data.media.downloadStatus4k ?? []).length > 0 - ) { - return 5000; - } else { - return 0; - } - }; - const { data: title, error } = useSWR( inView ? url : null ); @@ -305,7 +294,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { `/api/v1/request/${request.id}`, { fallbackData: request, - refreshInterval: refreshIntervalChecker(request), + refreshInterval: refreshIntervalHelper( + { + downloadStatus: request.media.downloadStatus, + downloadStatus4k: request.media.downloadStatus4k, + }, + 15000 + ), } ); diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index cd98561955..b8ebe70854 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -67,6 +67,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({ 'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}', editJobScheduleSelectorMinutes: 'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}', + editJobScheduleSelectorSeconds: + 'Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}', imagecache: 'Image Cache', imagecacheDescription: 'When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in {appDataPath}/cache/images.', @@ -78,7 +80,7 @@ interface Job { id: JobId; name: string; type: 'process' | 'command'; - interval: 'short' | 'long' | 'fixed'; + interval: 'seconds' | 'minutes' | 'hours' | 'fixed'; cronSchedule: string; nextExecutionTime: string; running: boolean; @@ -89,10 +91,11 @@ type JobModalState = { job?: Job; scheduleHours: number; scheduleMinutes: number; + scheduleSeconds: number; }; type JobModalAction = - | { type: 'set'; hours?: number; minutes?: number } + | { type: 'set'; hours?: number; minutes?: number; seconds?: number } | { type: 'close'; } @@ -115,6 +118,7 @@ const jobModalReducer = ( job: action.job, scheduleHours: 1, scheduleMinutes: 5, + scheduleSeconds: 30, }; case 'set': @@ -122,6 +126,7 @@ const jobModalReducer = ( ...state, scheduleHours: action.hours ?? state.scheduleHours, scheduleMinutes: action.minutes ?? state.scheduleMinutes, + scheduleSeconds: action.seconds ?? state.scheduleSeconds, }; } }; @@ -149,6 +154,7 @@ const SettingsJobs = () => { isOpen: false, scheduleHours: 1, scheduleMinutes: 5, + scheduleSeconds: 30, }); const [isSaving, setIsSaving] = useState(false); @@ -200,9 +206,11 @@ const SettingsJobs = () => { const jobScheduleCron = ['0', '0', '*', '*', '*', '*']; try { - if (jobModalState.job?.interval === 'short') { + if (jobModalState.job?.interval === 'seconds') { + jobScheduleCron.splice(0, 2, `*/${jobModalState.scheduleSeconds}`, '*'); + } else if (jobModalState.job?.interval === 'minutes') { jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`; - } else if (jobModalState.job?.interval === 'long') { + } else if (jobModalState.job?.interval === 'hours') { jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`; } else { // jobs with interval: fixed should not be editable @@ -286,7 +294,30 @@ const SettingsJobs = () => { {intl.formatMessage(messages.editJobSchedulePrompt)}
- {jobModalState.job?.interval === 'short' ? ( + {jobModalState.job?.interval === 'seconds' ? ( + + ) : jobModalState.job?.interval === 'minutes' ? (