mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-02 12:48:45 -05:00
feat: Radarr & Sonarr Sync (#734)
This commit is contained in:
@@ -35,6 +35,9 @@ const messages = defineMessages({
|
||||
apiKeyPlaceholder: 'Your Radarr API key',
|
||||
baseUrl: 'Base URL',
|
||||
baseUrlPlaceholder: 'Example: /radarr',
|
||||
syncEnabled: 'Enable Sync',
|
||||
externalUrl: 'External URL',
|
||||
externalUrlPlaceholder: 'External URL pointing to your Radarr server',
|
||||
qualityprofile: 'Quality Profile',
|
||||
rootfolder: 'Root Folder',
|
||||
minimumAvailability: 'Minimum Availability',
|
||||
@@ -46,6 +49,7 @@ const messages = defineMessages({
|
||||
testFirstQualityProfiles: 'Test connection to load quality profiles',
|
||||
loadingrootfolders: 'Loading root folders…',
|
||||
testFirstRootFolders: 'Test connection to load root folders',
|
||||
preventSearch: 'Disable Auto-Search',
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
@@ -188,6 +192,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
minimumAvailability: radarr?.minimumAvailability ?? 'released',
|
||||
isDefault: radarr?.isDefault ?? false,
|
||||
is4k: radarr?.is4k ?? false,
|
||||
externalUrl: radarr?.externalUrl,
|
||||
syncEnabled: radarr?.syncEnabled,
|
||||
preventSearch: radarr?.preventSearch,
|
||||
}}
|
||||
validationSchema={RadarrSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
@@ -209,6 +216,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
is4k: values.is4k,
|
||||
minimumAvailability: values.minimumAvailability,
|
||||
isDefault: values.isDefault,
|
||||
externalUrl: values.externalUrl,
|
||||
syncEnabled: values.syncEnabled,
|
||||
preventSearch: values.preventSearch,
|
||||
};
|
||||
if (!radarr) {
|
||||
await axios.post('/api/v1/settings/radarr', submission);
|
||||
@@ -290,6 +300,22 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="is4k"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.server4k)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="is4k"
|
||||
name="is4k"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||
<label
|
||||
htmlFor="name"
|
||||
@@ -567,18 +593,60 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||
<label
|
||||
htmlFor="is4k"
|
||||
htmlFor="externalUrl"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.server4k)}
|
||||
{intl.formatMessage(messages.externalUrl)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="flex max-w-lg rounded-md shadow-sm">
|
||||
<Field
|
||||
id="externalUrl"
|
||||
name="externalUrl"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.externalUrlPlaceholder
|
||||
)}
|
||||
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
|
||||
/>
|
||||
</div>
|
||||
{errors.externalUrl && touched.externalUrl && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{errors.externalUrl}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="syncEnabled"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.syncEnabled)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="is4k"
|
||||
name="is4k"
|
||||
id="syncEnabled"
|
||||
name="syncEnabled"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="preventSearch"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.preventSearch)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="preventSearch"
|
||||
name="preventSearch"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,35 +4,87 @@ import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
|
||||
import Button from '../Common/Button';
|
||||
import Table from '../Common/Table';
|
||||
import Spinner from '../../assets/spinner.svg';
|
||||
import axios from 'axios';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import Badge from '../Common/Badge';
|
||||
|
||||
const messages = defineMessages({
|
||||
jobname: 'Job Name',
|
||||
jobtype: 'Type',
|
||||
nextexecution: 'Next Execution',
|
||||
runnow: 'Run Now',
|
||||
canceljob: 'Cancel Job',
|
||||
jobstarted: '{jobname} started.',
|
||||
jobcancelled: '{jobname} cancelled.',
|
||||
});
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
nextExecutionTime: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
const SettingsJobs: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useSWR<{ name: string; nextExecutionTime: string }[]>(
|
||||
'/api/v1/settings/jobs'
|
||||
);
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR<Job[]>('/api/v1/settings/jobs', {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const runJob = async (job: Job) => {
|
||||
await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
|
||||
addToast(
|
||||
intl.formatMessage(messages.jobstarted, {
|
||||
jobname: job.name,
|
||||
}),
|
||||
{
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
}
|
||||
);
|
||||
revalidate();
|
||||
};
|
||||
|
||||
const cancelJob = async (job: Job) => {
|
||||
await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
|
||||
addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
revalidate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data?.map((job, index) => (
|
||||
<tr key={`job-list-${index}`}>
|
||||
{data?.map((job) => (
|
||||
<tr key={`job-list-${job.id}`}>
|
||||
<Table.TD>
|
||||
<div className="text-sm leading-5 text-white">{job.name}</div>
|
||||
<div className="flex items-center text-sm leading-5 text-white">
|
||||
{job.running && <Spinner className="w-5 h-5 mr-2" />}
|
||||
<span>{job.name}</span>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<Badge
|
||||
badgeType={job.type === 'process' ? 'primary' : 'warning'}
|
||||
className="uppercase"
|
||||
>
|
||||
{job.type}
|
||||
</Badge>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="text-sm leading-5 text-white">
|
||||
@@ -46,9 +98,15 @@ const SettingsJobs: React.FC = () => {
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
<Button buttonType="primary" disabled>
|
||||
{intl.formatMessage(messages.runnow)}
|
||||
</Button>
|
||||
{job.running ? (
|
||||
<Button buttonType="danger" onClick={() => cancelJob(job)}>
|
||||
{intl.formatMessage(messages.canceljob)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button buttonType="primary" onClick={() => runJob(job)}>
|
||||
{intl.formatMessage(messages.runnow)}
|
||||
</Button>
|
||||
)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -46,6 +46,10 @@ const messages = defineMessages({
|
||||
testFirstQualityProfiles: 'Test connection to load quality profiles',
|
||||
loadingrootfolders: 'Loading root folders…',
|
||||
testFirstRootFolders: 'Test connection to load root folders',
|
||||
syncEnabled: 'Enable Sync',
|
||||
externalUrl: 'External URL',
|
||||
externalUrlPlaceholder: 'External URL pointing to your Sonarr server',
|
||||
preventSearch: 'Disable Auto-Search',
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
@@ -189,6 +193,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
isDefault: sonarr?.isDefault ?? false,
|
||||
is4k: sonarr?.is4k ?? false,
|
||||
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
||||
externalUrl: sonarr?.externalUrl,
|
||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||
preventSearch: sonarr?.preventSearch ?? false,
|
||||
}}
|
||||
validationSchema={SonarrSettingsSchema}
|
||||
onSubmit={async (values) => {
|
||||
@@ -218,6 +225,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
is4k: values.is4k,
|
||||
isDefault: values.isDefault,
|
||||
enableSeasonFolders: values.enableSeasonFolders,
|
||||
externalUrl: values.externalUrl,
|
||||
syncEnabled: values.syncEnabled,
|
||||
preventSearch: values.preventSearch,
|
||||
};
|
||||
if (!sonarr) {
|
||||
await axios.post('/api/v1/settings/sonarr', submission);
|
||||
@@ -299,6 +309,22 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="is4k"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.server4k)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="is4k"
|
||||
name="is4k"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||
<label
|
||||
htmlFor="name"
|
||||
@@ -632,22 +658,6 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="is4k"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.server4k)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="is4k"
|
||||
name="is4k"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="enableSeasonFolders"
|
||||
@@ -664,6 +674,64 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
|
||||
<label
|
||||
htmlFor="externalUrl"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.externalUrl)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div className="flex max-w-lg rounded-md shadow-sm">
|
||||
<Field
|
||||
id="externalUrl"
|
||||
name="externalUrl"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.externalUrlPlaceholder
|
||||
)}
|
||||
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
|
||||
/>
|
||||
</div>
|
||||
{errors.externalUrl && touched.externalUrl && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{errors.externalUrl}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="syncEnabled"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.syncEnabled)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="syncEnabled"
|
||||
name="syncEnabled"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
|
||||
<label
|
||||
htmlFor="preventSearch"
|
||||
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
|
||||
>
|
||||
{intl.formatMessage(messages.preventSearch)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="preventSearch"
|
||||
name="preventSearch"
|
||||
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user