mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-23 18:29:19 -05:00
* fix(blacklist): handle invalid keywords gracefully * fix(blacklist): only remove keywords on 404 errors * fix(blacklist): remove non-null assertion and add proper type annotation * refactor(blacklist): return null instead of 404 for missing keywords * fix(blacklist): add type annotation for validKeywords * fix(selector): update type annotation for validKeywords
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
import type { DVRTestResponse } from '@app/components/Settings/SettingsServices';
|
|
import globalMessages from '@app/i18n/globalMessages';
|
|
import defineMessages from '@app/utils/defineMessages';
|
|
import { PencilIcon, TrashIcon } from '@heroicons/react/24/solid';
|
|
import type { TmdbGenre } from '@server/api/themoviedb/interfaces';
|
|
import type OverrideRule from '@server/entity/OverrideRule';
|
|
import type { User } from '@server/entity/User';
|
|
import type {
|
|
DVRSettings,
|
|
Language,
|
|
RadarrSettings,
|
|
SonarrSettings,
|
|
} from '@server/lib/settings';
|
|
import type { Keyword } from '@server/models/common';
|
|
import axios from 'axios';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { useIntl } from 'react-intl';
|
|
import useSWR from 'swr';
|
|
|
|
const messages = defineMessages('components.Settings.OverrideRuleTile', {
|
|
qualityprofile: 'Quality Profile',
|
|
rootfolder: 'Root Folder',
|
|
tags: 'Tags',
|
|
users: 'Users',
|
|
genre: 'Genre',
|
|
language: 'Language',
|
|
keywords: 'Keywords',
|
|
conditions: 'Conditions',
|
|
settings: 'Settings',
|
|
});
|
|
|
|
interface OverrideRuleTilesProps {
|
|
rules: OverrideRule[];
|
|
setOverrideRuleModal: ({
|
|
open,
|
|
rule,
|
|
}: {
|
|
open: boolean;
|
|
rule: OverrideRule | null;
|
|
}) => void;
|
|
revalidate: () => void;
|
|
radarrServices: RadarrSettings[];
|
|
sonarrServices: SonarrSettings[];
|
|
}
|
|
|
|
const OverrideRuleTiles = ({
|
|
rules,
|
|
setOverrideRuleModal,
|
|
revalidate,
|
|
radarrServices,
|
|
sonarrServices,
|
|
}: OverrideRuleTilesProps) => {
|
|
const intl = useIntl();
|
|
const [users, setUsers] = useState<User[] | null>(null);
|
|
const [keywords, setKeywords] = useState<Keyword[] | null>(null);
|
|
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
|
|
const { data: genres } = useSWR<TmdbGenre[]>('/api/v1/genres/movie');
|
|
const [testResponses, setTestResponses] = useState<
|
|
(DVRTestResponse & { type: string; id: number })[]
|
|
>([]);
|
|
|
|
const getServiceInfos = useCallback(async () => {
|
|
const results: (DVRTestResponse & { type: string; id: number })[] = [];
|
|
const services: DVRSettings[] = [...radarrServices, ...sonarrServices];
|
|
for (const service of services) {
|
|
const { hostname, port, apiKey, baseUrl, useSsl = false } = service;
|
|
try {
|
|
const response = await axios.post<DVRTestResponse>(
|
|
`/api/v1/settings/${
|
|
radarrServices.includes(service as RadarrSettings)
|
|
? 'radarr'
|
|
: 'sonarr'
|
|
}/test`,
|
|
{
|
|
hostname,
|
|
apiKey,
|
|
port: Number(port),
|
|
baseUrl,
|
|
useSsl,
|
|
}
|
|
);
|
|
results.push({
|
|
type: radarrServices.includes(service as RadarrSettings)
|
|
? 'radarr'
|
|
: 'sonarr',
|
|
id: service.id,
|
|
...response.data,
|
|
});
|
|
} catch {
|
|
results.push({
|
|
type: radarrServices.includes(service as RadarrSettings)
|
|
? 'radarr'
|
|
: 'sonarr',
|
|
id: service.id,
|
|
profiles: [],
|
|
rootFolders: [],
|
|
tags: [],
|
|
});
|
|
}
|
|
}
|
|
setTestResponses(results);
|
|
}, [radarrServices, sonarrServices]);
|
|
|
|
useEffect(() => {
|
|
getServiceInfos();
|
|
}, [getServiceInfos]);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
const keywords = await Promise.all(
|
|
rules
|
|
.map((rule) => rule.keywords?.split(','))
|
|
.flat()
|
|
.filter((keywordId) => keywordId)
|
|
.map(async (keywordId) => {
|
|
const response = await axios.get<Keyword | null>(
|
|
`/api/v1/keyword/${keywordId}`
|
|
);
|
|
return response.data;
|
|
})
|
|
);
|
|
const validKeywords: Keyword[] = keywords.filter(
|
|
(keyword): keyword is Keyword => keyword !== null
|
|
);
|
|
setKeywords(validKeywords);
|
|
const allUsersFromRules = rules
|
|
.map((rule) => rule.users)
|
|
.filter((users) => users)
|
|
.join(',');
|
|
if (allUsersFromRules) {
|
|
const response = await axios.get(
|
|
`/api/v1/user?includeIds=${encodeURIComponent(allUsersFromRules)}`
|
|
);
|
|
const users: User[] = response.data.results;
|
|
setUsers(users);
|
|
}
|
|
setUsers(users);
|
|
})();
|
|
}, [rules, users]);
|
|
|
|
return (
|
|
<>
|
|
{rules.map((rule) => (
|
|
<li className="flex h-full flex-col rounded-lg bg-gray-800 text-left shadow ring-1 ring-gray-500">
|
|
<div className="flex w-full flex-1 items-center justify-between space-x-6 p-6">
|
|
<div className="flex-1 truncate">
|
|
<span className="text-lg">
|
|
{intl.formatMessage(messages.conditions)}
|
|
</span>
|
|
{rule.users && (
|
|
<p className="truncate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.users)}
|
|
</span>
|
|
<div className="inline-flex gap-2">
|
|
{rule.users.split(',').map((userId) => {
|
|
return (
|
|
<span>
|
|
{
|
|
users?.find((user) => user.id === Number(userId))
|
|
?.displayName
|
|
}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</p>
|
|
)}
|
|
{rule.genre && (
|
|
<p className="truncate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.genre)}
|
|
</span>
|
|
<div className="inline-flex gap-2">
|
|
{rule.genre.split(',').map((genreId) => (
|
|
<span>
|
|
{genres?.find((g) => g.id === Number(genreId))?.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</p>
|
|
)}
|
|
{rule.language && (
|
|
<p className="truncate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.language)}
|
|
</span>
|
|
<div className="inline-flex gap-2">
|
|
{rule.language
|
|
.split('|')
|
|
.filter((languageId) => languageId !== 'server')
|
|
.map((languageId) => {
|
|
const language = languages?.find(
|
|
(language) => language.iso_639_1 === languageId
|
|
);
|
|
if (!language) return null;
|
|
const languageName =
|
|
intl.formatDisplayName(language.iso_639_1, {
|
|
type: 'language',
|
|
fallback: 'none',
|
|
}) ?? language.english_name;
|
|
return <span>{languageName}</span>;
|
|
})}
|
|
</div>
|
|
</p>
|
|
)}
|
|
{rule.keywords && (
|
|
<p className="truncate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.keywords)}
|
|
</span>
|
|
<div className="inline-flex gap-2">
|
|
{rule.keywords.split(',').map((keywordId) => {
|
|
return (
|
|
<span>
|
|
{
|
|
keywords?.find(
|
|
(keyword) => keyword.id === Number(keywordId)
|
|
)?.name
|
|
}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</p>
|
|
)}
|
|
<span className="text-lg">
|
|
{intl.formatMessage(messages.settings)}
|
|
</span>
|
|
{rule.profileId && (
|
|
<p className="runcate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.qualityprofile)}
|
|
</span>
|
|
{testResponses
|
|
.find(
|
|
(r) =>
|
|
(r.id === rule.radarrServiceId &&
|
|
r.type === 'radarr') ||
|
|
(r.id === rule.sonarrServiceId && r.type === 'sonarr')
|
|
)
|
|
?.profiles.find((profile) => rule.profileId === profile.id)
|
|
?.name || rule.profileId}
|
|
</p>
|
|
)}
|
|
{rule.rootFolder && (
|
|
<p className="truncate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.rootfolder)}
|
|
</span>
|
|
{rule.rootFolder}
|
|
</p>
|
|
)}
|
|
{rule.tags && rule.tags.length > 0 && (
|
|
<p className="truncate text-sm leading-5 text-gray-300">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.tags)}
|
|
</span>
|
|
<div className="inline-flex gap-2">
|
|
{rule.tags.split(',').map((tag) => (
|
|
<span>
|
|
{testResponses
|
|
.find(
|
|
(r) =>
|
|
(r.id === rule.radarrServiceId &&
|
|
r.type === 'radarr') ||
|
|
(r.id === rule.sonarrServiceId &&
|
|
r.type === 'sonarr')
|
|
)
|
|
?.tags?.find((t) => t.id === Number(tag))?.label ||
|
|
tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="border-t border-gray-500">
|
|
<div className="-mt-px flex">
|
|
<div className="flex w-0 flex-1 border-r border-gray-500">
|
|
<button
|
|
onClick={() => setOverrideRuleModal({ open: true, rule })}
|
|
className="focus:ring-blue relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
|
>
|
|
<PencilIcon className="mr-2 h-5 w-5" />
|
|
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
|
</button>
|
|
</div>
|
|
<div className="-ml-px flex w-0 flex-1">
|
|
<button
|
|
onClick={async () => {
|
|
await axios.delete(`/api/v1/overrideRule/${rule.id}`);
|
|
revalidate();
|
|
}}
|
|
className="focus:ring-blue relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out hover:text-white focus:z-10 focus:border-gray-500 focus:outline-none"
|
|
>
|
|
<TrashIcon className="mr-2 h-5 w-5" />
|
|
<span>{intl.formatMessage(globalMessages.delete)}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default OverrideRuleTiles;
|