Lidarr and Readarr integrations

This commit is contained in:
ajnart
2022-05-25 10:50:57 +02:00
parent 4f68f7e395
commit 2cb6781a94
4 changed files with 218 additions and 24 deletions

View File

@@ -173,7 +173,10 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
{...form.getInputProps('type')} {...form.getInputProps('type')}
/> />
<LoadingOverlay visible={isLoading} /> <LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' || form.values.type === 'Radarr') && ( {(form.values.type === 'Sonarr' ||
form.values.type === 'Radarr' ||
form.values.type === 'Lidarr' ||
form.values.type === 'Readarr') && (
<TextInput <TextInput
required required
label="API key" label="API key"

View File

@@ -1,12 +1,17 @@
/* eslint-disable react/no-children-prop */ /* eslint-disable react/no-children-prop */
import { Popover, Box, ScrollArea, Divider, Indicator, useMantineTheme } from '@mantine/core'; import { Box, Divider, Indicator, Popover, ScrollArea, useMantineTheme } from '@mantine/core';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Calendar } from '@mantine/dates'; import { Calendar } from '@mantine/dates';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { Calendar as CalendarIcon, Check } from 'tabler-icons-react'; import { Calendar as CalendarIcon, Check } from 'tabler-icons-react';
import { RadarrMediaDisplay, SonarrMediaDisplay } from './MediaDisplay';
import { useConfig } from '../../../tools/state'; import { useConfig } from '../../../tools/state';
import { IModule } from '../modules'; import { IModule } from '../modules';
import {
SonarrMediaDisplay,
RadarrMediaDisplay,
LidarrMediaDisplay,
ReadarrMediaDisplay,
} from './MediaDisplay';
export const CalendarModule: IModule = { export const CalendarModule: IModule = {
title: 'Calendar', title: 'Calendar',
@@ -19,17 +24,25 @@ export const CalendarModule: IModule = {
export default function CalendarComponent(props: any) { export default function CalendarComponent(props: any) {
const { config } = useConfig(); const { config } = useConfig();
const [sonarrMedias, setSonarrMedias] = useState([] as any); const [sonarrMedias, setSonarrMedias] = useState([] as any);
const [lidarrMedias, setLidarrMedias] = useState([] as any);
const [radarrMedias, setRadarrMedias] = useState([] as any); const [radarrMedias, setRadarrMedias] = useState([] as any);
const [readarrMedias, setReadarrMedias] = useState([] as any);
useEffect(() => { useEffect(() => {
// Filter only sonarr and radarr services // Filter only sonarr and radarr services
const filtered = config.services.filter( const filtered = config.services.filter(
(service) => service.type === 'Sonarr' || service.type === 'Radarr' (service) =>
service.type === 'Sonarr' ||
service.type === 'Radarr' ||
service.type === 'Lidarr' ||
service.type === 'Readarr'
); );
// Get the url and apiKey for all Sonarr and Radarr services // Get the url and apiKey for all Sonarr and Radarr services
const sonarrService = filtered.filter((service) => service.type === 'Sonarr').at(0); const sonarrService = filtered.filter((service) => service.type === 'Sonarr').at(0);
const radarrService = filtered.filter((service) => service.type === 'Radarr').at(0); const radarrService = filtered.filter((service) => service.type === 'Radarr').at(0);
const lidarrService = filtered.filter((service) => service.type === 'Lidarr').at(0);
const readarrService = filtered.filter((service) => service.type === 'Readarr').at(0);
const nextMonth = new Date(new Date().setMonth(new Date().getMonth() + 2)).toISOString(); const nextMonth = new Date(new Date().setMonth(new Date().getMonth() + 2)).toISOString();
if (sonarrService && sonarrService.apiKey) { if (sonarrService && sonarrService.apiKey) {
const baseUrl = new URL(sonarrService.url).origin; const baseUrl = new URL(sonarrService.url).origin;
@@ -69,6 +82,44 @@ export default function CalendarComponent(props: any) {
} }
); );
} }
if (lidarrService && lidarrService.apiKey) {
const baseUrl = new URL(lidarrService.url).origin;
fetch(`${baseUrl}/api/v1/calendar?apikey=${lidarrService?.apiKey}&end=${nextMonth}`).then(
(response) => {
response.ok &&
response.json().then((data) => {
setLidarrMedias(data);
showNotification({
title: 'Lidarr',
icon: <Check />,
color: 'green',
autoClose: 1500,
radius: 'md',
message: `Loaded ${data.length} releases`,
});
});
}
);
}
if (readarrService && readarrService.apiKey) {
const baseUrl = new URL(readarrService.url).origin;
fetch(`${baseUrl}/api/v1/calendar?apikey=${readarrService?.apiKey}&end=${nextMonth}`).then(
(response) => {
response.ok &&
response.json().then((data) => {
setReadarrMedias(data);
showNotification({
title: 'Readarr',
icon: <Check />,
color: 'green',
autoClose: 1500,
radius: 'md',
message: `Loaded ${data.length} releases`,
});
});
}
);
}
}, [config.services]); }, [config.services]);
if (sonarrMedias === undefined && radarrMedias === undefined) { if (sonarrMedias === undefined && radarrMedias === undefined) {
@@ -82,6 +133,8 @@ export default function CalendarComponent(props: any) {
renderdate={renderdate} renderdate={renderdate}
sonarrmedias={sonarrMedias} sonarrmedias={sonarrMedias}
radarrmedias={radarrMedias} radarrmedias={radarrMedias}
lidarrmedias={lidarrMedias}
readarrmedias={readarrMedias}
/> />
)} )}
/> />
@@ -93,12 +146,25 @@ function DayComponent(props: any) {
renderdate, renderdate,
sonarrmedias, sonarrmedias,
radarrmedias, radarrmedias,
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props; lidarrmedias,
readarrmedias,
}: { renderdate: Date; sonarrmedias: []; radarrmedias: []; lidarrmedias: []; readarrmedias: [] } =
props;
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const theme = useMantineTheme(); const theme = useMantineTheme();
const day = renderdate.getDate(); const day = renderdate.getDate();
// Itterate over the medias and filter the ones that are on the same day
const readarrFiltered = readarrmedias.filter((media: any) => {
const mediaDate = new Date(media.releaseDate);
return mediaDate.getDate() === day;
});
const lidarrFiltered = lidarrmedias.filter((media: any) => {
const date = new Date(media.releaseDate);
// Return true if the date is renerdate without counting hours and minutes
return date.getDate() === day && date.getMonth() === renderdate.getMonth();
});
const sonarrFiltered = sonarrmedias.filter((media: any) => { const sonarrFiltered = sonarrmedias.filter((media: any) => {
const date = new Date(media.airDate); const date = new Date(media.airDate);
// Return true if the date is renerdate without counting hours and minutes // Return true if the date is renerdate without counting hours and minutes
@@ -109,7 +175,12 @@ function DayComponent(props: any) {
// Return true if the date is renerdate without counting hours and minutes // Return true if the date is renerdate without counting hours and minutes
return date.getDate() === day && date.getMonth() === renderdate.getMonth(); return date.getDate() === day && date.getMonth() === renderdate.getMonth();
}); });
if (sonarrFiltered.length === 0 && radarrFiltered.length === 0) { if (
sonarrFiltered.length === 0 &&
radarrFiltered.length === 0 &&
lidarrFiltered.length === 0 &&
readarrFiltered.length === 0
) {
return <div>{day}</div>; return <div>{day}</div>;
} }
@@ -119,8 +190,58 @@ function DayComponent(props: any) {
setOpened(true); setOpened(true);
}} }}
> >
{radarrFiltered.length > 0 && <Indicator size={7} color="yellow" children={null} />} {readarrFiltered.length > 0 && (
{sonarrFiltered.length > 0 && <Indicator size={7} offset={8} color="blue" children={null} />} <Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
left: 8,
}}
color="red"
children={null}
/>
)}
{radarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
left: 8,
}}
color="yellow"
children={null}
/>
)}
{sonarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
right: 8,
}}
color="blue"
children={null}
/>
)}
{lidarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
right: 8,
}}
color="green"
children={undefined}
/>
)}
<Popover <Popover
position="left" position="left"
radius="lg" radius="lg"
@@ -147,6 +268,18 @@ function DayComponent(props: any) {
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />} {index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment> </React.Fragment>
))} ))}
{lidarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<LidarrMediaDisplay media={media} />
{index < lidarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{readarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<ReadarrMediaDisplay media={media} />
{index < readarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
</ScrollArea> </ScrollArea>
</Popover> </Popover>
</Box> </Box>

View File

@@ -3,9 +3,10 @@ import { Link } from 'tabler-icons-react';
export interface IMedia { export interface IMedia {
overview: string; overview: string;
imdbId: any; imdbId?: any;
artist?: string;
title: string; title: string;
poster: string; poster?: string;
genres: string[]; genres: string[];
seasonNumber?: number; seasonNumber?: number;
episodeNumber?: number; episodeNumber?: number;
@@ -15,14 +16,17 @@ function MediaDisplay(props: { media: IMedia }) {
const { media }: { media: IMedia } = props; const { media }: { media: IMedia } = props;
return ( return (
<Group noWrap align="self-start" mr={15}> <Group noWrap align="self-start" mr={15}>
<Image {media.poster && (
radius="md" <Image
fit="cover" radius="md"
src={media.poster} fit="cover"
alt={media.title} src={media.poster}
width={300} alt={media.title}
height={400} width={300}
/> height={400}
/>
)}
<Stack <Stack
justify="space-between" justify="space-between"
sx={(theme) => ({ sx={(theme) => ({
@@ -32,12 +36,28 @@ function MediaDisplay(props: { media: IMedia }) {
<Group direction="column"> <Group direction="column">
<Group noWrap> <Group noWrap>
<Title order={3}>{media.title}</Title> <Title order={3}>{media.title}</Title>
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank"> {media.imdbId && (
<ActionIcon> <Anchor
<Link /> href={`https://www.imdb.com/title/${media.imdbId}`}
</ActionIcon> target="_blank"
</Anchor> rel="noopener noreferrer"
>
<ActionIcon>
<Link />
</ActionIcon>
</Anchor>
)}
</Group> </Group>
{media.artist && (
<Text
style={{
textAlign: 'center',
color: '#a0aec0',
}}
>
New album from {media.artist}
</Text>
)}
{media.episodeNumber && media.seasonNumber && ( {media.episodeNumber && media.seasonNumber && (
<Text <Text
style={{ style={{
@@ -63,6 +83,42 @@ function MediaDisplay(props: { media: IMedia }) {
); );
} }
export function ReadarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.author.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
title: media.title,
poster: poster ? poster.url : undefined,
artist: media.author.authorName,
overview: media.overview,
genres: media.genres,
}}
/>
);
}
export function LidarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.artist.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
title: media.title,
poster: poster ? poster.url : undefined,
artist: media.artist.artistName,
overview: media.overview,
genres: media.genres,
}}
/>
);
}
export function RadarrMediaDisplay(props: any) { export function RadarrMediaDisplay(props: any) {
const { media }: { media: any } = props; const { media }: { media: any } = props;
// Find a poster CoverType // Find a poster CoverType

View File

@@ -27,6 +27,7 @@ export const ServiceTypeList = [
'Lidarr', 'Lidarr',
'Plex', 'Plex',
'Radarr', 'Radarr',
'Readarr',
'Sonarr', 'Sonarr',
'qBittorrent', 'qBittorrent',
]; ];
@@ -36,6 +37,7 @@ export type ServiceType =
| 'Lidarr' | 'Lidarr'
| 'Plex' | 'Plex'
| 'Radarr' | 'Radarr'
| 'Readarr'
| 'Sonarr' | 'Sonarr'
| 'qBittorrent'; | 'qBittorrent';