diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 6926356e0..54af1cebe 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -17,7 +17,6 @@ import { Tooltip, } from '@mantine/core'; import { useForm } from '@mantine/form'; -import { useDebouncedValue } from '@mantine/hooks'; import { IconApps as Apps } from '@tabler/icons'; import { useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -249,6 +248,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & {(form.values.type === 'Sonarr' || form.values.type === 'Radarr' || form.values.type === 'Lidarr' || + form.values.type === 'Overseerr' || form.values.type === 'Readarr') && ( <> - - - ); -} diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 8282dcc67..3cb7d8931 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -8,9 +8,10 @@ import { Anchor, ScrollArea, createStyles, + Tooltip, } from '@mantine/core'; import { useMediaQuery } from '@mantine/hooks'; -import { IconLink as Link } from '@tabler/icons'; +import { IconLink, IconPlayerPlay } from '@tabler/icons'; import { useConfig } from '../../tools/state'; import { serviceItem } from '../../tools/types'; diff --git a/src/modules/index.ts b/src/modules/index.ts index 941ea3c99..88cb1ad02 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -6,3 +6,4 @@ export * from './ping'; export * from './search'; export * from './weather'; export * from './docker'; +export * from './overseerr'; diff --git a/src/modules/overseerr/OverseerrMediaDisplay.tsx b/src/modules/overseerr/OverseerrMediaDisplay.tsx new file mode 100644 index 000000000..a81416bfd --- /dev/null +++ b/src/modules/overseerr/OverseerrMediaDisplay.tsx @@ -0,0 +1,18 @@ +import { MediaDisplay } from '../common'; + +export default function OverseerrMediaDisplay(props: any) { + const { media }: { media: any } = props; + return ( + + ); +} diff --git a/src/modules/overseerr/OverseerrModule.tsx b/src/modules/overseerr/OverseerrModule.tsx new file mode 100644 index 000000000..9ba95c6d7 --- /dev/null +++ b/src/modules/overseerr/OverseerrModule.tsx @@ -0,0 +1,14 @@ +import { IconEyeglass } from '@tabler/icons'; +import { IModule } from '../ModuleTypes'; +import OverseerrMediaDisplay from './OverseerrMediaDisplay'; + +export const OverseerrModule: IModule = { + title: 'Overseerr', + description: 'Allows you to search and add media from Overseerr', + icon: IconEyeglass, + component: OverseerrMediaDisplay, +}; + +export interface OverseerSearchProps { + query: string; +} diff --git a/src/components/modules/overseerr/example.json b/src/modules/overseerr/example.json similarity index 100% rename from src/components/modules/overseerr/example.json rename to src/modules/overseerr/example.json diff --git a/src/modules/overseerr/index.ts b/src/modules/overseerr/index.ts new file mode 100644 index 000000000..bc20880a5 --- /dev/null +++ b/src/modules/overseerr/index.ts @@ -0,0 +1 @@ +export { OverseerrModule } from './OverseerrModule'; diff --git a/src/modules/search/SearchModule.tsx b/src/modules/search/SearchModule.tsx index 807f2b6ad..2cff9a7ae 100644 --- a/src/modules/search/SearchModule.tsx +++ b/src/modules/search/SearchModule.tsx @@ -1,14 +1,18 @@ -import { Kbd, createStyles, Autocomplete } from '@mantine/core'; +import { Kbd, createStyles, Autocomplete, ScrollArea, Popover, Divider } from '@mantine/core'; import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks'; -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { IconSearch as Search, IconBrandYoutube as BrandYoutube, IconDownload as Download, + IconMovie, } from '@tabler/icons'; import axios from 'axios'; +import { showNotification } from '@mantine/notifications'; import { useConfig } from '../../tools/state'; import { IModule } from '../ModuleTypes'; +import { OverseerrModule } from '../overseerr'; +import OverseerrMediaDisplay from '../overseerr/OverseerrMediaDisplay'; const useStyles = createStyles((theme) => ({ hide: { @@ -22,48 +26,72 @@ const useStyles = createStyles((theme) => ({ export const SearchModule: IModule = { title: 'Search Bar', - description: 'Show the current time and date in a card', + description: 'Search bar to search the web, youtube, torrents or overseerr', icon: Search, component: SearchBar, }; 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 { classes, cx } = useStyles(); + // Config + const { config } = useConfig(); + const isModuleEnabled = config.modules?.[SearchModule.title]?.enabled ?? false; + const isOverseerrEnabled = config.modules?.[OverseerrModule.title]?.enabled ?? false; + const OverseerrService = config.services.find((service) => service.type === 'Overseerr'); const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q='; + + const [OverseerrResults, setOverseerrResults] = useState([]); + const [icon, setIcon] = useState(); + const [results, setResults] = useState([]); + const [opened, setOpened] = useState(false); + const textInput = useRef(); - // Find a service with the type of 'Overseerr' + useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]); + const form = useForm({ initialValues: { query: '', }, }); - const [debounced, cancel] = useDebouncedValue(form.values.query, 250); - const [results, setResults] = useState([]); + useEffect(() => { - if (form.values.query !== debounced || form.values.query === '') return; - axios - .get(`/api/modules/search?q=${form.values.query}`) - .then((res) => setResults(res.data ?? [])); + if (OverseerrService === undefined && isOverseerrEnabled) { + showNotification({ + title: 'Overseerr integration', + message: 'Module enabled but no service is configured with the type "Overseerr"', + color: 'red', + }); + } + }, [OverseerrService, isOverseerrEnabled]); + + useEffect(() => { + if ( + form.values.query !== debounced || + form.values.query === '' || + (form.values.query.startsWith('!') && !form.values.query.startsWith('!os')) + ) { + return; + } + if (form.values.query.startsWith('!os')) { + axios + .get( + `/api/modules/overseerr?query=${form.values.query.replace('!os ', '')}&id=${ + OverseerrService?.id + }` + ) + .then((res) => { + setOverseerrResults(res.data.results ?? []); + }); + } else { + setOverseerrResults([]); + axios + .get(`/api/modules/search?q=${form.values.query}`) + .then((res) => setResults(res.data ?? [])); + } }, [debounced]); - useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]); - const { classes, cx } = useStyles(); - const rightSection = ( -
- Ctrl - + - K -
- ); - // If enabled modules doesn't contain the module, return null - // If module in enabled - - const exists = config.modules?.[SearchModule.title]?.enabled ?? false; - if (!exists) { + if (!isModuleEnabled) { return null; } @@ -76,53 +104,86 @@ export default function SearchBar(props: any) { onChange={() => { // If query contains !yt or !t add "Searching on YouTube" or "Searching torrent" const query = form.values.query.trim(); - const isYoutube = query.startsWith('!yt'); - const isTorrent = query.startsWith('!t'); - if (isYoutube) { - setIcon(); - } else if (isTorrent) { - setIcon(); - } else { - setIcon(); + switch (query.substring(0, 3)) { + case '!yt': + setIcon(); + break; + case '!t ': + setIcon(); + break; + case '!os': + setIcon(); + break; + default: + setIcon(); + break; } }} onSubmit={form.onSubmit((values) => { const query = values.query.trim(); - const isYoutube = query.startsWith('!yt'); - const isTorrent = query.startsWith('!t'); - form.setValues({ query: '' }); setTimeout(() => { - if (isYoutube) { - window.open(`https://www.youtube.com/results?search_query=${query.substring(4)}`); - } else if (isTorrent) { - window.open(`https://bitsearch.to/search?q=${query.substring(4)}`); - } else { - window.open( - `${ - queryUrl.includes('%s') - ? queryUrl.replace('%s', values.query) - : queryUrl + values.query - }` - ); + form.setValues({ query: '' }); + switch (query.substring(0, 3)) { + case '!yt': + window.open(`https://www.youtube.com/results?search_query=${query.substring(3)}`); + break; + case '!t ': + window.open(`https://www.torrentdownloads.me/search/?search=${query.substring(3)}`); + break; + case '!os': + break; + default: + window.open( + `${queryUrl.includes('%s') ? queryUrl.replace('%s', query) : `${queryUrl}${query}`}` + ); + break; } - }, 20); + }, 500); })} > - + trapFocus={false} + transition="pop-bottom-right" + onFocusCapture={() => setOpened(true)} + onBlurCapture={() => setOpened(false)} + target={ + + Ctrl + + + K + + } + radius="md" + size="md" + styles={{ rightSection: { pointerEvents: 'none' } }} + placeholder="Search the web..." + {...props} + {...form.getInputProps('query')} + /> + } + > + + {OverseerrResults.slice(0, 5).map((result, index) => ( + + + {index < OverseerrResults.length - 1 && } + + ))} + + ); } diff --git a/src/pages/api/modules/overseerr.ts b/src/pages/api/modules/overseerr.ts index 782f16b62..73ecf1768 100644 --- a/src/pages/api/modules/overseerr.ts +++ b/src/pages/api/modules/overseerr.ts @@ -1,15 +1,19 @@ import axios from 'axios'; +import { getCookie } from 'cookies-next'; import { NextApiRequest, NextApiResponse } from 'next'; -import { serviceItem } from '../../../tools/types'; +import { getConfig } from '../../../tools/getConfig'; +import { Config } from '../../../tools/types'; -async function Post(req: NextApiRequest, res: NextApiResponse) { - const { service }: { service: serviceItem } = req.body; - const { query } = req.query; +async function Get(req: NextApiRequest, res: NextApiResponse) { + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const { query, id } = req.query; + const service = config.services.find((service) => service.id === id); // If query is an empty string, return an empty array - if (query === '') { + if (query === '' || query === undefined) { return res.status(200).json([]); } - if (!service || !query || !service.apiKey) { + if (!service || !query || service === undefined || !service.apiKey) { return res.status(400).json({ error: 'Wrong request', }); @@ -24,15 +28,13 @@ async function Post(req: NextApiRequest, res: NextApiResponse) { }) .then((res) => res.data); // Get login, password and url from the body - res.status(200).json( - data, - ); + 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); + if (req.method === 'GET') { + return Get(req, res); } return res.status(405).json({ statusCode: 405, diff --git a/src/pages/tryoverseerr.tsx b/src/pages/tryoverseerr.tsx deleted file mode 100644 index fae3d28d8..000000000 --- a/src/pages/tryoverseerr.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Group, Title } from '@mantine/core'; -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 663263c6e..97b53d60f 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -70,6 +70,7 @@ export const ServiceTypeList = [ 'Readarr', 'Sonarr', 'Transmission', + 'Overseerr', ]; export type ServiceType = | 'Other' @@ -82,6 +83,7 @@ export type ServiceType = | 'Radarr' | 'Readarr' | 'Sonarr' + | 'Overseerr' | 'Transmission'; export function tryMatchPort(name: string, form?: any) { @@ -101,6 +103,9 @@ export const portmap = [ { name: 'readarr', value: '8787' }, { name: 'deluge', value: '8112' }, { name: 'transmission', value: '9091' }, + { name: 'plex', value: '32400' }, + { name: 'emby', value: '8096' }, + { name: 'overseerr', value: '5055' }, { name: 'dash.', value: '3001' }, ];