mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 23:45:48 +01:00
✨ Lidarr and Readarr integrations
This commit is contained in:
@@ -173,7 +173,10 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<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
|
||||
required
|
||||
label="API key"
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
/* 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 { Calendar } from '@mantine/dates';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { Calendar as CalendarIcon, Check } from 'tabler-icons-react';
|
||||
import { RadarrMediaDisplay, SonarrMediaDisplay } from './MediaDisplay';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { IModule } from '../modules';
|
||||
import {
|
||||
SonarrMediaDisplay,
|
||||
RadarrMediaDisplay,
|
||||
LidarrMediaDisplay,
|
||||
ReadarrMediaDisplay,
|
||||
} from './MediaDisplay';
|
||||
|
||||
export const CalendarModule: IModule = {
|
||||
title: 'Calendar',
|
||||
@@ -19,17 +24,25 @@ export const CalendarModule: IModule = {
|
||||
export default function CalendarComponent(props: any) {
|
||||
const { config } = useConfig();
|
||||
const [sonarrMedias, setSonarrMedias] = useState([] as any);
|
||||
const [lidarrMedias, setLidarrMedias] = useState([] as any);
|
||||
const [radarrMedias, setRadarrMedias] = useState([] as any);
|
||||
const [readarrMedias, setReadarrMedias] = useState([] as any);
|
||||
|
||||
useEffect(() => {
|
||||
// Filter only sonarr and radarr services
|
||||
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
|
||||
const sonarrService = filtered.filter((service) => service.type === 'Sonarr').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();
|
||||
if (sonarrService && sonarrService.apiKey) {
|
||||
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]);
|
||||
|
||||
if (sonarrMedias === undefined && radarrMedias === undefined) {
|
||||
@@ -82,6 +133,8 @@ export default function CalendarComponent(props: any) {
|
||||
renderdate={renderdate}
|
||||
sonarrmedias={sonarrMedias}
|
||||
radarrmedias={radarrMedias}
|
||||
lidarrmedias={lidarrMedias}
|
||||
readarrmedias={readarrMedias}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -93,12 +146,25 @@ function DayComponent(props: any) {
|
||||
renderdate,
|
||||
sonarrmedias,
|
||||
radarrmedias,
|
||||
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props;
|
||||
lidarrmedias,
|
||||
readarrmedias,
|
||||
}: { renderdate: Date; sonarrmedias: []; radarrmedias: []; lidarrmedias: []; readarrmedias: [] } =
|
||||
props;
|
||||
const [opened, setOpened] = useState(false);
|
||||
const theme = useMantineTheme();
|
||||
|
||||
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 date = new Date(media.airDate);
|
||||
// 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 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>;
|
||||
}
|
||||
|
||||
@@ -119,8 +190,58 @@ function DayComponent(props: any) {
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
{radarrFiltered.length > 0 && <Indicator size={7} color="yellow" children={null} />}
|
||||
{sonarrFiltered.length > 0 && <Indicator size={7} offset={8} color="blue" children={null} />}
|
||||
{readarrFiltered.length > 0 && (
|
||||
<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
|
||||
position="left"
|
||||
radius="lg"
|
||||
@@ -147,6 +268,18 @@ function DayComponent(props: any) {
|
||||
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
|
||||
</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>
|
||||
</Popover>
|
||||
</Box>
|
||||
|
||||
@@ -3,9 +3,10 @@ import { Link } from 'tabler-icons-react';
|
||||
|
||||
export interface IMedia {
|
||||
overview: string;
|
||||
imdbId: any;
|
||||
imdbId?: any;
|
||||
artist?: string;
|
||||
title: string;
|
||||
poster: string;
|
||||
poster?: string;
|
||||
genres: string[];
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
@@ -15,6 +16,7 @@ function MediaDisplay(props: { media: IMedia }) {
|
||||
const { media }: { media: IMedia } = props;
|
||||
return (
|
||||
<Group noWrap align="self-start" mr={15}>
|
||||
{media.poster && (
|
||||
<Image
|
||||
radius="md"
|
||||
fit="cover"
|
||||
@@ -23,6 +25,8 @@ function MediaDisplay(props: { media: IMedia }) {
|
||||
width={300}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
justify="space-between"
|
||||
sx={(theme) => ({
|
||||
@@ -32,12 +36,28 @@ function MediaDisplay(props: { media: IMedia }) {
|
||||
<Group direction="column">
|
||||
<Group noWrap>
|
||||
<Title order={3}>{media.title}</Title>
|
||||
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
|
||||
{media.imdbId && (
|
||||
<Anchor
|
||||
href={`https://www.imdb.com/title/${media.imdbId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ActionIcon>
|
||||
<Link />
|
||||
</ActionIcon>
|
||||
</Anchor>
|
||||
)}
|
||||
</Group>
|
||||
{media.artist && (
|
||||
<Text
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
color: '#a0aec0',
|
||||
}}
|
||||
>
|
||||
New album from {media.artist}
|
||||
</Text>
|
||||
)}
|
||||
{media.episodeNumber && media.seasonNumber && (
|
||||
<Text
|
||||
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) {
|
||||
const { media }: { media: any } = props;
|
||||
// Find a poster CoverType
|
||||
|
||||
@@ -27,6 +27,7 @@ export const ServiceTypeList = [
|
||||
'Lidarr',
|
||||
'Plex',
|
||||
'Radarr',
|
||||
'Readarr',
|
||||
'Sonarr',
|
||||
'qBittorrent',
|
||||
];
|
||||
@@ -36,6 +37,7 @@ export type ServiceType =
|
||||
| 'Lidarr'
|
||||
| 'Plex'
|
||||
| 'Radarr'
|
||||
| 'Readarr'
|
||||
| 'Sonarr'
|
||||
| 'qBittorrent';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user