diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 09e445637..ba0cca0a1 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -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')} /> )} + {media.plexUrl && ( + + Available on Plex + + )} {media.imdbId && ( diff --git a/src/components/modules/overseerr/OverseerrMediaDisplay.tsx b/src/components/modules/overseerr/OverseerrMediaDisplay.tsx index c9f979b93..708d2347b 100644 --- a/src/components/modules/overseerr/OverseerrMediaDisplay.tsx +++ b/src/components/modules/overseerr/OverseerrMediaDisplay.tsx @@ -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 ( diff --git a/src/components/modules/search/SearchModule.tsx b/src/components/modules/search/SearchModule.tsx index d553965e7..3cc8e946a 100644 --- a/src/components/modules/search/SearchModule.tsx +++ b/src/components/modules/search/SearchModule.tsx @@ -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(); const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q='; const textInput = useRef(); - 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 = (
@@ -43,12 +75,6 @@ export default function SearchBar(props: any) {
); - 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 (
{ @@ -89,36 +116,46 @@ export default function SearchBar(props: any) { })} > setOpened(true)} - onBlurCapture={() => setOpened(false)} + opened={results} target={ - + trapFocus={false} + transition="pop-bottom-right" + onFocusCapture={() => setOpened(true)} + onBlurCapture={() => setOpened(false)} + target={ + + } + > + + tip: Use the prefixes !yt and !t in front of your query to search on + YouTube or for a Torrent respectively. + + } > - - tip: Use the prefixes !yt and !t in front of your query to search on YouTube - or for a Torrent respectively. - + {/* Loop on the first 5 items of data */} + {data.slice(0, 5).map((item, index) => ( + + ))} ); diff --git a/src/pages/api/modules/overseerr.ts b/src/pages/api/modules/overseerr.ts new file mode 100644 index 000000000..782f16b62 --- /dev/null +++ b/src/pages/api/modules/overseerr.ts @@ -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', + }); +}; diff --git a/src/pages/tryoverseerr.tsx b/src/pages/tryoverseerr.tsx index dbe9ebdce..fae3d28d8 100644 --- a/src/pages/tryoverseerr.tsx +++ b/src/pages/tryoverseerr.tsx @@ -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 ( + ); } diff --git a/src/tools/types.ts b/src/tools/types.ts index 13c2311db..50bf03782 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -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;