fix: added a refresh interval if download status is in progress (#3275)

* fix: added a refresh interval if download status is in progress

* refactor: switched to a function instead of useEffect

* feat: added editable download sync schedule
pull/3346/head
Brandon Cohen 2 years ago committed by GitHub
parent dd1378cef5
commit 1e2c6f46ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,7 +14,7 @@ interface ScheduledJob {
job: schedule.Job; job: schedule.Job;
name: string; name: string;
type: 'process' | 'command'; type: 'process' | 'command';
interval: 'short' | 'long' | 'fixed'; interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
cronSchedule: string; cronSchedule: string;
running?: () => boolean; running?: () => boolean;
cancelFn?: () => void; cancelFn?: () => void;
@ -30,7 +30,7 @@ export const startJobs = (): void => {
id: 'plex-recently-added-scan', id: 'plex-recently-added-scan',
name: 'Plex Recently Added Scan', name: 'Plex Recently Added Scan',
type: 'process', type: 'process',
interval: 'short', interval: 'minutes',
cronSchedule: jobs['plex-recently-added-scan'].schedule, cronSchedule: jobs['plex-recently-added-scan'].schedule,
job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => { job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Recently Added Scan', { logger.info('Starting scheduled job: Plex Recently Added Scan', {
@ -47,7 +47,7 @@ export const startJobs = (): void => {
id: 'plex-full-scan', id: 'plex-full-scan',
name: 'Plex Full Library Scan', name: 'Plex Full Library Scan',
type: 'process', type: 'process',
interval: 'long', interval: 'hours',
cronSchedule: jobs['plex-full-scan'].schedule, cronSchedule: jobs['plex-full-scan'].schedule,
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => { job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
logger.info('Starting scheduled job: Plex Full Library Scan', { logger.info('Starting scheduled job: Plex Full Library Scan', {
@ -64,7 +64,7 @@ export const startJobs = (): void => {
id: 'plex-watchlist-sync', id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync', name: 'Plex Watchlist Sync',
type: 'process', type: 'process',
interval: 'short', interval: 'minutes',
cronSchedule: jobs['plex-watchlist-sync'].schedule, cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => { job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', { logger.info('Starting scheduled job: Plex Watchlist Sync', {
@ -79,7 +79,7 @@ export const startJobs = (): void => {
id: 'radarr-scan', id: 'radarr-scan',
name: 'Radarr Scan', name: 'Radarr Scan',
type: 'process', type: 'process',
interval: 'long', interval: 'hours',
cronSchedule: jobs['radarr-scan'].schedule, cronSchedule: jobs['radarr-scan'].schedule,
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => { job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' }); logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
@ -94,7 +94,7 @@ export const startJobs = (): void => {
id: 'sonarr-scan', id: 'sonarr-scan',
name: 'Sonarr Scan', name: 'Sonarr Scan',
type: 'process', type: 'process',
interval: 'long', interval: 'hours',
cronSchedule: jobs['sonarr-scan'].schedule, cronSchedule: jobs['sonarr-scan'].schedule,
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => { job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' }); logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
@ -109,7 +109,7 @@ export const startJobs = (): void => {
id: 'download-sync', id: 'download-sync',
name: 'Download Sync', name: 'Download Sync',
type: 'command', type: 'command',
interval: 'fixed', interval: 'seconds',
cronSchedule: jobs['download-sync'].schedule, cronSchedule: jobs['download-sync'].schedule,
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => { job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
logger.debug('Starting scheduled job: Download Sync', { logger.debug('Starting scheduled job: Download Sync', {
@ -124,7 +124,7 @@ export const startJobs = (): void => {
id: 'download-sync-reset', id: 'download-sync-reset',
name: 'Download Sync Reset', name: 'Download Sync Reset',
type: 'command', type: 'command',
interval: 'long', interval: 'hours',
cronSchedule: jobs['download-sync-reset'].schedule, cronSchedule: jobs['download-sync-reset'].schedule,
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => { job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
logger.info('Starting scheduled job: Download Sync Reset', { 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({ scheduledJobs.push({
id: 'image-cache-cleanup', id: 'image-cache-cleanup',
name: 'Image Cache Cleanup', name: 'Image Cache Cleanup',
type: 'process', type: 'process',
interval: 'long', interval: 'hours',
cronSchedule: jobs['image-cache-cleanup'].schedule, cronSchedule: jobs['image-cache-cleanup'].schedule,
job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => { job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => {
logger.info('Starting scheduled job: Image Cache Cleanup', { logger.info('Starting scheduled job: Image Cache Cleanup', {

@ -10,6 +10,7 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
import type { Collection } from '@server/models/Collection'; import type { Collection } from '@server/models/Collection';
@ -39,6 +40,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
const [requestModal, setRequestModal] = useState(false); const [requestModal, setRequestModal] = useState(false);
const [is4k, setIs4k] = 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 { const {
data, data,
error, error,
@ -46,21 +60,19 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
} = useSWR<Collection>(`/api/v1/collection/${router.query.collectionId}`, { } = useSWR<Collection>(`/api/v1/collection/${router.query.collectionId}`, {
fallbackData: collection, fallbackData: collection,
revalidateOnMount: true, revalidateOnMount: true,
refreshInterval: refreshIntervalHelper(
returnCollectionDownloadItems(collection),
15000
),
}); });
const { data: genres } = const { data: genres } =
useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`); useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
const [downloadStatus, downloadStatus4k] = useMemo(() => { const [downloadStatus, downloadStatus4k] = useMemo(() => {
return [ const downloadItems = returnCollectionDownloadItems(data);
data?.parts.flatMap((item) => return [downloadItems.downloadStatus, downloadItems.downloadStatus4k];
item.mediaInfo?.downloadStatus ? item.mediaInfo?.downloadStatus : [] }, [data]);
),
data?.parts.flatMap((item) =>
item.mediaInfo?.downloadStatus4k ? item.mediaInfo?.downloadStatus4k : []
),
];
}, [data?.parts]);
const [titles, titles4k] = useMemo(() => { const [titles, titles4k] = useMemo(() => {
return [ return [

@ -26,6 +26,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers'; import { sortCrewPriority } from '@app/utils/creditHelpers';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { import {
ArrowRightCircleIcon, ArrowRightCircleIcon,
CloudIcon, CloudIcon,
@ -110,6 +111,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
mutate: revalidate, mutate: revalidate,
} = useSWR<MovieDetailsType>(`/api/v1/movie/${router.query.movieId}`, { } = useSWR<MovieDetailsType>(`/api/v1/movie/${router.query.movieId}`, {
fallbackData: movie, fallbackData: movie,
refreshInterval: refreshIntervalHelper(
{
downloadStatus: movie?.mediaInfo?.downloadStatus,
downloadStatus4k: movie?.mediaInfo?.downloadStatus4k,
},
15000
),
}); });
const { data: ratingData } = useSWR<RTRating>( const { data: ratingData } = useSWR<RTRating>(

@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks'; import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { withProperties } from '@app/utils/typeHelpers'; import { withProperties } from '@app/utils/typeHelpers';
import { import {
ArrowPathIcon, ArrowPathIcon,
@ -220,6 +221,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
request.type === 'movie' request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}` ? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`; : `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>( const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? `${url}` : null inView ? `${url}` : null
); );
@ -229,6 +231,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
mutate: revalidate, mutate: revalidate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, { } = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
fallbackData: request, fallbackData: request,
refreshInterval: refreshIntervalHelper(
{
downloadStatus: request.media.downloadStatus,
downloadStatus4k: request.media.downloadStatus4k,
},
15000
),
}); });
const { plexUrl, plexUrl4k } = useDeepLinks({ const { plexUrl, plexUrl4k } = useDeepLinks({

@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks'; import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { import {
ArrowPathIcon, ArrowPathIcon,
CheckIcon, CheckIcon,
@ -293,6 +294,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
`/api/v1/request/${request.id}`, `/api/v1/request/${request.id}`,
{ {
fallbackData: request, fallbackData: request,
refreshInterval: refreshIntervalHelper(
{
downloadStatus: request.media.downloadStatus,
downloadStatus4k: request.media.downloadStatus4k,
},
15000
),
} }
); );

@ -67,6 +67,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}', 'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
editJobScheduleSelectorMinutes: editJobScheduleSelectorMinutes:
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}', 'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
editJobScheduleSelectorSeconds:
'Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}',
imagecache: 'Image Cache', imagecache: 'Image Cache',
imagecacheDescription: 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 <code>{appDataPath}/cache/images</code>.', '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 <code>{appDataPath}/cache/images</code>.',
@ -78,7 +80,7 @@ interface Job {
id: JobId; id: JobId;
name: string; name: string;
type: 'process' | 'command'; type: 'process' | 'command';
interval: 'short' | 'long' | 'fixed'; interval: 'seconds' | 'minutes' | 'hours' | 'fixed';
cronSchedule: string; cronSchedule: string;
nextExecutionTime: string; nextExecutionTime: string;
running: boolean; running: boolean;
@ -89,10 +91,11 @@ type JobModalState = {
job?: Job; job?: Job;
scheduleHours: number; scheduleHours: number;
scheduleMinutes: number; scheduleMinutes: number;
scheduleSeconds: number;
}; };
type JobModalAction = type JobModalAction =
| { type: 'set'; hours?: number; minutes?: number } | { type: 'set'; hours?: number; minutes?: number; seconds?: number }
| { | {
type: 'close'; type: 'close';
} }
@ -115,6 +118,7 @@ const jobModalReducer = (
job: action.job, job: action.job,
scheduleHours: 1, scheduleHours: 1,
scheduleMinutes: 5, scheduleMinutes: 5,
scheduleSeconds: 30,
}; };
case 'set': case 'set':
@ -122,6 +126,7 @@ const jobModalReducer = (
...state, ...state,
scheduleHours: action.hours ?? state.scheduleHours, scheduleHours: action.hours ?? state.scheduleHours,
scheduleMinutes: action.minutes ?? state.scheduleMinutes, scheduleMinutes: action.minutes ?? state.scheduleMinutes,
scheduleSeconds: action.seconds ?? state.scheduleSeconds,
}; };
} }
}; };
@ -149,6 +154,7 @@ const SettingsJobs = () => {
isOpen: false, isOpen: false,
scheduleHours: 1, scheduleHours: 1,
scheduleMinutes: 5, scheduleMinutes: 5,
scheduleSeconds: 30,
}); });
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@ -200,9 +206,11 @@ const SettingsJobs = () => {
const jobScheduleCron = ['0', '0', '*', '*', '*', '*']; const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
try { 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}`; jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`;
} else if (jobModalState.job?.interval === 'long') { } else if (jobModalState.job?.interval === 'hours') {
jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`; jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`;
} else { } else {
// jobs with interval: fixed should not be editable // jobs with interval: fixed should not be editable
@ -286,7 +294,30 @@ const SettingsJobs = () => {
{intl.formatMessage(messages.editJobSchedulePrompt)} {intl.formatMessage(messages.editJobSchedulePrompt)}
</label> </label>
<div className="form-input-area"> <div className="form-input-area">
{jobModalState.job?.interval === 'short' ? ( {jobModalState.job?.interval === 'seconds' ? (
<select
name="jobScheduleSeconds"
className="inline"
value={jobModalState.scheduleSeconds}
onChange={(e) =>
dispatch({
type: 'set',
seconds: Number(e.target.value),
})
}
>
{[30, 45, 60].map((v) => (
<option value={v} key={`jobScheduleSeconds-${v}`}>
{intl.formatMessage(
messages.editJobScheduleSelectorSeconds,
{
jobScheduleSeconds: v,
}
)}
</option>
))}
</select>
) : jobModalState.job?.interval === 'minutes' ? (
<select <select
name="jobScheduleMinutes" name="jobScheduleMinutes"
className="inline" className="inline"

@ -30,6 +30,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers'; import { sortCrewPriority } from '@app/utils/creditHelpers';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { Disclosure, Transition } from '@headlessui/react'; import { Disclosure, Transition } from '@headlessui/react';
import { import {
ArrowRightCircleIcon, ArrowRightCircleIcon,
@ -109,6 +110,13 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
mutate: revalidate, mutate: revalidate,
} = useSWR<TvDetailsType>(`/api/v1/tv/${router.query.tvId}`, { } = useSWR<TvDetailsType>(`/api/v1/tv/${router.query.tvId}`, {
fallbackData: tv, fallbackData: tv,
refreshInterval: refreshIntervalHelper(
{
downloadStatus: tv?.mediaInfo?.downloadStatus,
downloadStatus4k: tv?.mediaInfo?.downloadStatus4k,
},
15000
),
}); });
const { data: ratingData } = useSWR<RTRating>( const { data: ratingData } = useSWR<RTRating>(

@ -739,6 +739,7 @@
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "New Frequency", "components.Settings.SettingsJobsCache.editJobSchedulePrompt": "New Frequency",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}", "components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}", "components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache", "components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup", "components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
"components.Settings.SettingsJobsCache.imagecache": "Image Cache", "components.Settings.SettingsJobsCache.imagecache": "Image Cache",

@ -0,0 +1,18 @@
import type { DownloadingItem } from '@server/lib/downloadtracker';
export const refreshIntervalHelper = (
downloadItem: {
downloadStatus: DownloadingItem[] | undefined;
downloadStatus4k: DownloadingItem[] | undefined;
},
timer: number
) => {
if (
(downloadItem.downloadStatus ?? []).length > 0 ||
(downloadItem.downloadStatus4k ?? []).length > 0
) {
return timer;
} else {
return 0;
}
};
Loading…
Cancel
Save