🏗️ Migrate overseerr media requests to tRPC

This commit is contained in:
Meier Lukas
2023-06-10 16:00:52 +02:00
parent 0a53602701
commit c1c5197d0e
2 changed files with 116 additions and 39 deletions

View File

@@ -1,17 +1,16 @@
import { Alert, Button, Checkbox, createStyles, Group, Modal, Stack, Table } from '@mantine/core'; import { Alert, Button, Checkbox, createStyles, Group, Modal, Stack, Table } from '@mantine/core';
import { showNotification, updateNotification } from '@mantine/notifications'; import { showNotification, updateNotification } from '@mantine/notifications';
import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons-react'; import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons-react';
import axios from 'axios';
import Consola from 'consola'; import Consola from 'consola';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { useColorTheme } from '../../tools/color'; import { useColorTheme } from '../../tools/color';
import { MovieResult } from './Movie.d'; import { MovieResult } from './Movie.d';
import { MediaType, Result } from './SearchResult.d'; import { Result } from './SearchResult.d';
import { TvShowResult, TvShowResultSeason } from './TvShow.d'; import { TvShowResult, TvShowResultSeason } from './TvShow.d';
import { api } from '~/utils/api';
import { useConfigContext } from '~/config/provider';
interface RequestModalProps { interface RequestModalProps {
base: Result; base: Result;
@@ -62,6 +61,7 @@ export function MovieRequestModal({
setOpened: (opened: boolean) => void; setOpened: (opened: boolean) => void;
}) { }) {
const { secondaryColor } = useColorTheme(); const { secondaryColor } = useColorTheme();
const requestMediaAsync = useMediaRequestMutation();
const { t } = useTranslation('modules/overseerr'); const { t } = useTranslation('modules/overseerr');
return ( return (
@@ -97,7 +97,7 @@ export function MovieRequestModal({
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
askForMedia('movie', result.id, result.title, []); requestMediaAsync('movie', result.id, result.title);
}} }}
> >
{t('popup.item.buttons.request')} {t('popup.item.buttons.request')}
@@ -120,6 +120,7 @@ export function TvRequestModal({
const [selection, setSelection] = useState<TvShowResultSeason[]>(result.seasons); const [selection, setSelection] = useState<TvShowResultSeason[]>(result.seasons);
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const { t } = useTranslation('modules/overseerr'); const { t } = useTranslation('modules/overseerr');
const requestMediaAsync = useMediaRequestMutation();
const toggleRow = (container: TvShowResultSeason) => const toggleRow = (container: TvShowResultSeason) =>
setSelection((current: TvShowResultSeason[]) => setSelection((current: TvShowResultSeason[]) =>
@@ -200,7 +201,7 @@ export function TvRequestModal({
variant="outline" variant="outline"
disabled={selection.length === 0} disabled={selection.length === 0}
onClick={() => { onClick={() => {
askForMedia( requestMediaAsync(
'tv', 'tv',
result.id, result.id,
result.name, result.name,
@@ -216,37 +217,51 @@ export function TvRequestModal({
); );
} }
function askForMedia(type: MediaType, id: number, name: string, seasons?: number[]) { const useMediaRequestMutation = () => {
Consola.info(`Requesting ${type} ${id} ${name}`); const { name: configName } = useConfigContext();
showNotification({ const { mutateAsync } = api.overseerr.request.useMutation();
title: 'Request',
id: id.toString(), return async (type: 'tv' | 'movie', id: number, name: string, seasons?: number[]) => {
message: `Requesting media ${name}`, Consola.info(`Requesting ${type} ${id} ${name}`);
color: 'orange', showNotification({
loading: true, title: 'Request',
autoClose: false, id: id.toString(),
withCloseButton: false, message: `Requesting media ${name}`,
icon: <IconAlertCircle />, color: 'orange',
}); loading: true,
axios autoClose: false,
.post(`/api/modules/overseerr/${id}`, { type, seasons }) withCloseButton: false,
.then(() => { icon: <IconAlertCircle />,
updateNotification({
id: id.toString(),
title: '',
color: 'green',
message: ` ${name} requested`,
icon: <IconCheck />,
autoClose: 2000,
});
})
.catch((err) => {
updateNotification({
id: id.toString(),
color: 'red',
title: 'There was an error',
message: err.message,
autoClose: 2000,
});
}); });
}
await mutateAsync(
{
configName: configName!,
id,
type,
seasons: seasons ?? [],
},
{
onSuccess: () => {
updateNotification({
id: id.toString(),
title: '',
color: 'green',
message: ` ${name} requested`,
icon: <IconCheck />,
autoClose: 2000,
});
},
onError: (err) => {
updateNotification({
id: id.toString(),
color: 'red',
title: 'There was an error',
message: err.message,
autoClose: 2000,
});
},
}
);
};
};

View File

@@ -104,4 +104,66 @@ export const overseerrRouter = createTRPCRouter({
}); });
return tv; return tv;
}), }),
request: publicProcedure
.input(
z
.object({
configName: z.string(),
id: z.number(),
})
.and(
z
.object({
seasons: z.array(z.number()),
type: z.literal('tv'),
})
.or(
z.object({
type: z.literal('movie'),
})
)
)
)
.mutation(async ({ input }) => {
const config = getConfig(input.configName);
const app = config.apps.find(
(app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
);
const apiKey = app?.integration?.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'No app found',
});
}
const appUrl = new URL(app.url);
Consola.info('Got an Overseerr request with these arguments', {
mediaType: input.type,
mediaId: input.id,
seasons: input.type === 'tv' ? input.seasons : undefined,
});
return axios
.post(
`${appUrl.origin}/api/v1/request`,
{
mediaType: input.type,
mediaId: input.id,
seasons: input.type === 'tv' ? input.seasons : undefined,
},
{
headers: {
// Set X-Api-Key to the value of the API key
'X-Api-Key': apiKey,
},
}
)
.then((res) => res.data)
.catch((err) => {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: err.message,
});
});
}),
}); });