diff --git a/server/job/schedule.ts b/server/job/schedule.ts index 725e67b5..7925dd21 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', { @@ -134,12 +134,12 @@ export const startJobs = (): void => { }), }); - // Run image cache cleanup every 5 minutes + // Run image cache cleanup every 24 hours scheduledJobs.push({ id: 'image-cache-cleanup', name: 'Image Cache Cleanup', type: 'process', - interval: 'long', + interval: 'hours', 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 0136113a..34b379e2 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,6 +40,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { const [requestModal, setRequestModal] = useState(false); const [is4k, setIs4k] = useState(false); + const returnCollectionDownloadItems = (data: Collection | undefined) => { + const [downloadStatus, downloadStatus4k] = [ + data?.parts.flatMap((item) => + item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] + ), + data?.parts.flatMap((item) => + item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : [] + ), + ]; + + return { downloadStatus, downloadStatus4k }; + }; + const { data, error, @@ -46,21 +60,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => { } = useSWR(`/api/v1/collection/${router.query.collectionId}`, { fallbackData: collection, revalidateOnMount: true, + 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 6f8b2e01..1b142d4d 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, @@ -110,6 +111,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { mutate: revalidate, } = useSWR(`/api/v1/movie/${router.query.movieId}`, { fallbackData: 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 a7a76bec..44abd555 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, @@ -220,6 +221,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { request.type === 'movie' ? `/api/v1/movie/${request.media.tmdbId}` : `/api/v1/tv/${request.media.tmdbId}`; + const { data: title, error } = useSWR( inView ? `${url}` : null ); @@ -229,6 +231,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { mutate: revalidate, } = useSWR(`/api/v1/request/${request.id}`, { fallbackData: 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 dbce03e5..a42483ab 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, @@ -293,6 +294,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { `/api/v1/request/${request.id}`, { fallbackData: 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 cd985619..b8ebe708 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' ? (