Avancement on Overseerr integration

This commit is contained in:
ajnart
2022-05-29 21:39:57 +02:00
parent 596db5fefc
commit 1de20d1583
7 changed files with 153 additions and 106 deletions

View File

@@ -14,9 +14,10 @@ import {
Text,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { IconApps as Apps } from '@tabler/icons';
import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks';
import { useConfig } from '../../tools/state';
import { ServiceTypeList } from '../../tools/types';
@@ -134,6 +135,14 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
},
});
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
useEffect(() => {
if (form.values.name !== debounced) return;
MatchIcon(form.values.name, form);
MatchService(form.values.name, form);
MatchPort(form.values.name, form);
}, [debounced]);
// Try to set const hostname to new URL(form.values.url).hostname)
// If it fails, set it to the form.values.url
let hostname = form.values.url;
@@ -186,14 +195,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
required
label="Service name"
placeholder="Plex"
value={form.values.name}
onChange={(event) => {
form.setFieldValue('name', event.currentTarget.value);
MatchIcon(event.currentTarget.value, form);
MatchService(event.currentTarget.value, form);
MatchPort(event.currentTarget.value, form);
}}
error={form.errors.name && 'Invalid icon url'}
{...form.getInputProps('name')}
/>
<TextInput

View File

@@ -1,4 +1,15 @@
import { Image, Group, Title, Badge, Text, ActionIcon, Anchor, ScrollArea, Tooltip, GroupProps } from '@mantine/core';
import {
Image,
Group,
Title,
Badge,
Text,
ActionIcon,
Anchor,
ScrollArea,
Tooltip,
GroupProps,
} from '@mantine/core';
import { IconLink, IconPlayerPlay } from '@tabler/icons';
import { useConfig } from '../../../tools/state';
import { serviceItem } from '../../../tools/types';
@@ -46,6 +57,11 @@ export function MediaDisplay(
</Anchor>
</Tooltip>
)}
{media.plexUrl && (
<Badge color="green" size="lg">
Available on Plex
</Badge>
)}
{media.imdbId && (
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
<ActionIcon>

View File

@@ -1,62 +1,8 @@
import { Card } from '@mantine/core';
import { MediaDisplay } from '../calendar/MediaDisplay';
export interface OverseerrMedia {
id: number;
firstAirDate: string;
genreIds: number[];
mediaType: string;
name: string;
originCountry: string[];
originalLanguage: string;
originalName: string;
overview: string;
popularity: number;
voteAverage: number;
voteCount: number;
backdropPath: string;
posterPath: string;
mediaInfo: MediaInfo;
}
export interface MediaInfo {
downloadStatus: any[];
downloadStatus4k: any[];
id: number;
mediaType: string;
tmdbId: number;
tvdbId: number;
imdbId: null;
status: number;
status4k: number;
createdAt: string;
updatedAt: string;
lastSeasonChange: string;
mediaAddedAt: string;
serviceId: number;
serviceId4k: null;
externalServiceId: number;
externalServiceId4k: null;
externalServiceSlug: string;
externalServiceSlug4k: null;
ratingKey: string;
ratingKey4k: null;
seasons: Season[];
plexUrl: string;
serviceUrl: string;
}
export interface Season {
id: number;
seasonNumber: number;
status: number;
status4k: number;
createdAt: string;
updatedAt: string;
}
export default function OverseerrMediaDisplay(props: any) {
const { media }: { media: OverseerrMedia } = props;
const { media }: { media: any } = props;
return (
<Card shadow="xl" withBorder>
<MediaDisplay
@@ -64,13 +10,13 @@ export default function OverseerrMediaDisplay(props: any) {
width: 600,
}}
media={{
title: media.name,
seasonNumber: media.mediaInfo.seasons.length + 1,
title: media.name ?? media.originalTitle,
overview: media.overview,
plexUrl: media.mediaInfo.plexUrl,
imdbId: media.mediaInfo.imdbId,
poster: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${media.posterPath}`,
genres: [`score: ${media.voteAverage}/10`],
seasonNumber: media.mediaInfo?.seasons.length,
plexUrl: media.mediaInfo?.plexUrl,
imdbId: media.mediaInfo?.imdbId,
}}
/>
</Card>

View File

@@ -1,13 +1,21 @@
import { TextInput, Kbd, createStyles, Text, Popover } from '@mantine/core';
import { useForm, useHotkeys } from '@mantine/hooks';
import { useRef, useState } from 'react';
import {
Kbd,
createStyles,
Text,
Popover,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
import { useEffect, useRef, useState } from 'react';
import {
IconSearch as Search,
IconBrandYoutube as BrandYoutube,
IconDownload as Download,
} from '@tabler/icons';
import axios from 'axios';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
import OverseerrMediaDisplay from '../overseerr/OverseerrMediaDisplay';
const useStyles = createStyles((theme) => ({
hide: {
@@ -29,11 +37,35 @@ export const SearchModule: IModule = {
export default function SearchBar(props: any) {
const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false);
const [results, setOpenedResults] = useState(false);
const [icon, setIcon] = useState(<Search />);
const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q=';
const textInput = useRef<HTMLInputElement>();
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
// Find a service with the type of 'Overseerr'
const service = config.services.find((s) => s.type === 'Overseerr');
const form = useForm({
initialValues: {
query: '',
},
});
const [debounced, cancel] = useDebouncedValue(form.values.query, 250);
const [data, setData] = useState([]);
useEffect(() => {
if (form.values.query !== debounced || form.values.query === '') return;
setOpened(false);
setOpenedResults(true);
if (service) {
const serviceUrl = new URL(service.url);
axios
.post(`/api/modules/overseerr?query=${form.values.query}`, {
service,
})
.then((res) => setData(res.data.results ?? []));
}
}, [debounced]);
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
const { classes, cx } = useStyles();
const rightSection = (
<div className={classes.hide}>
@@ -43,12 +75,6 @@ export default function SearchBar(props: any) {
</div>
);
const form = useForm({
initialValues: {
query: '',
},
});
// If enabled modules doesn't contain the module, return null
// If module in enabled
@@ -57,6 +83,7 @@ export default function SearchBar(props: any) {
return null;
}
// Data with label as item.name
return (
<form
onChange={() => {
@@ -88,6 +115,9 @@ export default function SearchBar(props: any) {
}, 20);
})}
>
<Popover
opened={results}
target={
<Popover
opened={opened}
position="bottom"
@@ -116,10 +146,17 @@ export default function SearchBar(props: any) {
}
>
<Text>
tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
or for a Torrent respectively.
tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on
YouTube or for a Torrent respectively.
</Text>
</Popover>
}
>
{/* Loop on the first 5 items of data */}
{data.slice(0, 5).map((item, index) => (
<OverseerrMediaDisplay key={index} media={item} />
))}
</Popover>
</form>
);
}

View File

@@ -0,0 +1,41 @@
import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next';
import { serviceItem } from '../../../tools/types';
async function Post(req: NextApiRequest, res: NextApiResponse) {
const { service }: { service: serviceItem } = req.body;
const { query } = req.query;
// If query is an empty string, return an empty array
if (query === '') {
return res.status(200).json([]);
}
if (!service || !query || !service.apiKey) {
return res.status(400).json({
error: 'Wrong request',
});
}
const serviceUrl = new URL(service.url);
const data = await axios
.get(`${serviceUrl.origin}/api/v1/search?query=${query}`, {
headers: {
// Set X-Api-Key to the value of the API key
'X-Api-Key': service.apiKey,
},
})
.then((res) => res.data);
// Get login, password and url from the body
res.status(200).json(
data,
);
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the reuqest is a POST or a GET
if (req.method === 'POST') {
return Post(req, res);
}
return res.status(405).json({
statusCode: 405,
message: 'Method not allowed',
});
};

View File

@@ -3,11 +3,14 @@ import OverseerrMediaDisplay, {
OverseerrMedia,
} from '../components/modules/overseerr/OverseerrMediaDisplay';
import media from '../components/modules/overseerr/example.json';
import { ModuleWrapper } from '../components/modules/moduleWrapper';
import { SearchModule } from '../components/modules';
export default function TryOverseerr() {
return (
<Group direction="column">
<OverseerrMediaDisplay media={media} />
<ModuleWrapper module={SearchModule} />
</Group>
);
}

View File

@@ -31,6 +31,7 @@ export const ServiceTypeList = [
'Readarr',
'Sonarr',
'qBittorrent',
'Overseerr',
];
export type ServiceType =
| 'Other'
@@ -41,7 +42,8 @@ export type ServiceType =
| 'Radarr'
| 'Readarr'
| 'Sonarr'
| 'qBittorrent';
| 'qBittorrent'
| 'Overseerr';
export interface serviceItem {
id: string;