From 7ee56bd6edd73aed0210ee494569379ab1d994e6 Mon Sep 17 00:00:00 2001 From: ajnart Date: Fri, 27 May 2022 00:32:27 +0200 Subject: [PATCH 01/35] :sparkles: add default overseer image display --- .../modules/calendar/MediaDisplay.tsx | 23 ++++-- .../overseerr/OverseerrMediaDisplay.tsx | 78 +++++++++++++++++++ src/components/modules/overseerr/example.json | 72 +++++++++++++++++ src/pages/tryoverseerr.tsx | 13 ++++ 4 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 src/components/modules/overseerr/OverseerrMediaDisplay.tsx create mode 100644 src/components/modules/overseerr/example.json create mode 100644 src/pages/tryoverseerr.tsx diff --git a/src/components/modules/calendar/MediaDisplay.tsx b/src/components/modules/calendar/MediaDisplay.tsx index fc707c611..90ef42eef 100644 --- a/src/components/modules/calendar/MediaDisplay.tsx +++ b/src/components/modules/calendar/MediaDisplay.tsx @@ -11,13 +11,15 @@ export interface IMedia { poster?: string; genres: string[]; seasonNumber?: number; + plexUrl?: string; episodeNumber?: number; } - -function MediaDisplay(props: { media: IMedia }) { +export function MediaDisplay( + props: GroupProps & React.RefAttributes & { media: IMedia } +) { const { media }: { media: IMedia } = props; return ( - + {media.poster && ( )} - + {media.title} + {media.plexUrl && ( + + + + + + + + )} {media.imdbId && ( @@ -53,14 +64,14 @@ function MediaDisplay(props: { media: IMedia }) { New release from {media.artist} )} - {media.episodeNumber && media.seasonNumber && ( + {(media.episodeNumber || media.seasonNumber) && ( - Season {media.seasonNumber} episode {media.episodeNumber} + Season {media.seasonNumber} {media.episodeNumber && `episode ${media.episodeNumber}`} )} diff --git a/src/components/modules/overseerr/OverseerrMediaDisplay.tsx b/src/components/modules/overseerr/OverseerrMediaDisplay.tsx new file mode 100644 index 000000000..c9f979b93 --- /dev/null +++ b/src/components/modules/overseerr/OverseerrMediaDisplay.tsx @@ -0,0 +1,78 @@ +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; + return ( + + + + ); +} diff --git a/src/components/modules/overseerr/example.json b/src/components/modules/overseerr/example.json new file mode 100644 index 000000000..d9f35cad6 --- /dev/null +++ b/src/components/modules/overseerr/example.json @@ -0,0 +1,72 @@ +{ + "id": 86831, + "firstAirDate": "2019-03-15", + "genreIds": [ + 16, + 10765 + ], + "mediaType": "tv", + "name": "Love, Death & Robots", + "originCountry": [ + "US" + ], + "originalLanguage": "en", + "originalName": "Love, Death & Robots", + "overview": "Terrifying creatures, wicked surprises and dark comedy converge in this NSFW anthology of animated stories presented by Tim Miller and David Fincher.", + "popularity": 623.833, + "voteAverage": 8.2, + "voteCount": 1720, + "backdropPath": "/78NtUwwo3lhH7QGh4vG3U1qK1mc.jpg", + "posterPath": "/cRiDlzzZC5lL7fvImuSjs04SUIJ.jpg", + "mediaInfo": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 79, + "mediaType": "tv", + "tmdbId": 86831, + "tvdbId": 357888, + "imdbId": null, + "status": 4, + "status4k": 1, + "createdAt": "2022-02-05T04:30:01.000Z", + "updatedAt": "2022-02-05T09:25:22.000Z", + "lastSeasonChange": "2022-02-05T04:30:01.000Z", + "mediaAddedAt": "2022-02-04T01:16:35.000Z", + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 7, + "externalServiceId4k": null, + "externalServiceSlug": "love-death-and-robots", + "externalServiceSlug4k": null, + "ratingKey": "182", + "ratingKey4k": null, + "seasons": [ + { + "id": 11, + "seasonNumber": 1, + "status": 1, + "status4k": 1, + "createdAt": "2022-02-05T04:30:01.000Z", + "updatedAt": "2022-02-05T04:30:01.000Z" + }, + { + "id": 24, + "seasonNumber": 2, + "status": 5, + "status4k": 1, + "createdAt": "2022-02-05T04:30:01.000Z", + "updatedAt": "2022-02-05T04:30:01.000Z" + }, + { + "id": 85, + "seasonNumber": 3, + "status": 3, + "status4k": 1, + "createdAt": "2022-04-26T04:30:02.000Z", + "updatedAt": "2022-04-26T04:30:02.000Z" + } + ], + "plexUrl": "https://app.plex.tv/desktop#!/server/5b88b3c20d2d092c0ee848f9044f3f3bee033d91/details?key=%2Flibrary%2Fmetadata%2F182", + "serviceUrl": "http://server:8989/series/love-death-and-robots" + } +} \ No newline at end of file diff --git a/src/pages/tryoverseerr.tsx b/src/pages/tryoverseerr.tsx new file mode 100644 index 000000000..dbe9ebdce --- /dev/null +++ b/src/pages/tryoverseerr.tsx @@ -0,0 +1,13 @@ +import { Group, Title } from '@mantine/core'; +import OverseerrMediaDisplay, { + OverseerrMedia, +} from '../components/modules/overseerr/OverseerrMediaDisplay'; +import media from '../components/modules/overseerr/example.json'; + +export default function TryOverseerr() { + return ( + + + + ); +} From 596db5fefccf557ce3ab2721ba857d00e20fdbff Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 29 May 2022 19:09:12 +0200 Subject: [PATCH 02/35] :arrow_up: Upgrade dependencies --- src/components/modules/calendar/MediaDisplay.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/modules/calendar/MediaDisplay.tsx b/src/components/modules/calendar/MediaDisplay.tsx index 90ef42eef..b9843a2bb 100644 --- a/src/components/modules/calendar/MediaDisplay.tsx +++ b/src/components/modules/calendar/MediaDisplay.tsx @@ -1,5 +1,5 @@ -import { Image, Group, Title, Badge, Text, ActionIcon, Anchor, ScrollArea } from '@mantine/core'; -import { IconLink as Link } from '@tabler/icons'; +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'; @@ -41,7 +41,7 @@ export function MediaDisplay( - + @@ -49,7 +49,7 @@ export function MediaDisplay( {media.imdbId && ( - + )} From 1de20d15834178ebeeba932e6abc340406c0f0f4 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 29 May 2022 21:39:57 +0200 Subject: [PATCH 03/35] :sparkles: Avancement on Overseerr integration --- src/components/AppShelf/AddAppShelfItem.tsx | 20 ++-- .../modules/calendar/MediaDisplay.tsx | 18 ++- .../overseerr/OverseerrMediaDisplay.tsx | 64 +--------- .../modules/search/SearchModule.tsx | 109 ++++++++++++------ src/pages/api/modules/overseerr.ts | 41 +++++++ src/pages/tryoverseerr.tsx | 3 + src/tools/types.ts | 4 +- 7 files changed, 153 insertions(+), 106 deletions(-) create mode 100644 src/pages/api/modules/overseerr.ts 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; From 1f2d5608938cfa1d02f225fe6bb81212c42a05cf Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 24 Jul 2022 21:54:42 +0200 Subject: [PATCH 04/35] =?UTF-8?q?=F0=9F=90=9B=20Fix=20an=20old=20bug=20wit?= =?UTF-8?q?h=20the=20search=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/search/SearchModule.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/search/SearchModule.tsx b/src/modules/search/SearchModule.tsx index 6c0764936..807f2b6ad 100644 --- a/src/modules/search/SearchModule.tsx +++ b/src/modules/search/SearchModule.tsx @@ -93,9 +93,9 @@ export default function SearchBar(props: any) { form.setValues({ query: '' }); setTimeout(() => { if (isYoutube) { - window.open(`https://www.youtube.com/results?search_query=${query.substring(3)}`); + window.open(`https://www.youtube.com/results?search_query=${query.substring(4)}`); } else if (isTorrent) { - window.open(`https://bitsearch.to/search?q=${query.substring(3)}`); + window.open(`https://bitsearch.to/search?q=${query.substring(4)}`); } else { window.open( `${ From a3f5b252b935f1b97a73d12ff888443de8ff2d65 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 24 Jul 2022 23:18:01 +0200 Subject: [PATCH 05/35] :construction: WIP on Overseerr integration --- src/components/AppShelf/AddAppShelfItem.tsx | 2 +- .../overseerr/OverseerrMediaDisplay.tsx | 24 --- src/modules/common/MediaDisplay.tsx | 3 +- src/modules/index.ts | 1 + .../overseerr/OverseerrMediaDisplay.tsx | 18 ++ src/modules/overseerr/OverseerrModule.tsx | 14 ++ .../modules/overseerr/example.json | 0 src/modules/overseerr/index.ts | 1 + src/modules/search/SearchModule.tsx | 193 ++++++++++++------ src/pages/api/modules/overseerr.ts | 24 ++- src/pages/tryoverseerr.tsx | 16 -- src/tools/types.ts | 5 + 12 files changed, 182 insertions(+), 119 deletions(-) delete mode 100644 src/components/modules/overseerr/OverseerrMediaDisplay.tsx create mode 100644 src/modules/overseerr/OverseerrMediaDisplay.tsx create mode 100644 src/modules/overseerr/OverseerrModule.tsx rename src/{components => }/modules/overseerr/example.json (100%) create mode 100644 src/modules/overseerr/index.ts delete mode 100644 src/pages/tryoverseerr.tsx 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' }, ]; From b04171aa762e8265ed6697c87097f835c7b24b64 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 24 Jul 2022 23:48:48 +0200 Subject: [PATCH 06/35] :sparkles: Add preview if media is available on Plex --- src/modules/common/MediaDisplay.tsx | 80 +++++++++---------- .../overseerr/OverseerrMediaDisplay.tsx | 1 + src/modules/search/SearchModule.tsx | 6 +- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 3cb7d8931..05a2aae25 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -9,9 +9,10 @@ import { ScrollArea, createStyles, Tooltip, + Button, } from '@mantine/core'; import { useMediaQuery } from '@mantine/hooks'; -import { IconLink, IconPlayerPlay } from '@tabler/icons'; +import { IconLink } from '@tabler/icons'; import { useConfig } from '../../tools/state'; import { serviceItem } from '../../tools/types'; @@ -20,6 +21,7 @@ export interface IMedia { imdbId?: any; artist?: string; title: string; + voteAverage?: string; poster?: string; genres: string[]; seasonNumber?: number; @@ -55,50 +57,48 @@ export function MediaDisplay(props: { media: IMedia }) { alt={media.title} /> )} - - + + {media.title} - {media.plexUrl && ( - - - - - - - - )} - {media.plexUrl && ( - - Available on Plex - - )} - {media.imdbId && ( - - - - - + {media.artist && New release from {media.artist}} + {(media.episodeNumber || media.seasonNumber) && ( + + Season {media.seasonNumber}{' '} + {media.episodeNumber && `episode ${media.episodeNumber}`} + )} - {media.artist && ( - window.open(media.plexUrl) + : () => { + // TODO: implement overseerr media requests + throw new Error('Need to implement media reqests'); + } + } > - New release from {media.artist} - + {media.plexUrl ? 'Available on Plex' : 'Request'} + )} - {(media.episodeNumber || media.seasonNumber) && ( - - Season {media.seasonNumber} {media.episodeNumber && `episode ${media.episodeNumber}`} - + {media.imdbId && ( + + + + + )} diff --git a/src/modules/overseerr/OverseerrMediaDisplay.tsx b/src/modules/overseerr/OverseerrMediaDisplay.tsx index a81416bfd..0af4c31a4 100644 --- a/src/modules/overseerr/OverseerrMediaDisplay.tsx +++ b/src/modules/overseerr/OverseerrMediaDisplay.tsx @@ -12,6 +12,7 @@ export default function OverseerrMediaDisplay(props: any) { seasonNumber: media.mediaInfo?.seasons.length, plexUrl: media.mediaInfo?.plexUrl, imdbId: media.mediaInfo?.imdbId, + ...media, }} /> ); diff --git a/src/modules/search/SearchModule.tsx b/src/modules/search/SearchModule.tsx index 2cff9a7ae..9911fc04b 100644 --- a/src/modules/search/SearchModule.tsx +++ b/src/modules/search/SearchModule.tsx @@ -41,6 +41,7 @@ export default function SearchBar(props: any) { const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q='; const [OverseerrResults, setOverseerrResults] = useState([]); + const [loading, setLoading] = useState(false); const [icon, setIcon] = useState(); const [results, setResults] = useState([]); const [opened, setOpened] = useState(false); @@ -82,7 +83,9 @@ export default function SearchBar(props: any) { ) .then((res) => { setOverseerrResults(res.data.results ?? []); + setLoading(false); }); + setLoading(true); } else { setOverseerrResults([]); axios @@ -142,10 +145,9 @@ export default function SearchBar(props: any) { })} > 0 && opened} position="bottom" placement="start" - withArrow radius="md" trapFocus={false} transition="pop-bottom-right" From 77daffcc4bebd8c57e2f5910ea58132cf47e64cf Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 24 Jul 2022 23:54:57 +0200 Subject: [PATCH 07/35] :lipstick: Small style changes --- src/modules/common/MediaDisplay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 05a2aae25..8caa4f179 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -60,7 +60,7 @@ export function MediaDisplay(props: { media: IMedia }) { Date: Mon, 25 Jul 2022 00:05:28 +0200 Subject: [PATCH 08/35] :green_heart: CI --- .github/workflows/docker_dev.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker_dev.yml b/.github/workflows/docker_dev.yml index 176ac912b..7075370dd 100644 --- a/.github/workflows/docker_dev.yml +++ b/.github/workflows/docker_dev.yml @@ -15,9 +15,9 @@ on: - '**.md' workflow_dispatch: inputs: - tags: + tag: required: true - description: 'Tags to deploy to' + description: 'Tag to deploy to' env: # Use docker.io for Docker Hub if empty @@ -79,7 +79,8 @@ jobs: # generate Docker tags based on the following events/attributes tags: | type=ref,event=pr - tpye=raw,value=dev,priority=1 + type=raw,value=${{ github.event.inputs.tag }}, prefix=test-${{ github.event.inputs.tag }},enable=${{ github.event.inputs.tag }} + tpye=raw,value=dev,priority=1,enable=!${{ github.event.inputs.tags }} - name: Set up QEMU uses: docker/setup-qemu-action@v2 From aa990671c19c6868385e72f5c17cb3903febfa22 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 25 Jul 2022 00:15:20 +0200 Subject: [PATCH 09/35] :green_heart: CI --- .github/workflows/docker_dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_dev.yml b/.github/workflows/docker_dev.yml index 7075370dd..db5961ed4 100644 --- a/.github/workflows/docker_dev.yml +++ b/.github/workflows/docker_dev.yml @@ -79,8 +79,8 @@ jobs: # generate Docker tags based on the following events/attributes tags: | type=ref,event=pr - type=raw,value=${{ github.event.inputs.tag }}, prefix=test-${{ github.event.inputs.tag }},enable=${{ github.event.inputs.tag }} - tpye=raw,value=dev,priority=1,enable=!${{ github.event.inputs.tags }} + type=raw,value=${{ github.event.inputs.tag }}, prefix=test-,enable=${{ github.event.inputs.tag != '' }} + tpye=raw,value=dev,priority=1,enable=${{ github.event.inputs.tag == '' }} - name: Set up QEMU uses: docker/setup-qemu-action@v2 From d07b51f67de5cd2b5cce81ea839cb8cb2a501874 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:13:26 +0200 Subject: [PATCH 10/35] :package: Add Consola for logging --- package.json | 2 ++ yarn.lock | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/package.json b/package.json index fdd982af7..b097dea78 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@mantine/dropzone": "^4.2.12", "@mantine/form": "^4.2.12", "@mantine/hooks": "^4.2.12", + "@mantine/modals": "^5.0.3", "@mantine/next": "^4.2.12", "@mantine/notifications": "^4.2.12", "@mantine/prism": "^4.2.12", @@ -42,6 +43,7 @@ "@nivo/line": "^0.79.1", "@tabler/icons": "^1.78.0", "axios": "^0.27.2", + "consola": "^2.15.3", "cookies-next": "^2.1.1", "dayjs": "^1.11.4", "dockerode": "^3.3.2", diff --git a/yarn.lock b/yarn.lock index 4e9549474..ca9ac261f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1102,6 +1102,20 @@ __metadata: languageName: node linkType: hard +"@mantine/modals@npm:^5.0.3": + version: 5.0.3 + resolution: "@mantine/modals@npm:5.0.3" + dependencies: + "@mantine/utils": 5.0.3 + peerDependencies: + "@mantine/core": 5.0.3 + "@mantine/hooks": 5.0.3 + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 4d38f867405a91421c7d84716cceeb34a766c476aa0159034342e1ee681aa990cd16863e55930a889b6149fdae1b43ba00703783d7d5d91a271db40c9d3c5ec9 + languageName: node + linkType: hard + "@mantine/next@npm:^4.2.12": version: 4.2.12 resolution: "@mantine/next@npm:4.2.12" @@ -1179,6 +1193,15 @@ __metadata: languageName: node linkType: hard +"@mantine/utils@npm:5.0.3": + version: 5.0.3 + resolution: "@mantine/utils@npm:5.0.3" + peerDependencies: + react: ">=16.8.0" + checksum: febde84bcb4369dad4f4476fbd864220608855a30921816ea7033689cc6c921212de2042ae5fc0bd446273874198c3775142c46dae6465f4173fa411798aff75 + languageName: node + linkType: hard + "@motionone/animation@npm:^10.12.0": version: 10.13.1 resolution: "@motionone/animation@npm:10.13.1" @@ -2910,6 +2933,13 @@ __metadata: languageName: node linkType: hard +"consola@npm:^2.15.3": + version: 2.15.3 + resolution: "consola@npm:2.15.3" + checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28 + languageName: node + linkType: hard + "console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -4474,6 +4504,7 @@ __metadata: "@mantine/dropzone": ^4.2.12 "@mantine/form": ^4.2.12 "@mantine/hooks": ^4.2.12 + "@mantine/modals": ^5.0.3 "@mantine/next": ^4.2.12 "@mantine/notifications": ^4.2.12 "@mantine/prism": ^4.2.12 @@ -4489,6 +4520,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.30.7 "@typescript-eslint/parser": ^5.30.7 axios: ^0.27.2 + consola: ^2.15.3 cookies-next: ^2.1.1 dayjs: ^1.11.4 dockerode: ^3.3.2 From f0bb3f08b05f1288b0c740c451970041d6ce4d45 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:13:44 +0200 Subject: [PATCH 11/35] :label: Fix missing types --- src/tools/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/types.ts b/src/tools/types.ts index 97b53d60f..527302b52 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -169,7 +169,7 @@ export const MatchingImages: { export interface serviceItem { id: string; name: string; - type: string; + type: ServiceType; url: string; icon: string; category?: string; From 13d70cf0fd8662cf938f0136137bb8c6c5993f90 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:14:17 +0200 Subject: [PATCH 12/35] :recycle: Rework Overseerr API --- src/pages/api/modules/overseerr/[id].tsx | 126 ++++++++++++++++++ .../{overseerr.ts => overseerr/index.ts} | 8 +- 2 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 src/pages/api/modules/overseerr/[id].tsx rename src/pages/api/modules/{overseerr.ts => overseerr/index.ts} (85%) diff --git a/src/pages/api/modules/overseerr/[id].tsx b/src/pages/api/modules/overseerr/[id].tsx new file mode 100644 index 000000000..3368ea6a7 --- /dev/null +++ b/src/pages/api/modules/overseerr/[id].tsx @@ -0,0 +1,126 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { getCookie } from 'cookies-next'; +import axios from 'axios'; +import Consola from 'consola'; +import { getConfig } from '../../../../tools/getConfig'; +import { Config } from '../../../../tools/types'; +import { MediaType } from '../../../../modules/overseerr/SearchResult'; + +async function Get(req: NextApiRequest, res: NextApiResponse) { + // Get the slug of the request + const { id, type } = req.query as { id: string; type: string }; + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const service = config.services.find((service) => service.type === 'Overseerr'); + if (!id) { + return res.status(400).json({ error: 'No id provided' }); + } + if (!type) { + return res.status(400).json({ error: 'No type provided' }); + } + if (!service?.apiKey) { + return res.status(400).json({ error: 'No service found' }); + } + + const serviceUrl = new URL(service.url); + switch (type) { + case 'movie': + return axios + .get(`${serviceUrl.origin}/api/v1/movie/${id}`, { + headers: { + // Set X-Api-Key to the value of the API key + 'X-Api-Key': service.apiKey, + }, + }) + .then((axiosres) => res.status(200).json(axiosres.data)) + + .catch((err) => { + Consola.error(err); + return res.status(500).json({ + message: 'Something went wrong', + }); + }); + case 'tv': + // Make request to the tv api + return axios + .get(`${serviceUrl.origin}/api/v1/tv/${id}`, { + headers: { + // Set X-Api-Key to the value of the API key + 'X-Api-Key': service.apiKey, + }, + }) + .then((axiosres) => res.status(200).json(axiosres.data)) + .catch((err) => { + Consola.error(err); + return res.status(500).json({ + message: 'Something went wrong', + }); + }); + + default: + return res.status(400).json({ + message: 'Wrong request, type should be movie or tv', + }); + } +} + +async function Post(req: NextApiRequest, res: NextApiResponse) { + // Get the slug of the request + const { id } = req.query as { id: string }; + const { seasons, type } = req.body as { seasons?: number[]; type: MediaType }; + const configName = getCookie('config-name', { req }); + const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props; + const service = config.services.find((service) => service.type === 'Overseerr'); + if (!id) { + return res.status(400).json({ error: 'No id provided' }); + } + if (!type) { + return res.status(400).json({ error: 'No type provided' }); + } + if (!service?.apiKey) { + return res.status(400).json({ error: 'No service found' }); + } + if (type === 'movie' && !seasons) { + return res.status(400).json({ error: 'No seasons provided' }); + } + const serviceUrl = new URL(service.url); + Consola.info('Got an Overseerr request with these arguments', { + mediaType: type, + mediaId: id, + seasons, + }); + return axios + .post( + `${serviceUrl.origin}/api/v1/request`, + { + mediaType: type, + mediaId: Number(id), + seasons, + }, + { + headers: { + // Set X-Api-Key to the value of the API key + 'X-Api-Key': service.apiKey, + }, + } + ) + .then((axiosres) => res.status(200).json(axiosres.data)) + .catch((err) => + res.status(500).json({ + message: err.message, + }) + ); +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + return Post(req, res); + } + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/src/pages/api/modules/overseerr.ts b/src/pages/api/modules/overseerr/index.ts similarity index 85% rename from src/pages/api/modules/overseerr.ts rename to src/pages/api/modules/overseerr/index.ts index 73ecf1768..92b86a5ac 100644 --- a/src/pages/api/modules/overseerr.ts +++ b/src/pages/api/modules/overseerr/index.ts @@ -1,14 +1,14 @@ import axios from 'axios'; import { getCookie } from 'cookies-next'; import { NextApiRequest, NextApiResponse } from 'next'; -import { getConfig } from '../../../tools/getConfig'; -import { Config } from '../../../tools/types'; +import { getConfig } from '../../../../tools/getConfig'; +import { Config } from '../../../../tools/types'; 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); + const { query } = req.query; + const service = config.services.find((service) => service.type === 'Overseerr'); // If query is an empty string, return an empty array if (query === '' || query === undefined) { return res.status(200).json([]); From 8abf2af212d19f8994f69045f47061b6acb0c9b2 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:14:37 +0200 Subject: [PATCH 13/35] :zap: Add ModalsProvider to the App --- src/pages/_app.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d22a65538..538390309 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,6 +6,7 @@ import Head from 'next/head'; import { MantineProvider, ColorScheme, ColorSchemeProvider, MantineTheme } from '@mantine/core'; import { NotificationsProvider } from '@mantine/notifications'; import { useHotkeys } from '@mantine/hooks'; +import { ModalsProvider } from '@mantine/modals'; import { ConfigProvider } from '../tools/state'; import { theme } from '../tools/theme'; import { styles } from '../tools/styles'; @@ -56,9 +57,11 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch withNormalizeCSS > - - - + + + + + From 0e3c9e7ba846011f9da8cc894f5309dce9393c49 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:14:57 +0200 Subject: [PATCH 14/35] :construction: Change query in SearchBar to use new API --- src/modules/search/SearchModule.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/search/SearchModule.tsx b/src/modules/search/SearchModule.tsx index 9911fc04b..38c30acc7 100644 --- a/src/modules/search/SearchModule.tsx +++ b/src/modules/search/SearchModule.tsx @@ -76,11 +76,7 @@ export default function SearchBar(props: any) { } if (form.values.query.startsWith('!os')) { axios - .get( - `/api/modules/overseerr?query=${form.values.query.replace('!os ', '')}&id=${ - OverseerrService?.id - }` - ) + .get(`/api/modules/overseerr?query=${form.values.query.replace('!os ', '')}`) .then((res) => { setOverseerrResults(res.data.results ?? []); setLoading(false); From 40a76593a2c16747083226c3e07a5204f44ae233 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:15:15 +0200 Subject: [PATCH 15/35] :test_tube: Add testing page for overseerr request --- src/pages/tryrequest.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/pages/tryrequest.tsx diff --git a/src/pages/tryrequest.tsx b/src/pages/tryrequest.tsx new file mode 100644 index 000000000..0cc195a2a --- /dev/null +++ b/src/pages/tryrequest.tsx @@ -0,0 +1,22 @@ +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import RequestModal from '../modules/overseerr/RequestModal'; + +export default function TryRequest(props: any) { + const [OverseerrResults, setOverseerrResults] = useState([]); + useEffect(() => { + // Use the overseerr API get the media info. + axios + .get('/api/modules/overseerr?query=Lucifer') + .then((res) => { + setOverseerrResults(res.data.results); + }) + .catch((err) => { + throw err; + }); + }, [props]); + if (!OverseerrResults || OverseerrResults.length === 0) { + return null; + } + return ; +} From a3bc9ab9f4f66ba115c72d98de4d2e8342a91500 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:15:35 +0200 Subject: [PATCH 16/35] :label: Add type definitions for Movie/Tv/Request --- src/modules/overseerr/Movie.d.ts | 248 ++++++++++++++++++++ src/modules/overseerr/SearchResult.d.ts | 65 ++++++ src/modules/overseerr/TvShow.d.ts | 295 ++++++++++++++++++++++++ 3 files changed, 608 insertions(+) create mode 100644 src/modules/overseerr/Movie.d.ts create mode 100644 src/modules/overseerr/SearchResult.d.ts create mode 100644 src/modules/overseerr/TvShow.d.ts diff --git a/src/modules/overseerr/Movie.d.ts b/src/modules/overseerr/Movie.d.ts new file mode 100644 index 000000000..558bccbcc --- /dev/null +++ b/src/modules/overseerr/Movie.d.ts @@ -0,0 +1,248 @@ +export interface MovieResult { + id: number; + adult: boolean; + budget: number; + genres: Genre[]; + relatedVideos: RelatedVideo[]; + originalLanguage: string; + originalTitle: string; + popularity: number; + productionCompanies: ProductionCompany[]; + productionCountries: ProductionCountry[]; + releaseDate: Date; + releases: Releases; + revenue: number; + spokenLanguages: SpokenLanguage[]; + status: string; + title: string; + video: boolean; + voteAverage: number; + voteCount: number; + backdropPath: string; + homepage: string; + imdbId: string; + overview: string; + posterPath: string; + runtime: number; + tagline: string; + credits: Credits; + collection: Collection; + externalIds: ExternalIDS; + mediaInfo: Media; + watchProviders: WatchProvider[]; +} + +export interface Collection { + id: number; + name: string; + posterPath: string; + backdropPath: string; +} + +export interface Credits { + cast: Cast[]; + crew: Crew[]; +} + +export interface Cast { + castId: number; + character: string; + creditId: string; + id: number; + name: string; + order: number; + gender: number; + profilePath: null | string; +} + +export interface Crew { + creditId: string; + department: Department; + id: number; + job: string; + name: string; + gender: number; + profilePath: null | string; +} + +export enum Department { + Art = 'Art', + Camera = 'Camera', + CostumeMakeUp = 'Costume & Make-Up', + Crew = 'Crew', + Directing = 'Directing', + Editing = 'Editing', + Production = 'Production', + Sound = 'Sound', + VisualEffects = 'Visual Effects', + Writing = 'Writing', +} + +export interface ExternalIDS { + facebookId: string; + imdbId: string; + instagramId: string; + twitterId: string; +} + +export interface Genre { + id: number; + name: string; +} + +export interface Request { + id: number; + status: number; + createdAt: Date; + updatedAt: Date; + type: string; + is4k: boolean; + serverId: number; + profileId: number; + rootFolder: string; + languageProfileId: null; + tags: any[]; + media: Media; + requestedBy: EdBy; + modifiedBy: EdBy; + seasons: any[]; + seasonCount: number; +} + +export interface Media { + downloadStatus: any[]; + downloadStatus4k: any[]; + id: number; + mediaType: string; + tmdbId: number; + tvdbId: null; + imdbId: null; + status: number; + status4k: number; + createdAt: Date; + updatedAt: Date; + lastSeasonChange: Date; + mediaAddedAt: Date; + serviceId: number; + serviceId4k: null; + externalServiceId: number; + externalServiceId4k: null; + externalServiceSlug: string; + externalServiceSlug4k: null; + ratingKey: string; + ratingKey4k: null; + requests?: Request[]; + issues?: any[]; + seasons: any[]; + plexUrl: string; + serviceUrl: string; +} + +export interface EdBy { + permissions: number; + id: number; + email: string; + plexUsername: string; + username: string; + recoveryLinkExpirationDate: null; + userType: number; + avatar: string; + movieQuotaLimit: null; + movieQuotaDays: null; + tvQuotaLimit: null; + tvQuotaDays: null; + createdAt: Date; + updatedAt: Date; + settings: Settings; + requestCount: number; + displayName: string; +} + +export interface Settings { + id: number; + locale: string; + region: string; + originalLanguage: null; + pgpKey: null; + discordId: string; + pushbulletAccessToken: null; + pushoverApplicationToken: null; + pushoverUserKey: null; + telegramChatId: null; + telegramSendSilently: null; + notificationTypes: NotificationTypes; +} + +export interface NotificationTypes { + discord: number; + email: number; + webpush: number; +} + +export interface ProductionCompany { + id: number; + name: string; + originCountry?: string; + logoPath: string; + displayPriority?: number; +} + +export interface ProductionCountry { + iso_3166_1: string; + name: string; +} + +export interface RelatedVideo { + site: string; + key: string; + name: string; + size: number; + type: string; + url: string; +} + +export interface Releases { + results: Result[]; +} + +export interface Result { + iso_3166_1: string; + release_dates: ReleaseDate[]; +} + +export interface ReleaseDate { + certification: string; + iso_639_1: ISO639_1 | null; + note: Note; + release_date: Date; + type: number; +} + +export enum ISO639_1 { + CS = 'cs', + Empty = '', +} + +export enum Note { + Empty = '', + HBOMax = 'HBO Max', + LosAngelesCalifornia = 'Los Angeles, California', + Starz = 'STARZ', + The4KUHDBluRayDVD = '4K UHD, Blu-ray & DVD', + TheMoreFunStuffVersion = 'The More Fun Stuff Version', + Tvod = 'TVOD', + VOD = 'VOD', +} + +export interface SpokenLanguage { + english_name: string; + iso_639_1: string; + name: string; +} + +export interface WatchProvider { + iso_3166_1: string; + link: string; + buy: ProductionCompany[]; + flatrate: ProductionCompany[]; +} diff --git a/src/modules/overseerr/SearchResult.d.ts b/src/modules/overseerr/SearchResult.d.ts new file mode 100644 index 000000000..89b3d3ade --- /dev/null +++ b/src/modules/overseerr/SearchResult.d.ts @@ -0,0 +1,65 @@ +export interface SearchResult { + page: number; + totalPages: number; + totalResults: number; + results: Result[]; +} + +export interface Result { + id: number; + mediaType: MediaType; + adult?: boolean; + genreIds: number[]; + originalLanguage: OriginalLanguage; + originalTitle?: string; + overview: string; + popularity: number; + releaseDate?: Date; + title?: string; + video?: boolean; + voteAverage: number; + voteCount: number; + backdropPath: null | string; + posterPath: string; + mediaInfo?: MediaInfo; + firstAirDate?: Date; + name?: string; + originCountry?: string[]; + originalName?: string; +} + +export interface MediaInfo { + downloadStatus: any[]; + downloadStatus4k: any[]; + id: number; + mediaType: MediaType; + tmdbId: number; + tvdbId: null; + imdbId: null; + status: number; + status4k: number; + createdAt: Date; + updatedAt: Date; + lastSeasonChange: Date; + mediaAddedAt: Date; + serviceId: number; + serviceId4k: null; + externalServiceId: number; + externalServiceId4k: null; + externalServiceSlug: string; + externalServiceSlug4k: null; + ratingKey: string; + ratingKey4k: null; + seasons: any[]; + plexUrl: string; + serviceUrl: string; +} + +export enum MediaType { + Movie = 'movie', + Tv = 'tv', +} + +export enum OriginalLanguage { + En = 'en', +} diff --git a/src/modules/overseerr/TvShow.d.ts b/src/modules/overseerr/TvShow.d.ts new file mode 100644 index 000000000..f9c3bd64a --- /dev/null +++ b/src/modules/overseerr/TvShow.d.ts @@ -0,0 +1,295 @@ +export interface TvShowResult { + createdBy: CreatedBy[]; + episodeRunTime: number[]; + firstAirDate: Date; + genres: Genre[]; + relatedVideos: RelatedVideo[]; + homepage: string; + id: number; + inProduction: boolean; + languages: string[]; + lastAirDate: Date; + name: string; + networks: Network[]; + numberOfEpisodes: number; + numberOfSeasons: number; + originCountry: string[]; + originalLanguage: string; + originalName: string; + tagline: string; + overview: string; + popularity: number; + productionCompanies: Network[]; + productionCountries: ProductionCountry[]; + contentRatings: ContentRatings; + spokenLanguages: SpokenLanguage[]; + seasons: TvShowResultSeason[]; + status: string; + type: string; + voteAverage: number; + voteCount: number; + backdropPath: string; + lastEpisodeToAir: LastEpisodeToAir; + posterPath: string; + credits: Credits; + externalIds: ExternalIDS; + keywords: Genre[]; + mediaInfo: Media; + watchProviders: WatchProvider[]; +} + +export interface ContentRatings { + results: Result[]; +} + +export interface Result { + iso_3166_1: string; + rating: string; +} + +export interface CreatedBy { + id: number; + credit_id: string; + name: string; + gender: number; + profile_path: string; +} + +export interface Credits { + cast: Cast[]; + crew: Crew[]; +} + +export interface Cast { + character: string; + creditId: string; + id: number; + name: string; + order: number; + gender: number; + profilePath: null | string; +} + +export interface Crew { + creditId: string; + department: string; + id: number; + job: string; + name: string; + gender: number; + profilePath: string; +} + +export interface ExternalIDS { + facebookId: string; + freebaseId: null; + freebaseMid: string; + imdbId: string; + instagramId: string; + tvdbId: number; + tvrageId: number; + twitterId: string; +} + +export interface Genre { + id: number; + name: string; +} + +export interface LastEpisodeToAir { + id: number; + airDate: Date; + episodeNumber: number; + name: string; + overview: string; + productionCode: string; + seasonNumber: number; + showId: number; + voteAverage: number; + stillPath: string; +} + +export interface Request { + id: number; + status: number; + createdAt: Date; + updatedAt: Date; + type: Type; + is4k: boolean; + serverId: null; + profileId: null; + rootFolder: null; + languageProfileId: null; + tags: null; + media: Media; + requestedBy: EdBy; + modifiedBy: EdBy; + seasons: MediaInfoSeason[]; + seasonCount: number; +} + +export interface Media { + downloadStatus: DownloadStatus[]; + downloadStatus4k: any[]; + id: number; + mediaType: Type; + tmdbId: number; + tvdbId: number; + imdbId: null; + status: number; + status4k: number; + createdAt: Date; + updatedAt: Date; + lastSeasonChange: Date; + mediaAddedAt: Date; + serviceId: number; + serviceId4k: null; + externalServiceId: number; + externalServiceId4k: null; + externalServiceSlug: string; + externalServiceSlug4k: null; + ratingKey: string; + ratingKey4k: null; + requests?: Request[]; + issues?: any[]; + seasons: MediaInfoSeason[]; + plexUrl: string; + serviceUrl: string; +} + +export interface EdBy { + permissions: number; + id: number; + email: string; + plexUsername: string; + username: string; + recoveryLinkExpirationDate: null; + userType: number; + avatar: string; + movieQuotaLimit: null; + movieQuotaDays: null; + tvQuotaLimit: null; + tvQuotaDays: null; + createdAt: Date; + updatedAt: Date; + settings: Settings; + requestCount: number; + displayName: string; +} + +export interface Settings { + id: number; + locale: string; + region: string; + originalLanguage: null; + pgpKey: null; + discordId: string; + pushbulletAccessToken: null; + pushoverApplicationToken: null; + pushoverUserKey: null; + telegramChatId: null; + telegramSendSilently: null; + notificationTypes: NotificationTypes; +} + +export interface NotificationTypes { + discord: number; + email: number; + webpush: number; +} + +export interface MediaInfoSeason { + id: number; + seasonNumber: number; + status: number; + status4k?: number; + createdAt: Date; + updatedAt: Date; +} + +export enum Type { + Tv = 'tv', +} + +export interface DownloadStatus { + externalId: number; + estimatedCompletionTime: Date; + mediaType: Type; + size: number; + sizeLeft: number; + status: Status; + timeLeft: string; + title: string; +} + +export enum Status { + Completed = 'completed', + Downloading = 'downloading', +} + +export interface Network { + id: number; + name: Name; + originCountry?: string; + logoPath: LogoPath | null; + displayPriority?: number; +} + +export enum LogoPath { + HbifXPpM55B1FL5WPo7T72VzN78PNG = '/hbifXPpM55B1fL5wPo7t72vzN78.png', + KhiCshsZBdtUUYOr4VLoCtuqCEqPNG = '/khiCshsZBdtUUYOr4VLoCtuqCEq.png', + O9ExgOSLF3OTwR6T3DJOuwOKJgqJpg = '/o9ExgOSLF3OTwR6T3DJOuwOKJgq.jpg', + PEURlLlr8JggOwK53FJ5WdQl05YJpg = '/peURlLlr8jggOwK53fJ5wdQl05y.jpg', + T2YyOv40HZeVlLjYsCSPHnWLk4WJpg = '/t2yyOv40HZeVlLjYsCsPHnWLk4W.jpg', + TBEdFQDwx5LEVr8WpSEXQSIirVqJpg = '/tbEdFQDwx5LEVr8WpSeXQSIirVq.jpg', + The5NyLm42TmCqCMOZFvH4FcoSNKEWJpg = '/5NyLm42TmCqCMOZFvH4fcoSNKEW.jpg', + WwemzKWzjKYJFfCeiB57Q3R4BcmPNG = '/wwemzKWzjKYJFfCeiB57q3r4Bcm.png', +} + +export enum Name { + AmazonVideo = 'Amazon Video', + AppleITunes = 'Apple iTunes', + Channel4 = 'Channel 4', + GooglePlayMovies = 'Google Play Movies', + HouseOfTomorrow = 'House of Tomorrow', + Ivi = 'Ivi', + Netflix = 'Netflix', + Zeppotron = 'Zeppotron', +} + +export interface ProductionCountry { + iso_3166_1: string; + name: string; +} + +export interface RelatedVideo { + site: string; + key: string; + name: string; + size: number; + type: string; + url: string; +} + +export interface TvShowResultSeason { + airDate: Date; + episodeCount: number; + id: number; + name: string; + overview: string; + seasonNumber: number; + posterPath: string; +} + +export interface SpokenLanguage { + englishName: string; + iso_639_1: string; + name: string; +} + +export interface WatchProvider { + iso_3166_1: string; + link: string; + buy: Network[]; + flatrate: Network[]; +} From b489c071770b73cca0aac6c039076e6ef714aaed Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:16:15 +0200 Subject: [PATCH 17/35] :bug: Fix a bug with mediadisplay --- src/modules/common/MediaDisplay.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 8caa4f179..65ba1e266 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -27,6 +27,7 @@ export interface IMedia { seasonNumber?: number; plexUrl?: string; episodeNumber?: number; + [key: string]: any; } const useStyles = createStyles((theme) => ({ @@ -86,7 +87,7 @@ export function MediaDisplay(props: { media: IMedia }) { ? () => window.open(media.plexUrl) : () => { // TODO: implement overseerr media requests - throw new Error('Need to implement media reqests'); + console.log(media); } } > From 70814d0bc699810f9e1eb3bb07eb9b5dd42c9a2f Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 12:16:29 +0200 Subject: [PATCH 18/35] :sparkles: Add Overseerr integration --- .../overseerr/OverseerrMediaDisplay.tsx | 8 +- src/modules/overseerr/RequestModal.tsx | 198 ++++++++++++++++++ 2 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 src/modules/overseerr/RequestModal.tsx diff --git a/src/modules/overseerr/OverseerrMediaDisplay.tsx b/src/modules/overseerr/OverseerrMediaDisplay.tsx index 0af4c31a4..2e160030f 100644 --- a/src/modules/overseerr/OverseerrMediaDisplay.tsx +++ b/src/modules/overseerr/OverseerrMediaDisplay.tsx @@ -1,18 +1,18 @@ import { MediaDisplay } from '../common'; +import { Result } from './SearchResult'; export default function OverseerrMediaDisplay(props: any) { - const { media }: { media: any } = props; + const { media }: { media: Result } = props; return ( ); diff --git a/src/modules/overseerr/RequestModal.tsx b/src/modules/overseerr/RequestModal.tsx new file mode 100644 index 000000000..81b76091c --- /dev/null +++ b/src/modules/overseerr/RequestModal.tsx @@ -0,0 +1,198 @@ +import { Alert, Checkbox, createStyles, Group, LoadingOverlay, Stack, Table } from '@mantine/core'; +import { openConfirmModal } from '@mantine/modals'; +import { showNotification, updateNotification } from '@mantine/notifications'; +import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import { useColorTheme } from '../../tools/color'; +import { MovieResult } from './Movie.d'; +import { MediaType, Result } from './SearchResult.d'; +import { TvShowResult, TvShowResultSeason } from './TvShow.d'; + +interface RequestModalProps { + base: Result; +} + +const useStyles = createStyles((theme) => ({ + rowSelected: { + backgroundColor: + theme.colorScheme === 'dark' + ? theme.fn.rgba(theme.colors[theme.primaryColor][7], 0.2) + : theme.colors[theme.primaryColor][0], + }, +})); + +export default function RequestModal({ base }: RequestModalProps) { + const [result, setResult] = useState(); + + useEffect(() => { + // Use the overseerr API get the media info. + axios.get(`/api/modules/overseerr/${base.id}?type=${base.mediaType}`).then((res) => { + setResult(res.data); + }); + }, [base]); + + const { secondaryColor } = useColorTheme(); + if (!result) { + return ; + } + return base.mediaType === 'movie' ? ( + + ) : ( + + ); +} + +function MovieRequestModal({ result }: { result: MovieResult }) { + const { secondaryColor } = useColorTheme(); + openConfirmModal({ + title: ( + + + Ask for {result.title} + + ), + radius: 'lg', + labels: { confirm: 'Request', cancel: 'Cancel' }, + onConfirm: () => { + askForMedia(MediaType.Movie, result.id, result.title); + }, + size: 'lg', + children: ( + + } + title="Using API key" + color={secondaryColor} + radius="md" + variant="filled" + > + This request will be automatically approved + + + ), + }); + return null; +} + +function TvRequestModal({ result }: { result: TvShowResult }) { + const [selection, setSelection] = useState(result.seasons); + const { classes, cx } = useStyles(); + + const toggleRow = (container: TvShowResultSeason) => + setSelection((current: TvShowResultSeason[]) => + current.includes(container) ? current.filter((c) => c !== container) : [...current, container] + ); + const toggleAll = () => + setSelection((current: any) => + current.length === result.seasons.length ? [] : result.seasons.map((c) => c) + ); + + const rows = result.seasons.map((element) => { + const selected = selection.includes(element); + return ( + + + toggleRow(element)} + transitionDuration={0} + /> + + {element.name} + {element.episodeCount} + + ); + }); + const { secondaryColor } = useColorTheme(); + + openConfirmModal({ + title: ( + + + Ask for {result.name} + + ), + radius: 'lg', + labels: { confirm: 'Request', cancel: 'Cancel' }, + confirmProps: { + disabled: selection.length === 0, + }, + onConfirm: () => { + askForMedia( + MediaType.Tv, + result.id, + result.name, + selection.map((s) => s.seasonNumber) + ); + }, + size: 'lg', + children: ( + + } + title="Using API key" + color={secondaryColor} + radius="md" + variant="filled" + > + This request will be automatically approved + + + + + + + + + + + {rows} +
Tick the seasons that you want to be downloaded
+ 0 && selection.length !== result.seasons.length} + transitionDuration={0} + /> + SeasonNumber of episodes
+
+ ), + }); + return null; +} + +function askForMedia(type: MediaType, id: number, name: string, seasons?: number[]) { + showNotification({ + title: 'Request', + id: id.toString(), + message: `Requesting media ${name}`, + color: 'orange', + loading: true, + autoClose: false, + disallowClose: true, + icon: , + }); + axios + .post(`/api/modules/overseerr/${id}`, { type, seasons }) + .then(() => { + updateNotification({ + id: id.toString(), + title: '', + color: 'green', + message: ` ${name} requested`, + icon: , + autoClose: 2000, + }); + }) + .catch((err) => { + updateNotification({ + id: id.toString(), + color: 'red', + title: 'There was an error', + message: err.message, + autoClose: 2000, + }); + }); +} From 03dd4b33acfd61736737c79e12148ee9a754a83d Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 17:19:39 +0200 Subject: [PATCH 19/35] :package: Add Mantine Modal --- package.json | 1 + yarn.lock | 33 ++++----------------------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 434488067..818fbc35b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@mantine/dropzone": "^5.0.2", "@mantine/form": "^5.0.2", "@mantine/hooks": "^5.0.2", + "@mantine/modals": "^5.0.3", "@mantine/next": "^5.0.2", "@mantine/notifications": "^5.0.2", "@mantine/prism": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index e489407ef..943f33277 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -1141,15 +1144,6 @@ __metadata: languageName: node linkType: hard -"@mantine/form@npm:^4.2.12": - version: 4.2.12 - resolution: "@mantine/form@npm:4.2.12" - peerDependencies: - react: ">=16.8.0" - checksum: 851576d75d6a9db737e1a985db60c6eaeb0fcd019fba3eb27e6bce1dfe915fd6dc138d7954c56228e9c6a6f80e4678e507159476f46436537cca9ea2388c1de7 - languageName: node - linkType: hard - "@mantine/form@npm:^5.0.2": version: 5.0.2 resolution: "@mantine/form@npm:5.0.2" @@ -1161,15 +1155,6 @@ __metadata: languageName: node linkType: hard -"@mantine/hooks@npm:^4.2.12": - version: 4.2.12 - resolution: "@mantine/hooks@npm:4.2.12" - peerDependencies: - react: ">=16.8.0" - checksum: 09dbbbf1d90c8a0b0ea02394454af0de9fcc975668a80ff5460161493509d55603f2e39feac2bb46019db673a576617f2ffb111f0373699ad5462e7c2da9bea6 - languageName: node - linkType: hard - "@mantine/hooks@npm:^5.0.2": version: 5.0.2 resolution: "@mantine/hooks@npm:5.0.2" @@ -1193,17 +1178,6 @@ __metadata: languageName: node linkType: hard -"@mantine/next@npm:^4.2.12": - version: 4.2.12 - resolution: "@mantine/next@npm:4.2.12" - dependencies: - klona: ^2.0.5 - peerDependencies: - react: ">=16.8.0" - checksum: db08b33a0e95a20fb4dffc22d58be7f5ece6bbd824b40fa1bccee310587b932f73492bdef1de8d8023777a8bf6d1c7fe76594dba319055b8865543b53f3c60e0 - languageName: node - linkType: hard - "@mantine/next@npm:^5.0.2": version: 5.0.2 resolution: "@mantine/next@npm:5.0.2" @@ -4675,6 +4649,7 @@ __metadata: "@mantine/dropzone": ^5.0.2 "@mantine/form": ^5.0.2 "@mantine/hooks": ^5.0.2 + "@mantine/modals": ^5.0.3 "@mantine/next": ^5.0.2 "@mantine/notifications": ^5.0.2 "@mantine/prism": ^5.0.0 From 68d1068059370d7ce6d945eef65f9ed90c9d5f59 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 17:20:34 +0200 Subject: [PATCH 20/35] :arrow_up: Migration to Mantine v5.0 in Popover --- src/modules/search/SearchModule.tsx | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/modules/search/SearchModule.tsx b/src/modules/search/SearchModule.tsx index a2836b2ca..29cbb6e3e 100644 --- a/src/modules/search/SearchModule.tsx +++ b/src/modules/search/SearchModule.tsx @@ -1,7 +1,7 @@ -import { Kbd, createStyles, Autocomplete } from '@mantine/core'; +import { Kbd, createStyles, Autocomplete, Popover, ScrollArea, Divider } from '@mantine/core'; import { useDebouncedValue, useHotkeys } from '@mantine/hooks'; import { useForm } from '@mantine/form'; -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { IconSearch as Search, IconBrandYoutube as BrandYoutube, @@ -144,14 +144,16 @@ export default function SearchBar(props: any) { 0 && opened} position="bottom" - placement="start" + withArrow + withinPortal + shadow="md" radius="md" - trapFocus={false} + trapFocus transition="pop-bottom-right" - onFocusCapture={() => setOpened(true)} - onBlurCapture={() => setOpened(false)} - target={ + > + setOpened(true)} autoFocus variant="filled" data={autocompleteData} @@ -172,16 +174,18 @@ export default function SearchBar(props: any) { {...props} {...form.getInputProps('query')} /> - } - > - - {OverseerrResults.slice(0, 5).map((result, index) => ( - - - {index < OverseerrResults.length - 1 && } - - ))} - + + + setOpened(false)}> + + {OverseerrResults.slice(0, 5).map((result, index) => ( + + + {index < OverseerrResults.length - 1 && } + + ))} + + ); From 67f19b518671b39db063134ab410024a33615ade Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 7 Aug 2022 17:20:59 +0200 Subject: [PATCH 21/35] :lipstick: Linting --- src/components/AppShelf/AddAppShelfItem.tsx | 1 - src/modules/common/MediaDisplay.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 27777f08d..621ba2d54 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -18,7 +18,6 @@ import { Tooltip, } from '@mantine/core'; import { useForm } from '@mantine/form'; -import { useDebouncedValue } from '@mantine/hooks'; import { IconApps } from '@tabler/icons'; import { useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 7b34c2584..e2d2ab78c 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -9,6 +9,7 @@ import { ScrollArea, createStyles, Stack, + Button, } from '@mantine/core'; import { useMediaQuery } from '@mantine/hooks'; import { IconLink } from '@tabler/icons'; From 1741829761bced08093db0c3763957009fad6f47 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:44:20 +0200 Subject: [PATCH 22/35] :fire: Remove tryRequest page --- src/pages/tryrequest.tsx | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/pages/tryrequest.tsx diff --git a/src/pages/tryrequest.tsx b/src/pages/tryrequest.tsx deleted file mode 100644 index 0cc195a2a..000000000 --- a/src/pages/tryrequest.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import axios from 'axios'; -import { useEffect, useState } from 'react'; -import RequestModal from '../modules/overseerr/RequestModal'; - -export default function TryRequest(props: any) { - const [OverseerrResults, setOverseerrResults] = useState([]); - useEffect(() => { - // Use the overseerr API get the media info. - axios - .get('/api/modules/overseerr?query=Lucifer') - .then((res) => { - setOverseerrResults(res.data.results); - }) - .catch((err) => { - throw err; - }); - }, [props]); - if (!OverseerrResults || OverseerrResults.length === 0) { - return null; - } - return ; -} From 04874e69f2000aa6a92dfa94a0e9f6d0585f3226 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:44:35 +0200 Subject: [PATCH 23/35] :bug: Fix module wrapper hover bug --- src/modules/moduleWrapper.tsx | 44 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/modules/moduleWrapper.tsx b/src/modules/moduleWrapper.tsx index 3ae65a0b2..d6e08506b 100644 --- a/src/modules/moduleWrapper.tsx +++ b/src/modules/moduleWrapper.tsx @@ -13,6 +13,7 @@ import { import { useHover } from '@mantine/hooks'; import { IconAdjustments } from '@tabler/icons'; import { motion } from 'framer-motion'; +import { useState } from 'react'; import { useConfig } from '../tools/state'; import { IModule } from './ModuleTypes'; @@ -148,7 +149,7 @@ export function ModuleWrapper(props: any) { // Remove 'Module' from enabled modules titles const isShown = enabledModules[module.title]?.enabled ?? false; //TODO: fix the hover problem - const { hovered, ref } = useHover(); + const [hovering, setHovering] = useState(false); if (!isShown) { return null; @@ -158,7 +159,6 @@ export function ModuleWrapper(props: any) { ); } @@ -185,6 +192,7 @@ export function ModuleMenu(props: any) { <> {module.options && ( - - - - - - - + + + + Settings From 9a53f5d1eeb60efe8db5a77b24fb4a9748477b9c Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:44:58 +0200 Subject: [PATCH 24/35] :zap: Improve MediaDisplay overseerr --- src/modules/common/MediaDisplay.tsx | 248 ++++++++++-------- .../overseerr/OverseerrMediaDisplay.tsx | 19 -- src/modules/overseerr/OverseerrModule.tsx | 2 +- 3 files changed, 140 insertions(+), 129 deletions(-) delete mode 100644 src/modules/overseerr/OverseerrMediaDisplay.tsx diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index e2d2ab78c..2de1707b0 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -1,26 +1,18 @@ -import { - Image, - Group, - Title, - Badge, - Text, - ActionIcon, - Anchor, - ScrollArea, - createStyles, - Stack, - Button, -} from '@mantine/core'; -import { useMediaQuery } from '@mantine/hooks'; -import { IconLink } from '@tabler/icons'; +import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core'; +import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons'; +import { useState } from 'react'; import { useConfig } from '../../tools/state'; import { serviceItem } from '../../tools/types'; +import { RequestModal } from '../overseerr/RequestModal'; +import { Result } from '../overseerr/SearchResult'; export interface IMedia { overview: string; imdbId?: any; artist?: string; - title: string; + title?: string; + type: 'movie' | 'tvshow' | 'book' | 'music' | 'overseer'; + episodetitle?: string; voteAverage?: string; poster?: string; genres: string[]; @@ -30,83 +22,24 @@ export interface IMedia { [key: string]: any; } -const useStyles = createStyles((theme) => ({ - overview: { - [theme.fn.largerThan('sm')]: { - width: 400, - }, - }, -})); - -export function MediaDisplay(props: { media: IMedia }) { - const { media }: { media: IMedia } = props; - const { classes, cx } = useStyles(); - const phone = useMediaQuery('(min-width: 800px)'); +export function OverseerrMediaDisplay(props: any) { + const { media }: { media: Result } = props; return ( - - - {media.poster && ( - {media.title} - )} - - - {media.title} - {media.artist && New release from {media.artist}} - {(media.episodeNumber || media.seasonNumber) && ( - - Season {media.seasonNumber}{' '} - {media.episodeNumber && `episode ${media.episodeNumber}`} - - )} - - {media.voteAverage && ( - - )} - {media.imdbId && ( - - - - - - )} - - - {media.overview} - - {media.genres.slice(-5).map((genre: string, i: number) => ( - - {genre} - - ))} - - - - + ); } @@ -127,11 +60,14 @@ export function ReadarrMediaDisplay(props: any) { return ( ); @@ -154,6 +90,7 @@ export function LidarrMediaDisplay(props: any) { return ( image.coverType === 'poster'); - // Return a movie poster containting the title and the description return ( image.coverType === 'poster')?.url ?? undefined, + voteAverage: media.ratings.tmdb.value.toString() ?? undefined, + imdbId: media.imdbId ?? undefined, + type: 'movie', }} /> ); @@ -190,14 +128,106 @@ export function SonarrMediaDisplay(props: any) { return ( ); } + +export function MediaDisplay({ media }: { media: IMedia }) { + const [opened, setOpened] = useState(false); + return ( + + + + + + {media.title} + + + {media.type === 'tvshow' && ( + + s{media.seasonNumber}e{media.episodeNumber} - {media.episodetitle} + + )} + {media.type === 'music' && ( + + {media.artist} + + )} + {media.type === 'movie' && ( + + Radarr + + )} + {media.type === 'book' && ( + + Readarr + + )} + {media.genres.slice(0, 2).map((genre) => ( + + {genre} + + ))} + + + {media.overview} + + + + {media.plexUrl && ( + + )} + {media.imdbId && ( + + )} + {media.type === 'overseer' && ( + <> + + + + )} + + + + ); +} diff --git a/src/modules/overseerr/OverseerrMediaDisplay.tsx b/src/modules/overseerr/OverseerrMediaDisplay.tsx deleted file mode 100644 index 2e160030f..000000000 --- a/src/modules/overseerr/OverseerrMediaDisplay.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { MediaDisplay } from '../common'; -import { Result } from './SearchResult'; - -export default function OverseerrMediaDisplay(props: any) { - const { media }: { media: Result } = props; - return ( - - ); -} diff --git a/src/modules/overseerr/OverseerrModule.tsx b/src/modules/overseerr/OverseerrModule.tsx index 9ba95c6d7..4f8e44802 100644 --- a/src/modules/overseerr/OverseerrModule.tsx +++ b/src/modules/overseerr/OverseerrModule.tsx @@ -1,6 +1,6 @@ import { IconEyeglass } from '@tabler/icons'; +import { OverseerrMediaDisplay } from '../common'; import { IModule } from '../ModuleTypes'; -import OverseerrMediaDisplay from './OverseerrMediaDisplay'; export const OverseerrModule: IModule = { title: 'Overseerr', From f9caf6ef26e83eaae9ff9717a80d4023e5bef583 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:45:12 +0200 Subject: [PATCH 25/35] :coffin: Remove allowTransparency from dashdot --- src/modules/dashdot/DashdotModule.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/dashdot/DashdotModule.tsx b/src/modules/dashdot/DashdotModule.tsx index 1cb9bc716..254ddadcc 100644 --- a/src/modules/dashdot/DashdotModule.tsx +++ b/src/modules/dashdot/DashdotModule.tsx @@ -237,7 +237,6 @@ export function DashdotComponent() { : '' }`} frameBorder="0" - allowTransparency /> ))} From b430e24cdb1cd2d9e991307441b89ee31283da2b Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:45:36 +0200 Subject: [PATCH 26/35] :pencil2: Fix request Modal --- src/modules/overseerr/RequestModal.tsx | 148 +++++++++++++++---------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/src/modules/overseerr/RequestModal.tsx b/src/modules/overseerr/RequestModal.tsx index 81b76091c..e25637933 100644 --- a/src/modules/overseerr/RequestModal.tsx +++ b/src/modules/overseerr/RequestModal.tsx @@ -1,8 +1,19 @@ -import { Alert, Checkbox, createStyles, Group, LoadingOverlay, Stack, Table } from '@mantine/core'; +import { + Alert, + Button, + Checkbox, + createStyles, + Group, + LoadingOverlay, + Modal, + Stack, + Table, +} from '@mantine/core'; import { openConfirmModal } from '@mantine/modals'; import { showNotification, updateNotification } from '@mantine/notifications'; import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons'; import axios from 'axios'; +import Consola from 'consola'; import { useEffect, useState } from 'react'; import { useColorTheme } from '../../tools/color'; import { MovieResult } from './Movie.d'; @@ -11,6 +22,8 @@ import { TvShowResult, TvShowResultSeason } from './TvShow.d'; interface RequestModalProps { base: Result; + opened: boolean; + setOpened: (opened: boolean) => void; } const useStyles = createStyles((theme) => ({ @@ -22,43 +35,53 @@ const useStyles = createStyles((theme) => ({ }, })); -export default function RequestModal({ base }: RequestModalProps) { +export function RequestModal({ base, opened, setOpened }: RequestModalProps) { const [result, setResult] = useState(); - - useEffect(() => { - // Use the overseerr API get the media info. + const { secondaryColor } = useColorTheme(); + function getResults(base: Result) { axios.get(`/api/modules/overseerr/${base.id}?type=${base.mediaType}`).then((res) => { setResult(res.data); }); - }, [base]); - - const { secondaryColor } = useColorTheme(); - if (!result) { - return ; + } + if (opened && !result) { + getResults(base); + } + if (!result || !opened) { + return null; } return base.mediaType === 'movie' ? ( - + ) : ( - + ); } -function MovieRequestModal({ result }: { result: MovieResult }) { +export function MovieRequestModal({ + result, + opened, + setOpened, +}: { + result: MovieResult; + opened: boolean; + setOpened: (opened: boolean) => void; +}) { const { secondaryColor } = useColorTheme(); - openConfirmModal({ - title: ( - - - Ask for {result.title} - - ), - radius: 'lg', - labels: { confirm: 'Request', cancel: 'Cancel' }, - onConfirm: () => { - askForMedia(MediaType.Movie, result.id, result.title); - }, - size: 'lg', - children: ( + return ( + setOpened(false)} + radius="lg" + size="lg" + trapFocus + zIndex={150} + withinPortal + opened={opened} + title={ + + + Ask for {result.title} + + } + > } @@ -69,13 +92,30 @@ function MovieRequestModal({ result }: { result: MovieResult }) { > This request will be automatically approved + + + + - ), - }); - return null; + + ); } -function TvRequestModal({ result }: { result: TvShowResult }) { +export function TvRequestModal({ + result, + opened, + setOpened, +}: { + result: TvShowResult; + opened: boolean; + setOpened: (opened: boolean) => void; +}) { const [selection, setSelection] = useState(result.seasons); const { classes, cx } = useStyles(); @@ -107,28 +147,8 @@ function TvRequestModal({ result }: { result: TvShowResult }) { }); const { secondaryColor } = useColorTheme(); - openConfirmModal({ - title: ( - - - Ask for {result.name} - - ), - radius: 'lg', - labels: { confirm: 'Request', cancel: 'Cancel' }, - confirmProps: { - disabled: selection.length === 0, - }, - onConfirm: () => { - askForMedia( - MediaType.Tv, - result.id, - result.name, - selection.map((s) => s.seasonNumber) - ); - }, - size: 'lg', - children: ( + return ( + setOpened(false)} radius="lg" size="lg" opened={opened}> } @@ -157,13 +177,29 @@ function TvRequestModal({ result }: { result: TvShowResult }) { {rows} + + + + - ), - }); - return null; + + ); } function askForMedia(type: MediaType, id: number, name: string, seasons?: number[]) { + Consola.info(`Requesting ${type} ${id} ${name}`); showNotification({ title: 'Request', id: id.toString(), From 1e69e3a2b0eeceb52f4720cb8424ac22c18e89dc Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:45:54 +0200 Subject: [PATCH 27/35] :bug: Fix onBlurCapture in the Dropdown of overseerr --- src/modules/search/SearchModule.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/search/SearchModule.tsx b/src/modules/search/SearchModule.tsx index 29cbb6e3e..02a39bfba 100644 --- a/src/modules/search/SearchModule.tsx +++ b/src/modules/search/SearchModule.tsx @@ -13,7 +13,7 @@ import { showNotification } from '@mantine/notifications'; import { useConfig } from '../../tools/state'; import { IModule } from '../ModuleTypes'; import { OverseerrModule } from '../overseerr'; -import OverseerrMediaDisplay from '../overseerr/OverseerrMediaDisplay'; +import { OverseerrMediaDisplay } from '../common'; const useStyles = createStyles((theme) => ({ hide: { @@ -148,8 +148,9 @@ export default function SearchBar(props: any) { withinPortal shadow="md" radius="md" + zIndex={100} trapFocus - transition="pop-bottom-right" + transition="pop-top-right" > - setOpened(false)}> - + setOpened(false)}> + {OverseerrResults.slice(0, 5).map((result, index) => ( From 772fe7622d08de1deac0905d5502aa0d67ce5ba8 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:46:14 +0200 Subject: [PATCH 28/35] :bug: Fix bug with Downloadmodule width --- src/modules/downloads/DownloadsModule.tsx | 30 +++++++++-------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/modules/downloads/DownloadsModule.tsx b/src/modules/downloads/DownloadsModule.tsx index e27f28195..769a8eeb3 100644 --- a/src/modules/downloads/DownloadsModule.tsx +++ b/src/modules/downloads/DownloadsModule.tsx @@ -189,23 +189,17 @@ export default function DownloadComponent() { }); return ( - - - {rows.length > 0 ? ( - - {ths} - {rows} -
- ) : ( -
- -
- )} -
-
+ + {rows.length > 0 ? ( + + {ths} + {rows} +
+ ) : ( +
+ +
+ )} +
); } From 60fc6732b8aa9b1d3082a41bf0bb02b49a3cec9f Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:47:15 +0200 Subject: [PATCH 29/35] :memo: Add examples for JSON formats I could possibly turn these into type declarations with some online parser but at the moment it stays here for developpment purposes --- src/modules/common/examples/book.json | 57 + src/modules/common/examples/movie.json | 70 + .../common/examples/multiplemovies.json | 3324 +++++++++++++++++ .../common/examples/multipletvshows.json | 409 ++ src/modules/common/examples/music.json | 832 +++++ src/modules/common/examples/request.json | 47 + src/modules/common/examples/tvshow.json | 110 + 7 files changed, 4849 insertions(+) create mode 100644 src/modules/common/examples/book.json create mode 100644 src/modules/common/examples/movie.json create mode 100644 src/modules/common/examples/multiplemovies.json create mode 100644 src/modules/common/examples/multipletvshows.json create mode 100644 src/modules/common/examples/music.json create mode 100644 src/modules/common/examples/request.json create mode 100644 src/modules/common/examples/tvshow.json diff --git a/src/modules/common/examples/book.json b/src/modules/common/examples/book.json new file mode 100644 index 000000000..9548309a4 --- /dev/null +++ b/src/modules/common/examples/book.json @@ -0,0 +1,57 @@ +{ + "title": "Mika in Real Life", + "authorTitle": "jean, emiko Mika in Real Life", + "seriesTitle": "", + "disambiguation": "", + "authorId": 1, + "foreignBookId": "93584169", + "titleSlug": "93584169", + "monitored": true, + "anyEditionOk": false, + "ratings": { + "votes": 149, + "value": 4.15, + "popularity": 618.35 + }, + "releaseDate": "2022-08-09T00:00:00Z", + "pageCount": 384, + "genres": [ + "fiction", + "romance", + "contemporary", + "adult", + "adult-fiction", + "chick-lit", + "womens-fiction", + "asian-literature", + "family", + "lgbt" + ], + "images": [ + { + "url": "/MediaCover/Books/1/cover.jpg?lastWrite=637899714580000000", + "coverType": "cover", + "extension": ".jpg" + } + ], + "links": [ + { + "url": "https://www.goodreads.com/work/editions/93584169", + "name": "Goodreads Editions" + }, + { + "url": "https://www.goodreads.com/book/show/59430548-mika-in-real-life", + "name": "Goodreads Book" + } + ], + "statistics": { + "bookFileCount": 0, + "bookCount": 0, + "totalBookCount": 1, + "sizeOnDisk": 0, + "percentOfBooks": 0 + }, + "added": "2022-08-07T20:48:09Z", + "grabbed": false, + "id": 1 +} \ No newline at end of file diff --git a/src/modules/common/examples/movie.json b/src/modules/common/examples/movie.json new file mode 100644 index 000000000..594b14c73 --- /dev/null +++ b/src/modules/common/examples/movie.json @@ -0,0 +1,70 @@ +{ + "title": "The Tunnel to Summer, the Exit of Goodbyes", + "originalTitle": "夏へのトンネル、さよならの出口", + "originalLanguage": { + "id": 8, + "name": "Japanese" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 1, + "title": "Natsu e no Tunnel, Sayonara no Deguchi", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "tunnel to summer exit goodbyes", + "sizeOnDisk": 0, + "status": "announced", + "overview": "Tono Kaoru heard a rumor: The laws of space and time mean nothing to the Urashima Tunnel. If you find it, walk through and you'll find your heart's desire on the other side...in exchange for years of your own life. On the night Kaoru just so happens to find himself standing in front of a tunnel that looks suspiciously like the one the rumor describes, he finds himself thinking of Karen, the sister he lost in an accident five years ago. To Kaoru's surprise, he's been followed by the new transfer student Anzu Hanaki, who promises to help him experiment with the mysterious tunnel--but what does she want from Kaoru in exchange? And what will he have left to give, after the tunnel's done with him?", + "inCinemas": "2022-09-09T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/3x5gc6dHsfNqZryipu159IALEPH.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/zO3QSYs858SqiapafD7iJp17KVD.jpg" + } + ], + "website": "https://natsuton.com/", + "year": 2022, + "hasFile": false, + "youTubeTrailerId": "", + "studio": "Pony Canyon", + "path": "/data/Library/Movies/The Tunnel to Summer, the Exit of Goodbyes (2022)", + "qualityProfileId": 4, + "monitored": true, + "minimumAvailability": "announced", + "isAvailable": true, + "folderName": "/data/Library/Movies/The Tunnel to Summer, the Exit of Goodbyes (2022)", + "runtime": 0, + "cleanTitle": "thetunneltosummerexitgoodbyes", + "imdbId": "tt17382524", + "tmdbId": 916192, + "titleSlug": "916192", + "genres": [ + "Animation", + "Drama", + "Mystery" + ], + "tags": [], + "added": "2022-07-05T07:50:42Z", + "ratings": { + "tmdb": { + "votes": 0, + "value": 0, + "type": "user" + } + }, + "id": 1 +} \ No newline at end of file diff --git a/src/modules/common/examples/multiplemovies.json b/src/modules/common/examples/multiplemovies.json new file mode 100644 index 000000000..d1f8dd71d --- /dev/null +++ b/src/modules/common/examples/multiplemovies.json @@ -0,0 +1,3324 @@ +[ + { + "title": "Sword Art Online the Movie -Progressive- Aria of a Starless Night", + "originalTitle": "劇場版 ソードアート・オンライン-プログレッシブ- 星なき夜のアリア", + "originalLanguage": { + "id": 8, + "name": "Japanese" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Gekijō-ban Sword Art Online: Progressive Hoshi Naki Yoru no Aria", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1749 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "SAO Progressive", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1751 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Gekijouban Sword Art Online: Progressive - Hoshinaki Yoru no Ari", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1752 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Sōdo Āto Onrain", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1753 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Sword Art Online the Movie: The Aria of the Night Without Stars", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1754 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "극장판 소드 아트 온라인 -프로그레시브-: 별 없는 밤의 아리아", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1755 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "소드 아트 온라인 프로그레시브", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1756 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Sword Art Online: Progressive - Aria of a Starless Night", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1757 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "刀劍神域劇場版 - PROGRESSIVE - 無星夜的詠嘆調", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1758 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "ซอร์ดอาร์ตออนไลน์ โปรเกรสซีฟ เดอะมูฟวี่", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 28, + "name": "Thai" + }, + "id": 1759 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "剧场版 刀剑神域 进击篇 无星之夜的咏叹调", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1760 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Sword Art Online Progressive: Ária de Uma Noite Sem Estrelas", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1761 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "刀剑神域 进击篇 无星夜的咏叹调", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1776 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "Sword Art Online Progressive Movie: Hoshi Naki Yoru no Aria", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1788 + }, + { + "sourceType": "tmdb", + "movieId": 170, + "title": "刀剑神域:无星之夜的咏叹调", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1826 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "sword art online movie progressive aria starless night", + "sizeOnDisk": 0, + "status": "released", + "overview": "One month after Kayaba Akihiko's game of death began, the death toll continues to rise, two thousand players having already lost their lives to the ultra-difficult VRMMO world of Sword Art Online. On the day of the strategy meeting to plan out the first-floor boss battle, Kirito, a solo player who vows to fight alone to get stronger, runs into a rare, high-level female player. She gracefully dispatches powerful monsters with a single rapier that flashes like a shooting star in the night...", + "inCinemas": "2021-10-30T00:00:00Z", + "physicalRelease": "2022-07-08T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/yD9RhgIVydQNBK7OLEbCWYcWMUd.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/2kvl6lcgoyAaf8cSRkzxE611u6T.jpg" + } + ], + "website": "https://sao-p.net", + "year": 2021, + "hasFile": false, + "youTubeTrailerId": "Jf6RyZkuVco", + "studio": "Aniplex", + "path": "/movies/Sword Art Online the Movie -Progressive- Aria of a Starless Night (2021)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Sword Art Online the Movie -Progressive- Aria of a Starless Night (2021)", + "runtime": 97, + "cleanTitle": "swordartonlinemovieprogressiveariastarlessnight", + "imdbId": "tt13424422", + "tmdbId": 761898, + "titleSlug": "761898", + "certification": "PG-13", + "genres": [ + "Animation", + "Action", + "Fantasy" + ], + "tags": [], + "added": "2022-07-04T17:05:05Z", + "ratings": { + "imdb": { + "votes": 1048, + "value": 7.1, + "type": "user" + }, + "tmdb": { + "votes": 82, + "value": 8.341, + "type": "user" + } + }, + "collection": { + "name": "Sword Art Online the Movie -Progressive-", + "tmdbId": 893713, + "images": [] + }, + "id": 170 + }, + { + "title": "Jujutsu Kaisen 0", + "originalTitle": "劇場版 呪術廻戦 0", + "originalLanguage": { + "id": 8, + "name": "Japanese" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 164, + "title": "Gekijō-ban Jujutsu Kaisen 0", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1672 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "Gekijou-ban Jujutsu Kaisen 0", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1673 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "극장판 주술회전 0", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1674 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "Jujutsu Kaisen 0: The Movie", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1675 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "呪術廻戦 0", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1676 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "JUJUTSU KAISEN 0 the Movie", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1677 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "劇場版 咒術迴戰 0", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1678 + }, + { + "sourceType": "tmdb", + "movieId": 164, + "title": "Jujutsu Kaisen 0: Film", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 12, + "name": "Polish" + }, + "id": 1845 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "jujutsu kaisen 0", + "sizeOnDisk": 0, + "status": "inCinemas", + "overview": "Yuta Okkotsu is a nervous high school student who is suffering from a serious problem—his childhood friend Rika has turned into a curse and won't leave him alone. Since Rika is no ordinary curse, his plight is noticed by Satoru Gojo, a teacher at Jujutsu High, a school where fledgling exorcists learn how to combat curses. Gojo convinces Yuta to enroll, but can he learn enough in time to confront the curse that haunts him?", + "inCinemas": "2021-12-24T00:00:00Z", + "physicalRelease": "2022-09-21T00:00:00Z", + "digitalRelease": "2022-09-21T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/3pTwMUEavTzVOh6yLN0aEwR7uSy.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/t9K8ycUBCplWiICDOKRNRYcEH9e.jpg" + } + ], + "website": "", + "year": 2021, + "hasFile": false, + "youTubeTrailerId": "2docezZl574", + "studio": "Toho", + "path": "/movies/Jujutsu Kaisen 0 (2021)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": false, + "folderName": "/movies/Jujutsu Kaisen 0 (2021)", + "runtime": 105, + "cleanTitle": "jujutsukaisen0", + "imdbId": "tt14331144", + "tmdbId": 810693, + "titleSlug": "810693", + "certification": "PG-13", + "genres": [ + "Animation", + "Action", + "Fantasy" + ], + "tags": [], + "added": "2022-06-27T13:00:43Z", + "ratings": { + "imdb": { + "votes": 11063, + "value": 7.9, + "type": "user" + }, + "tmdb": { + "votes": 241, + "value": 7.8, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 72, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 98, + "type": "user" + } + }, + "id": 164 + }, + { + "title": "Morbius", + "originalTitle": "Morbius", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 113, + "title": "Morbiuss", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1484 + }, + { + "sourceType": "tmdb", + "movieId": 113, + "title": "暗夜博士:莫比亚斯", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1600 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "morbius", + "sizeOnDisk": 2130913775, + "status": "released", + "overview": "Dangerously ill with a rare blood disorder, and determined to save others suffering his same fate, Dr. Michael Morbius attempts a desperate gamble. What at first appears to be a radical success soon reveals itself to be a remedy potentially worse than the disease.", + "inCinemas": "2022-03-30T00:00:00Z", + "physicalRelease": "2022-06-14T00:00:00Z", + "digitalRelease": "2022-05-14T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/6JjfSchsU6daXk2AKX8EEBjO3Fm.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/gG9fTyDL03fiKnOpf2tr01sncnt.jpg" + } + ], + "website": "https://www.morbius.movie/", + "year": 2022, + "hasFile": true, + "youTubeTrailerId": "wG2TjtueeSU", + "studio": "Columbia Pictures", + "path": "/movies/Morbius (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Morbius (2022)", + "runtime": 105, + "cleanTitle": "morbius", + "imdbId": "tt5108870", + "tmdbId": 526896, + "titleSlug": "526896", + "certification": "PG-13", + "genres": [ + "Action", + "Science Fiction", + "Fantasy" + ], + "tags": [], + "added": "2022-02-11T19:39:55Z", + "ratings": { + "imdb": { + "votes": 92720, + "value": 5.2, + "type": "user" + }, + "tmdb": { + "votes": 2174, + "value": 6.4, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 35, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 16, + "type": "user" + } + }, + "movieFile": { + "movieId": 113, + "relativePath": "Morbius (2022) WEBRip-1080p.mp4", + "path": "/movies/Morbius (2022)/Morbius (2022) WEBRip-1080p.mp4", + "size": 2130913775, + "dateAdded": "2022-05-17T03:04:01Z", + "sceneName": "Morbius.2022.1080p.WEBRip.x264-RARBG", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 15, + "name": "WEBRip-1080p", + "source": "webrip", + "resolution": 1080, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 224000, + "audioChannels": 5.1, + "audioCodec": "AAC", + "audioLanguages": "eng", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 2498238, + "videoCodec": "x264", + "videoDynamicRangeType": "", + "videoFps": 23.976, + "resolution": "1920x800", + "runTime": "1:44:09", + "scanType": "Progressive", + "subtitles": "" + }, + "originalFilePath": "Morbius.2022.1080p.WEBRip.x264-RARBG/Morbius.2022.1080p.WEBRip.x264-RARBG.mp4", + "qualityCutoffNotMet": false, + "languages": [ + { + "id": 1, + "name": "English" + } + ], + "releaseGroup": "RARBG", + "edition": "", + "id": 219 + }, + "id": 113 + }, + { + "title": "Rise", + "originalTitle": "En corps", + "originalLanguage": { + "id": 2, + "name": "French" + }, + "alternateTitles": [], + "secondaryYearSourceId": 0, + "sortTitle": "rise", + "sizeOnDisk": 0, + "status": "released", + "overview": "Elise thought she had the perfect life: an ideal boyfriend and a promising career as a ballet dancer. It all falls apart the day she catches him cheating on her with her stage backup; and after she suffers an injury on stage, it seems like she might not be able to dance ever again. A heartwarming and inspiring story that tells us how sometimes, the worst thing that could happen may turn out to be the best.", + "inCinemas": "2022-03-30T00:00:00Z", + "physicalRelease": "2022-08-03T00:00:00Z", + "digitalRelease": "2022-07-30T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/9lbVcQfSfcMewo03UM7knSvN31T.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/duCaYLwr7LOhGTcyhcZP94rd2BS.jpg" + } + ], + "website": "", + "year": 2022, + "hasFile": false, + "youTubeTrailerId": "", + "studio": "France 2 Cinéma", + "path": "/movies/Rise (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Rise (2022)", + "runtime": 117, + "cleanTitle": "rise", + "imdbId": "tt13531468", + "tmdbId": 771077, + "titleSlug": "771077", + "genres": [ + "Comedy", + "Drama" + ], + "tags": [], + "added": "2022-03-29T14:52:43Z", + "ratings": { + "imdb": { + "votes": 818, + "value": 7.5, + "type": "user" + }, + "tmdb": { + "votes": 167, + "value": 7.913, + "type": "user" + } + }, + "id": 147 + }, + { + "title": "Fantastic Beasts: The Secrets of Dumbledore", + "originalTitle": "Fantastic Beasts: The Secrets of Dumbledore", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Фантастические твари и где они обитают 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1251 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Φανταστικά Ζώα και που Βρίσκονται 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1252 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Les Animaux fantastiques - 3e partie", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 2, + "name": "French" + }, + "id": 1253 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Fantastic Beasts 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1255 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Fantastická zvířata 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1256 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Фантастичні звірі 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1257 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Fantastiske Skabninger: Dumbledores Hemmeligheder", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1258 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "怪獸與鄧不利多的秘密", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1453 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Fantastyczne zwierzęta: Tajemnice Dumbledore'a", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 12, + "name": "Polish" + }, + "id": 1482 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Fantastiskās būtnes: Dumidora noslēpumi", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1483 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Animales Fantásticos: Los Secretos de Dumbledore", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1540 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Fantastic Beasts 3 - The Secrets of Dumbledore", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1595 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Animali Fantastici - I Segreti Di Silente", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 5, + "name": "Italian" + }, + "id": 1638 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Animali Fantastici 3 I Segreti Di Silente", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 5, + "name": "Italian" + }, + "id": 1639 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "Les Animaux fantastiques 3 - Les Secrets de Dumbledore", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 2, + "name": "French" + }, + "id": 1774 + }, + { + "sourceType": "tmdb", + "movieId": 121, + "title": "神奇动物3:邓布利多之谜", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1823 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "fantastic beasts secrets dumbledore", + "sizeOnDisk": 9631363612, + "status": "released", + "overview": "Professor Albus Dumbledore knows the powerful, dark wizard Gellert Grindelwald is moving to seize control of the wizarding world. Unable to stop him alone, he entrusts magizoologist Newt Scamander to lead an intrepid team of wizards and witches. They soon encounter an array of old and new beasts as they clash with Grindelwald's growing legion of followers.", + "inCinemas": "2022-04-06T00:00:00Z", + "physicalRelease": "2022-06-28T00:00:00Z", + "digitalRelease": "2022-05-30T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/3c5GNLB4yRSLBby0trHoA1DSQxQ.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/zGLHX92Gk96O1DJvLil7ObJTbaL.jpg" + } + ], + "website": "https://www.fantasticbeasts.com", + "year": 2022, + "hasFile": true, + "youTubeTrailerId": "Fo6TfHkLW6Y", + "studio": "Warner Bros. Pictures", + "path": "/movies/Fantastic Beasts The Secrets of Dumbledore (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Fantastic Beasts The Secrets of Dumbledore (2022)", + "runtime": 142, + "cleanTitle": "fantasticbeastssecretsdumbledore", + "imdbId": "tt4123432", + "tmdbId": 338953, + "titleSlug": "338953", + "certification": "PG-13", + "genres": [ + "Fantasy", + "Adventure", + "Action" + ], + "tags": [], + "added": "2022-03-04T21:08:32Z", + "ratings": { + "imdb": { + "votes": 118753, + "value": 6.2, + "type": "user" + }, + "tmdb": { + "votes": 2455, + "value": 6.864, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 47, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 46, + "type": "user" + } + }, + "movieFile": { + "movieId": 121, + "relativePath": "Fantastic Beasts The Secrets of Dumbledore (2022) WEBDL-1080p.mkv", + "path": "/movies/Fantastic Beasts The Secrets of Dumbledore (2022)/Fantastic Beasts The Secrets of Dumbledore (2022) WEBDL-1080p.mkv", + "size": 9631363612, + "dateAdded": "2022-05-30T09:12:04Z", + "sceneName": "Fantastic.Beasts.The.Secrets.of.Dumbledore.2022.1080p.HMAX.WEBRip.DDP5.1.Atmos.x264-SMURF", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 3, + "name": "WEBDL-1080p", + "source": "webdl", + "resolution": 1080, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 768000, + "audioChannels": 5.1, + "audioCodec": "EAC3 Atmos", + "audioLanguages": "eng", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 0, + "videoCodec": "x264", + "videoDynamicRangeType": "", + "videoFps": 23.976, + "resolution": "1920x816", + "runTime": "2:22:39", + "scanType": "Progressive", + "subtitles": "eng/eng/bul/cze/dan/spa/spa/fin/hrv/hun/mac/dut/nor/pol/por/por/rum/slo/slv/srp/swe/spa" + }, + "originalFilePath": "Fantastic.Beasts.The.Secrets.of.Dumbledore.2022.1080p.HMAX.WEBRip.DDP5.1.Atmos.x264-SMURF/Fantastic.Beasts.The.Secrets.of.Dumbledore.2022.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-SMURF.mkv", + "qualityCutoffNotMet": false, + "languages": [ + { + "id": 1, + "name": "English" + } + ], + "releaseGroup": "SMURF", + "edition": "", + "id": 223 + }, + "collection": { + "name": "Fantastic Beasts Collection", + "tmdbId": 435259, + "images": [] + }, + "id": 121 + }, + { + "title": "Doctor Strange in the Multiverse of Madness", + "originalTitle": "Doctor Strange in the Multiverse of Madness", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1504 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Доктор Стрэндж 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1505 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doutor Estranho 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1506 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange v multivesmíre šialenstva", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1507 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange 2: El multiverso de la locura", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 3, + "name": "Spanish" + }, + "id": 1508 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "মহাবিশ্বের পাগলামিতে অদ্ভুত চিকিৎসক", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1510 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "จอมเวทย์มหากาฬ ในมัลติเวิร์สมหาภัย", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 28, + "name": "Thai" + }, + "id": 1511 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Marvel Studios' Doctor Strange in the Multiverse of Madness", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1512 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange en el Multiverso de la Locura de Marvel Studios", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 3, + "name": "Spanish" + }, + "id": 1513 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doktors Streindžs neprāta multivisumā", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1514 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange in the Multitude of Madness 3D", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1515 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "닥터 스트레인지 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1531 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "奇異博士2: 失控多元宇宙", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1532 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Ο Δόκτωρ Στρέιντζ στο πολυσύμπαν της τρέλας", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1544 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doktor Streync 2: Multikainatın Dəliliklərində", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1566 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange 2 - The Multiverse of Madness", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1664 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "奇异博士2:疯狂多元宇宙", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1665 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "奇異博士2:失控多重宇宙", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1667 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doutor Estranho no Multiverso da Loucura (2022)", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1668 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doctor Strange: Đa Vũ Trụ Hỗn Loạn", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1766 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Բժիշկ Սթրենջը Խելագարության Բազմաշխարհում", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1772 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Stellar Vortex", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1773 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "Doktor Strange 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 17, + "name": "Turkish" + }, + "id": 1777 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "დოქტორი სტრეინჯი სიგიჟის მრავალსამყაროში", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1834 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "დოქტორი სტრეინჯი სიგიჟის მულტისამყაროში", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1835 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "마블 닥터 스트레인지: 대혼돈의 멀티버스", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1836 + }, + { + "sourceType": "tmdb", + "movieId": 153, + "title": "닥터 스트레인지 대혼돈의 멀티버스", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1837 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "doctor strange in multiverse madness", + "sizeOnDisk": 9961899948, + "status": "released", + "overview": "Doctor Strange, with the help of mystical allies both old and new, traverses the mind-bending and dangerous alternate realities of the Multiverse to confront a mysterious new adversary.", + "inCinemas": "2022-05-04T00:00:00Z", + "physicalRelease": "2022-07-26T00:00:00Z", + "digitalRelease": "2022-06-22T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/9Gtg2DzBhmYamXBS1hKAhiwbBKS.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/wcKFYIiVDvRURrzglV9kGu7fpfY.jpg" + } + ], + "website": "https://www.marvel.com/movies/doctor-strange-in-the-multiverse-of-madness", + "year": 2022, + "hasFile": true, + "youTubeTrailerId": "Rf8LAYJSOL8", + "studio": "Marvel Studios", + "path": "/movies/Doctor Strange in the Multiverse of Madness (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Doctor Strange in the Multiverse of Madness (2022)", + "runtime": 126, + "cleanTitle": "doctorstrangeinmultiversemadness", + "imdbId": "tt9419884", + "tmdbId": 453395, + "titleSlug": "453395", + "certification": "PG-13", + "genres": [ + "Fantasy", + "Action", + "Adventure" + ], + "tags": [], + "added": "2022-05-03T20:20:14Z", + "ratings": { + "imdb": { + "votes": 336918, + "value": 7, + "type": "user" + }, + "tmdb": { + "votes": 5076, + "value": 7.5, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 60, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 74, + "type": "user" + } + }, + "movieFile": { + "movieId": 153, + "relativePath": "Doctor Strange in the Multiverse of Madness (2022) WEBRip-1080p.mkv", + "path": "/movies/Doctor Strange in the Multiverse of Madness (2022)/Doctor Strange in the Multiverse of Madness (2022) WEBRip-1080p.mkv", + "size": 9961899948, + "dateAdded": "2022-06-22T00:45:05Z", + "sceneName": "Doctor.Strange.in.the.Multiverse.of.Madness.2022.1080p.WEBRip.DDP5.1.Atmos.x264-NOGRP", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 15, + "name": "WEBRip-1080p", + "source": "webrip", + "resolution": 1080, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 768000, + "audioChannels": 5.1, + "audioCodec": "EAC3 Atmos", + "audioLanguages": "eng", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 0, + "videoCodec": "x264", + "videoDynamicRangeType": "", + "videoFps": 23.976, + "resolution": "1920x804", + "runTime": "2:06:31", + "scanType": "Progressive", + "subtitles": "eng/eng" + }, + "originalFilePath": "Doctor.Strange.in.the.Multiverse.of.Madness.2022.1080p.WEBRip.DDP5.1.Atmos.x264-NOGRP/Doctor.Strange.in.the.Multiverse.of.Madness.2022.1080p.WEBRip.DDP5.1.Atmos.x264-NOGRP.mkv", + "qualityCutoffNotMet": false, + "languages": [ + { + "id": 1, + "name": "English" + } + ], + "releaseGroup": "NOGRP", + "edition": "", + "id": 224 + }, + "collection": { + "name": "Doctor Strange Collection", + "tmdbId": 618529, + "images": [] + }, + "id": 153 + }, + { + "title": "Top Gun: Maverick", + "originalTitle": "Top Gun: Maverick", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Лучший стрелок 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1611 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Top Gun 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1612 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Кращий стрілець 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1614 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Top Gun 2 - Maverick", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 4, + "name": "German" + }, + "id": 1615 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Phi Công Siêu Đẳng Maverick", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1621 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Кращий стрілець: Меверік", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1623 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "壮志凌云2:独行侠", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1671 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Топ Ган: Мэверик", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1729 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "توب قن: مافريك", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1763 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Top Gun: Sayohatchi", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1764 + }, + { + "sourceType": "tmdb", + "movieId": 160, + "title": "Топ ган: Маверик", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1765 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "top gun maverick", + "sizeOnDisk": 0, + "status": "released", + "overview": "After more than thirty years of service as one of the Navy’s top aviators, and dodging the advancement in rank that would ground him, Pete “Maverick” Mitchell finds himself training a detachment of TOP GUN graduates for a specialized mission the likes of which no living pilot has ever seen.", + "inCinemas": "2022-05-24T00:00:00Z", + "physicalRelease": "2022-10-05T00:00:00Z", + "digitalRelease": "2022-08-08T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/62HCnUTziyWcpDaBO2i1DX17ljH.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/odJ4hx6g6vBt4lBWKFD1tI8WS4x.jpg" + } + ], + "website": "https://www.topgunmovie.com", + "year": 2022, + "hasFile": false, + "youTubeTrailerId": "giXco2jaZ_4", + "studio": "Paramount", + "path": "/movies/Top Gun Maverick (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Top Gun Maverick (2022)", + "runtime": 131, + "cleanTitle": "topgunmaverick", + "imdbId": "tt1745960", + "tmdbId": 361743, + "titleSlug": "361743", + "certification": "PG-13", + "genres": [ + "Action", + "Drama" + ], + "tags": [], + "added": "2022-06-07T19:53:34Z", + "ratings": { + "imdb": { + "votes": 255677, + "value": 8.6, + "type": "user" + }, + "tmdb": { + "votes": 1811, + "value": 8.3, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 78, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 97, + "type": "user" + } + }, + "collection": { + "name": "Top Gun Collection", + "tmdbId": 531330, + "images": [] + }, + "id": 160 + }, + { + "title": "Jurassic World Dominion", + "originalTitle": "Jurassic World Dominion", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Jurassic Park 6", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1872 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Jurassic World 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1873 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "쥬라기 공원 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1874 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Мир Юрского периода 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1875 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Jurassic World 3: Dominion", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1876 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Jurassic World 3: Dominio", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1877 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Світ Юрського періоду 3", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1878 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Світ Юрського періоду: Домініон", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1879 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Мир Юрского периода: Власть", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1880 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "จูราสสิค เวิลด์ ทวงคืนอาณาจักร", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 28, + "name": "Thai" + }, + "id": 1881 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "La monde jurassique : Domination", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1882 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Monde jurassique : la domination", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1883 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "ジュラシックワールドドミニオン", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1884 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Thế Giới Khủng Long: Lãnh Địa", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1885 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Jurassic World 3 - Dominion (2022)", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1887 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "Jurassic World", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 2, + "name": "French" + }, + "id": 1888 + }, + { + "sourceType": "tmdb", + "movieId": 176, + "title": "侏儸紀世界:統霸天下", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1891 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "jurassic world dominion", + "sizeOnDisk": 3288297053, + "status": "released", + "overview": "Four years after Isla Nublar was destroyed, dinosaurs now live—and hunt—alongside humans all over the world. This fragile balance will reshape the future and determine, once and for all, whether human beings are to remain the apex predators on a planet they now share with history’s most fearsome creatures.", + "inCinemas": "2022-06-01T00:00:00Z", + "physicalRelease": "2022-08-25T00:00:00Z", + "digitalRelease": "2022-07-14T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/kAVRgw7GgK1CfYEJq8ME6EvRIgU.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/9eAn20y26wtB3aet7w9lHjuSgZ3.jpg" + } + ], + "website": "https://www.jurassicworld.com", + "year": 2022, + "hasFile": true, + "youTubeTrailerId": "DtQycgMD4HQ", + "studio": "Universal Pictures", + "path": "/movies/Jurassic World Dominion (2022)", + "qualityProfileId": 4, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Jurassic World Dominion (2022)", + "runtime": 147, + "cleanTitle": "jurassicworlddominion", + "imdbId": "tt8041270", + "tmdbId": 507086, + "titleSlug": "507086", + "certification": "PG-13", + "genres": [ + "Adventure", + "Action", + "Science Fiction" + ], + "tags": [], + "added": "2022-08-03T22:37:52Z", + "ratings": { + "imdb": { + "votes": 105728, + "value": 5.7, + "type": "user" + }, + "tmdb": { + "votes": 2532, + "value": 7.1, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 38, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 30, + "type": "user" + } + }, + "movieFile": { + "movieId": 176, + "relativePath": "Jurassic World Dominion (2022) Bluray-1080p.mp4", + "path": "/movies/Jurassic World Dominion (2022)/Jurassic World Dominion (2022) Bluray-1080p.mp4", + "size": 3288297053, + "dateAdded": "2022-08-03T22:40:58Z", + "sceneName": "Jurassic.World.3.Dominion.2022.EXTENDED.1080p.BluRay.H264.AAC-RARBG", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 7, + "name": "Bluray-1080p", + "source": "bluray", + "resolution": 1080, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 224000, + "audioChannels": 5.1, + "audioCodec": "AAC", + "audioLanguages": "eng", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 2497768, + "videoCodec": "x264", + "videoDynamicRangeType": "", + "videoFps": 23.976, + "resolution": "1920x960", + "runTime": "2:40:40", + "scanType": "Progressive", + "subtitles": "" + }, + "originalFilePath": "Jurassic.World.3.Dominion.2022.EXTENDED.1080p.BluRay.H264.AAC-RARBG/Jurassic.World.3.Dominion.2022.EXTENDED.1080p.BluRay.H264.AAC-RARBG.mp4", + "qualityCutoffNotMet": false, + "languages": [ + { + "id": 1, + "name": "English" + } + ], + "releaseGroup": "RARBG", + "edition": "EXTENDED", + "id": 232 + }, + "collection": { + "name": "Jurassic Park Collection", + "tmdbId": 328, + "images": [] + }, + "id": 176 + }, + { + "title": "Lightyear", + "originalTitle": "Lightyear", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Баз Светлинна година", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 29, + "name": "Bulgarian" + }, + "id": 1580 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Buzz Astral", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 12, + "name": "Polish" + }, + "id": 1581 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Buzz Lightyear", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 18, + "name": "Portuguese" + }, + "id": 1582 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Lightyear - Cảnh Sát Vũ Trụ", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1583 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Bazs Gaismasgads", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1584 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "光年正傳", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1598 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "บัซ ไลท์เยียร์", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 28, + "name": "Thai" + }, + "id": 1633 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "光年正传", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1635 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Buzz l'Éclair", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 2, + "name": "French" + }, + "id": 1648 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Rakeťák", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1649 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Svjetlosni", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1650 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "लाइटईयर", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1651 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "שנות-אור", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1652 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "バズ・ライトイヤー", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1653 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Lightyear - La vera storia di Buzz", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 5, + "name": "Italian" + }, + "id": 1654 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Базз Лайтер", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1655 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "버즈 라이트이어", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1656 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Баз Светлосни", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1657 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "巴斯光年", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1658 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Işıkyılı", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 17, + "name": "Turkish" + }, + "id": 1659 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "Bazz Layter", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1660 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "ბაზ ლაითერი", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1829 + }, + { + "sourceType": "tmdb", + "movieId": 157, + "title": "სათამაშოების ისტორია: ბაზ ლაითერი", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1830 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "lightyear", + "sizeOnDisk": 4162236849, + "status": "released", + "overview": "Legendary Space Ranger Buzz Lightyear embarks on an intergalactic adventure alongside a group of ambitious recruits and his robot companion Sox.", + "inCinemas": "2022-06-15T00:00:00Z", + "physicalRelease": "2022-09-29T00:00:00Z", + "digitalRelease": "2022-08-03T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/ox4goZd956BxqJH6iLwhWPL9ct4.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/nW5fUbldp1DYf2uQ3zJTUdachOu.jpg" + } + ], + "website": "https://movies.disney.com/lightyear", + "year": 2022, + "hasFile": true, + "youTubeTrailerId": "0ir3Z8hLeHc", + "studio": "Walt Disney Pictures", + "path": "/movies/Lightyear (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "announced", + "isAvailable": true, + "folderName": "/movies/Lightyear (2022)", + "runtime": 105, + "cleanTitle": "lightyear", + "imdbId": "tt10298810", + "tmdbId": 718789, + "titleSlug": "718789", + "certification": "PG", + "genres": [ + "Animation", + "Science Fiction", + "Adventure" + ], + "tags": [], + "added": "2022-05-25T11:01:52Z", + "ratings": { + "imdb": { + "votes": 57059, + "value": 5.5, + "type": "user" + }, + "tmdb": { + "votes": 1405, + "value": 7.362, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 60, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 75, + "type": "user" + } + }, + "movieFile": { + "movieId": 157, + "relativePath": "Lightyear (2022) WEBRip-1080p.mkv", + "path": "/movies/Lightyear (2022)/Lightyear (2022) WEBRip-1080p.mkv", + "size": 4162236849, + "dateAdded": "2022-07-14T12:39:18Z", + "sceneName": "Lightyear.2022.1080p.WEBRip.DD5.1.x264-EVO[TGx]", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 15, + "name": "WEBRip-1080p", + "source": "webrip", + "resolution": 1080, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 384000, + "audioChannels": 5.1, + "audioCodec": "AC3", + "audioLanguages": "eng", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 0, + "videoCodec": "x264", + "videoDynamicRangeType": "", + "videoFps": 23.976, + "resolution": "1920x804", + "runTime": "1:45:04", + "scanType": "Progressive", + "subtitles": "" + }, + "originalFilePath": "Lightyear.2022.1080p.WEBRip.DD5.1.x264-EVO[TGx]/Lightyear.2022.1080p.WEBRip.DD5.1.x264-EVO.mkv", + "qualityCutoffNotMet": false, + "languages": [ + { + "id": 1, + "name": "English" + } + ], + "releaseGroup": "EVO", + "edition": "", + "id": 227 + }, + "id": 157 + }, + { + "title": "Minions: The Rise of Gru", + "originalTitle": "Minions: The Rise of Gru", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 165, + "title": "미니언즈: 라이즈 오브 그루", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1679 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Miljenici: Uspon Grua", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1680 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Käsilased 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1681 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions 2: Η Άνοδος του Γκρου", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1682 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minioni 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1683 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions: Historien om Gru", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 15, + "name": "Norwegian" + }, + "id": 1684 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Mínimos 2: A Ascensão de Gru", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 18, + "name": "Portuguese" + }, + "id": 1685 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Миньоны: Грювитация", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1686 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Малци: Успон Груа", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1687 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Mimoni: Zloduch prichádza", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1688 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions: El origen de Gru", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 3, + "name": "Spanish" + }, + "id": 1689 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minioner: Berättelsen om Gru", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1690 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions 2: Come Gru diventa cattivissimo", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 5, + "name": "Italian" + }, + "id": 1691 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions 2: Historien om Gru", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1692 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1693 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions 2: Nace un villano", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1694 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions - Auf der Suche nach dem Mini-Boss", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 4, + "name": "German" + }, + "id": 1695 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions 2: The Rise of Gru", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 7, + "name": "Dutch" + }, + "id": 1696 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "迷你兵團 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1697 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions: Hoe Gru superschurk were", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 7, + "name": "Dutch" + }, + "id": 1698 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "มินเนี่ยน 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 28, + "name": "Thai" + }, + "id": 1699 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minyonlar 2: Gru'nun Yükselişi", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 17, + "name": "Turkish" + }, + "id": 1726 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions: Nace un villano", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1734 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Մինիոններ. Գրուի վերելքը", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1787 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "Minions: KEBANGKITAN GRU", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1838 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "미니언즈 2", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1846 + }, + { + "sourceType": "tmdb", + "movieId": 165, + "title": "小黄人大眼萌:神偷奶爸前传", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1860 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "minions rise gru", + "sizeOnDisk": 6621540111, + "status": "released", + "overview": "A fanboy of a supervillain supergroup known as the Vicious 6, Gru hatches a plan to become evil enough to join them, with the backup of his followers, the Minions.", + "inCinemas": "2022-06-29T00:00:00Z", + "physicalRelease": "2022-09-15T00:00:00Z", + "digitalRelease": "2022-08-02T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/wKiOkZTN9lUUUNZLmtnwubZYONg.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/nmGWzTLMXy9x7mKd8NKPLmHtWGa.jpg" + } + ], + "website": "https://www.minionsmovie.com/", + "year": 2022, + "hasFile": true, + "youTubeTrailerId": "HhIl_XJ-OGA", + "studio": "Universal Pictures", + "path": "/movies/Minions The Rise of Gru (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": true, + "folderName": "/movies/Minions The Rise of Gru (2022)", + "runtime": 87, + "cleanTitle": "minionsrisegru", + "imdbId": "tt5113044", + "tmdbId": 438148, + "titleSlug": "438148", + "certification": "PG", + "genres": [ + "Family", + "Animation", + "Adventure" + ], + "tags": [], + "added": "2022-06-27T13:00:47Z", + "ratings": { + "imdb": { + "votes": 29174, + "value": 6.8, + "type": "user" + }, + "tmdb": { + "votes": 1038, + "value": 7.8, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 56, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 70, + "type": "user" + } + }, + "movieFile": { + "movieId": 165, + "relativePath": "Minions The Rise of Gru (2022) WEBRip-1080p.mkv", + "path": "/movies/Minions The Rise of Gru (2022)/Minions The Rise of Gru (2022) WEBRip-1080p.mkv", + "size": 6621540111, + "dateAdded": "2022-08-02T07:18:57Z", + "sceneName": "Minions.The.Rise.of.Gru.2022.1080p.AMZN.WEBRip.DDP5.1.Atmos.x264-NOGRP", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 15, + "name": "WEBRip-1080p", + "source": "webrip", + "resolution": 1080, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 768000, + "audioChannels": 5.1, + "audioCodec": "EAC3 Atmos", + "audioLanguages": "eng", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 0, + "videoCodec": "x264", + "videoDynamicRangeType": "", + "videoFps": 23.976, + "resolution": "1920x800", + "runTime": "1:27:35", + "scanType": "Progressive", + "subtitles": "eng/eng/fre" + }, + "originalFilePath": "Minions.The.Rise.of.Gru.2022.1080p.AMZN.WEBRip.DDP5.1.Atmos.x264-NOGRP/Minions.The.Rise.of.Gru.2022.1080p.AMZN.WEBRip.DDP5.1.Atmos.x264-NOGRP.mkv", + "qualityCutoffNotMet": false, + "languages": [ + { + "id": 1, + "name": "English" + } + ], + "releaseGroup": "NOGRP", + "edition": "", + "id": 230 + }, + "collection": { + "name": "Minions Collection", + "tmdbId": 544669, + "images": [] + }, + "id": 165 + }, + { + "title": "Thor: Love and Thunder", + "originalTitle": "Thor: Love and Thunder", + "originalLanguage": { + "id": 1, + "name": "English" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Thor 4", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1735 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "雷神4:爱与雷霆", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1736 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Тор 4", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 11, + "name": "Russian" + }, + "id": 1737 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "থর: প্রেম এবং বজ্রপাত", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1738 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Marvel Studios' Thor: Love and Thunder", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1739 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Thor: Love and Thunder de Marvel Studios", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 3, + "name": "Spanish" + }, + "id": 1740 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Tors: Mīla un Pērkons", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1741 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Thor : Amour et tonnerre de Marvel Studios", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1742 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Marvel Studios' Thor: Tình Yêu và Sấm Sét", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1743 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Thor: amor y trueno", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 3, + "name": "Spanish" + }, + "id": 1745 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "თორი: სიყვარული და ჭექა-ქუხილი", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1746 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "토르 4", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1770 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Թոր: Սեր և ամպրոպ", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1775 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "תור: אהבה ורעם", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1781 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "Thor: Cinta dan Guntur", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1790 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "마블 토르: 러브 앤 썬더", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1839 + }, + { + "sourceType": "tmdb", + "movieId": 168, + "title": "토르 러브 앤 썬더", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1840 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "thor love thunder", + "sizeOnDisk": 0, + "status": "inCinemas", + "overview": "After his retirement is interrupted by Gorr the God Butcher, a galactic killer who seeks the extinction of the gods, Thor enlists the help of King Valkyrie, Korg, and ex-girlfriend Jane Foster, who now inexplicably wields Mjolnir as the Mighty Thor. Together they embark upon a harrowing cosmic adventure to uncover the mystery of the God Butcher’s vengeance and stop him before it’s too late.", + "inCinemas": "2022-07-06T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/pIkRyD18kl4FhoCNQuWxWu5cBLM.jpg" + }, + { + "coverType": "fanart", + "url": "https://image.tmdb.org/t/p/original/p1F51Lvj3sMopG948F5HsBbl43C.jpg" + } + ], + "website": "https://www.marvel.com/movies/thor-love-and-thunder", + "year": 2022, + "hasFile": false, + "youTubeTrailerId": "Go8nTmfrQd8", + "studio": "Marvel Studios", + "path": "/movies/Thor Love and Thunder (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": false, + "folderName": "/movies/Thor Love and Thunder (2022)", + "runtime": 119, + "cleanTitle": "thorlovethunder", + "imdbId": "tt10648342", + "tmdbId": 616037, + "titleSlug": "616037", + "certification": "PG-13", + "genres": [ + "Action", + "Adventure", + "Fantasy" + ], + "tags": [], + "added": "2022-07-04T17:04:57Z", + "ratings": { + "imdb": { + "votes": 146274, + "value": 6.8, + "type": "user" + }, + "tmdb": { + "votes": 1695, + "value": 6.8, + "type": "user" + }, + "metacritic": { + "votes": 0, + "value": 57, + "type": "user" + }, + "rottenTomatoes": { + "votes": 0, + "value": 67, + "type": "user" + } + }, + "collection": { + "name": "Thor Collection", + "tmdbId": 131296, + "images": [] + }, + "id": 168 + }, + { + "title": "Sword Art Online the Movie -Progressive- Scherzo of a Deep Night", + "originalTitle": "劇場版 ソードアート・オンライン プログレッシブ 冥き夕闇のスケルツォ", + "originalLanguage": { + "id": 8, + "name": "Japanese" + }, + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 169, + "title": "Sword Art Online Progressive", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1748 + } + ], + "secondaryYearSourceId": 0, + "sortTitle": "sword art online movie progressive scherzo deep night", + "sizeOnDisk": 0, + "status": "announced", + "overview": "", + "inCinemas": "2022-09-10T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "https://image.tmdb.org/t/p/original/1L904CSzPCEEpPcUoBWH4cjuGJW.jpg" + } + ], + "website": "", + "year": 2022, + "hasFile": false, + "youTubeTrailerId": "", + "studio": "Aniplex", + "path": "/movies/Sword Art Online the Movie -Progressive- Scherzo of a Deep Night (2022)", + "qualityProfileId": 1, + "monitored": true, + "minimumAvailability": "released", + "isAvailable": false, + "folderName": "/movies/Sword Art Online the Movie -Progressive- Scherzo of a Deep Night (2022)", + "runtime": 0, + "cleanTitle": "swordartonlinemovieprogressivescherzodeepnight", + "tmdbId": 893712, + "titleSlug": "893712", + "genres": [ + "Animation", + "Action", + "Fantasy" + ], + "tags": [], + "added": "2022-07-04T17:05:01Z", + "ratings": { + "tmdb": { + "votes": 0, + "value": 0, + "type": "user" + } + }, + "collection": { + "name": "Sword Art Online the Movie -Progressive-", + "tmdbId": 893713, + "images": [] + }, + "id": 169 + } +] \ No newline at end of file diff --git a/src/modules/common/examples/multipletvshows.json b/src/modules/common/examples/multipletvshows.json new file mode 100644 index 000000000..7857b70ce --- /dev/null +++ b/src/modules/common/examples/multipletvshows.json @@ -0,0 +1,409 @@ +{ + "page": 1, + "totalPages": 2, + "totalResults": 21, + "results": [ + { + "id": 66025, + "firstAirDate": "2016-06-14", + "genreIds": [ + 80, + 18 + ], + "mediaType": "tv", + "name": "Animal Kingdom", + "originCountry": [ + "US" + ], + "originalLanguage": "en", + "originalName": "Animal Kingdom", + "overview": "Un jeune homme de dix-sept ans emménage avec la famille Cody après le décès de sa mère, une fratrie baignant dans la criminalité gouvernée d'une main de maître par la matriarche, Smurf.", + "popularity": 75.653, + "voteAverage": 7.7, + "voteCount": 318, + "backdropPath": "/eQJwfyMqSra10ck8HOoiCrbQR32.jpg", + "posterPath": "/rzvdKrnSRKPFI0pgqMQknDPpRC9.jpg", + "mediaInfo": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 217, + "mediaType": "tv", + "tmdbId": 66025, + "tvdbId": 304262, + "imdbId": null, + "status": 3, + "status4k": 1, + "createdAt": "2022-08-08T11:06:20.000Z", + "updatedAt": "2022-08-08T11:06:23.000Z", + "lastSeasonChange": "2022-08-08T11:06:20.000Z", + "mediaAddedAt": null, + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 56, + "externalServiceId4k": null, + "externalServiceSlug": "animal-kingdom-2016", + "externalServiceSlug4k": null, + "ratingKey": null, + "ratingKey4k": null, + "seasons": [], + "serviceUrl": "http://sonarr:8989/series/animal-kingdom-2016" + } + }, + { + "id": 44629, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 18, + 53, + 80, + 9648 + ], + "originalLanguage": "en", + "originalTitle": "Animal Kingdom", + "overview": "Une rue anonyme dans la banlieue de Melbourne. C’est là que vit la famille Cody. Profession: criminels. L’irruption parmi eux de Joshua, un neveu éloigné, offre à la police le moyen de les infiltrer. Il ne reste plus à Joshua qu’à choisir son camp...", + "popularity": 11.839, + "releaseDate": "2010-06-03", + "title": "Animal Kingdom", + "video": false, + "voteAverage": 6.8, + "voteCount": 643, + "backdropPath": "/dxOv6K3LNbZfQaGDyx7Tp94Koy.jpg", + "posterPath": "/qrVjc5JcaujL58SMMW9lqrp3bBX.jpg" + }, + { + "id": 95731, + "firstAirDate": "2020-09-25", + "genreIds": [ + 99 + ], + "mediaType": "tv", + "name": "Au cœur de Disney's Animal Kingdom", + "originCountry": [], + "originalLanguage": "en", + "originalName": "Magic of Disney's Animal Kingdom", + "overview": "Au cœur d’Animal Kingdom narrée par Josh Gad, une célébrité parmi les fans de Disney, nous emmène en coulisses découvrir la magie de deux des animations animalières les plus visitées au monde : le parc à thème de Disney, Animal Kingdom, et The Seas with Nemo & Friends à Epcot. Les spectateurs s’approchent au plus près de créatures parmi les plus rares et les plus belles de la planète et rencontrent les experts en soins animaliers qui ont tissé des liens stupéfiants avec les 5 000 et plus animaux du parc. Chacun des huit épisodes plonge au cœur de l’endroit le plus magique sur Terre, dévoilant les multiples facettes de sa conception et de sa gestion.", + "popularity": 3.367, + "voteAverage": 8, + "voteCount": 4, + "backdropPath": "/gMTMnd54VVAbGiodBqMTGCjM3b2.jpg", + "posterPath": "/gvNTeRAfu4KN3dD5HUO4Nbnri07.jpg" + }, + { + "id": 120862, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 35, + 18, + 10749 + ], + "originalLanguage": "en", + "originalTitle": "The Animal Kingdom", + "overview": "Tom Collier, jeune éditeur, a entretenu une liaison passionnée et intellectuelle avec une dessinatrice, Daisy Sage. Celle-ci ayant mis un terme à leur relation, il a fait la connaissance de Cecilia, qu'il a rapidement décidé d'épouser. Alors que les fiançailles sont annoncées, Daisy, toujours amoureuse, fait son retour, mais trop tard. Le mariage a lieu. Sous l'influence de Cecilia, Tom Collier, qui était un éditeur intègre et exigeant, fait de plus en plus de concessions commerciales. Daisy, elle demeure fidèle à elle-même. Tom Collier, se retrouve a évoluer, par amour pour sa femme, dans un milieu de conventions bourgeoises qui ne l'intéressent pas.", + "popularity": 2.102, + "releaseDate": "1932-12-28", + "title": "The Animal Kingdom", + "video": false, + "voteAverage": 6.3, + "voteCount": 13, + "backdropPath": "/5P1Hx46wvCVx9D9yT8M5rdUIHZB.jpg", + "posterPath": "/3sLWwNvS77xynAGLkbiHVXlO3UH.jpg" + }, + { + "id": 311015, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 99 + ], + "originalLanguage": "en", + "originalTitle": "Disney Parks: Disney's Animal Kingdom", + "overview": "", + "popularity": 1.208, + "releaseDate": "2010-01-01", + "title": "Disney Parks: Disney's Animal Kingdom", + "video": true, + "voteAverage": 9, + "voteCount": 2, + "backdropPath": null, + "posterPath": "/93OEKY5vnKqGFbOyHtUAdcEz8NV.jpg" + }, + { + "id": 291774, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "en", + "originalTitle": "Kenya 3D: Animal Kingdom", + "overview": "", + "popularity": 0.6, + "releaseDate": "2013-03-08", + "title": "Kenya 3D: Animal Kingdom", + "video": false, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": null + }, + { + "id": 640253, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "it", + "originalTitle": "Animal Kingdom", + "overview": "", + "popularity": 0.6, + "releaseDate": "2016-11-12", + "title": "Animal Kingdom", + "video": false, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": "/vJFK5cCcIh4X4op0oeK5iY2ibPv.jpg" + }, + { + "id": 507434, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 27 + ], + "originalLanguage": "en", + "originalTitle": "Animal Kingdom", + "overview": "", + "popularity": 0.6, + "releaseDate": "2017-02-25", + "title": "Animal Kingdom", + "video": false, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": "/8QxSJRLLw2m8ymrFsC2xJ26yd1n.jpg", + "posterPath": "/s77Q92boNGgkT2J5se3gwq5N8Xp.jpg" + }, + { + "id": 775877, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "en", + "originalTitle": "Disney's Animal Kingdom", + "overview": "", + "popularity": 0.6, + "releaseDate": "2004-05-12", + "title": "Disney's Animal Kingdom", + "video": true, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": null + }, + { + "id": 318575, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 99 + ], + "originalLanguage": "en", + "originalTitle": "Nature: Love in the Animal Kingdom", + "overview": "", + "popularity": 0.655, + "releaseDate": "2013-11-06", + "title": "Nature: Love in the Animal Kingdom", + "video": true, + "voteAverage": 9.5, + "voteCount": 2, + "backdropPath": "/vx2dfrXPTn0dKoyIqCEgrGvzwkd.jpg", + "posterPath": "/1fd53UCxtLAItNI5jMtVetFuw6v.jpg" + }, + { + "id": 743266, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "en", + "originalTitle": "Animal Kingdom: Great Are Thy Works", + "overview": "", + "popularity": 0.6, + "releaseDate": "1993-01-01", + "title": "Animal Kingdom: Great Are Thy Works", + "video": false, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": "/vjnsGLvymjG7dAIbjwzgFCdbhl6.jpg" + }, + { + "id": 828152, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 99 + ], + "originalLanguage": "en", + "originalTitle": "Disney's Animal Kingdom: Alive with Magic", + "overview": "", + "popularity": 0.6, + "releaseDate": "2017-06-27", + "title": "Disney's Animal Kingdom: Alive with Magic", + "video": false, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": "/amzVT8T9Ju3KLCDnBq4Rhf3LO8j.jpg" + }, + { + "id": 280391, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 12, + 35, + 16 + ], + "originalLanguage": "fr", + "originalTitle": "Pourquoi j'ai pas mangé mon père", + "overview": "L’histoire trépidante d’Édouard, fils aîné du roi des simiens, qui, considéré à sa naissance comme trop malingre, est rejeté par sa tribu. Il grandit loin d’eux, auprès de son ami Ian, et, incroyablement ingénieux, il découvre le feu, la chasse, l’habitat moderne, l’amour et même… l’espoir. Généreux, il veut tout partager, révolutionne l’ordre établi, et mène son peuple avec éclat et humour vers la véritable humanité… celle où on ne mange pas son père.", + "popularity": 12.971, + "releaseDate": "2015-04-08", + "title": "Pourquoi j'ai pas mangé mon père", + "video": false, + "voteAverage": 5.3, + "voteCount": 303, + "backdropPath": "/msDLrSt7Ozpe6oOg4XJrsQJd2IE.jpg", + "posterPath": "/efpzs2g1uRNcP8wPbIKSRPPH0aC.jpg" + }, + { + "id": 775559, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "en", + "originalTitle": "A New species of Theme Park: Disney’s Animal Kingdom", + "overview": "", + "popularity": 0.6, + "releaseDate": "1998-04-14", + "title": "A New species of Theme Park: Disney’s Animal Kingdom", + "video": true, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": null + }, + { + "id": 775831, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "en", + "originalTitle": "Disney Animal Kingdom Villas: A Village Comes to Life", + "overview": "", + "popularity": 0.6, + "releaseDate": "2007-06-14", + "title": "Disney Animal Kingdom Villas: A Village Comes to Life", + "video": true, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": null + }, + { + "id": 432906, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 99 + ], + "originalLanguage": "en", + "originalTitle": "Out in Nature: Homosexual Behaviour in the Animal Kingdom", + "overview": "", + "popularity": 0.6, + "releaseDate": "2001-09-07", + "title": "Out in Nature: Homosexual Behaviour in the Animal Kingdom", + "video": false, + "voteAverage": 6.8, + "voteCount": 4, + "backdropPath": null, + "posterPath": "/jjxhR9ZxZ3vhauK8IDR6wIBlCLI.jpg" + }, + { + "id": 128887, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 16, + 35 + ], + "originalLanguage": "ja", + "originalTitle": "クレヨンしんちゃん オタケべ!カスカベ野生王国", + "overview": "", + "popularity": 5.365, + "releaseDate": "2009-04-18", + "title": "クレヨンしんちゃん オタケべ!カスカベ野生王国", + "video": false, + "voteAverage": 8.5, + "voteCount": 10, + "backdropPath": "/azvwXB25Wvbx2Cou3Th7lbnjrqP.jpg", + "posterPath": "/h7LipCtdCyBOKR1By5wSP2Ufy3c.jpg" + }, + { + "id": 579733, + "mediaType": "movie", + "adult": false, + "genreIds": [], + "originalLanguage": "no", + "originalTitle": "Dyreriket", + "overview": "", + "popularity": 0.6, + "releaseDate": "2018-05-01", + "title": "Dyreriket", + "video": false, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": null, + "posterPath": null + }, + { + "id": 111612, + "firstAirDate": "2018-10-12", + "genreIds": [ + 10764 + ], + "mediaType": "tv", + "name": "坂上どうぶつ王国", + "originCountry": [ + "JP" + ], + "originalLanguage": "ja", + "originalName": "坂上どうぶつ王国", + "overview": "", + "popularity": 1.186, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": "/op8bK5R76L9QpwcVTnYG7nKXKsU.jpg", + "posterPath": "/2VPq9RYaDohOT8YqTibKZMMT2Ue.jpg" + }, + { + "id": 156216, + "firstAirDate": "2022-01-17", + "genreIds": [ + 16 + ], + "mediaType": "tv", + "name": "动物王国的故事", + "originCountry": [ + "CN" + ], + "originalLanguage": "zh", + "originalName": "动物王国的故事", + "overview": "", + "popularity": 0.6, + "voteAverage": 0, + "voteCount": 0, + "backdropPath": "/uxIJQnjzIQn2MGHk17nNhoIEkxU.jpg", + "posterPath": "/v90bqYZRUT30n22DdwahmW18LFn.jpg" + } + ] +} \ No newline at end of file diff --git a/src/modules/common/examples/music.json b/src/modules/common/examples/music.json new file mode 100644 index 000000000..e9d7933e2 --- /dev/null +++ b/src/modules/common/examples/music.json @@ -0,0 +1,832 @@ +{ + "title": "Celebrate", + "disambiguation": "", + "overview": "", + "artistId": 9, + "foreignAlbumId": "bfedab35-92b7-449b-adf0-875439ec9a85", + "monitored": true, + "anyReleaseOk": true, + "profileId": 1, + "duration": 1818062, + "albumType": "Album", + "secondaryTypes": [], + "mediumCount": 1, + "ratings": { + "votes": 1, + "value": 10 + }, + "releaseDate": "2022-07-27T00:00:00Z", + "releases": [ + { + "id": 202, + "albumId": 32, + "foreignReleaseId": "22bd49a1-f858-427d-94ee-1788b54fb508", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "ONCE JAPAN限定盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 203, + "albumId": 32, + "foreignReleaseId": "52c73f5f-4f91-451b-96d1-3ac3ef9371ee", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "初回限定盤B", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 204, + "albumId": 32, + "foreignReleaseId": "5745040b-a5fa-4dae-ad31-0bce9d501e23", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "JEONGYEON盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 205, + "albumId": 32, + "foreignReleaseId": "006f9135-454b-4182-a057-47d1b002a282", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "NAYEON盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 206, + "albumId": 32, + "foreignReleaseId": "eeacd54b-a2bd-48f8-8d7c-3ab55b68f17c", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 81, + "media": [ + { + "mediumNumber": 1, + "mediumName": "NAYEON盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 2, + "mediumName": "JEONGYEON盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 3, + "mediumName": "MOMO盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 4, + "mediumName": "SANA盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 5, + "mediumName": "JIHYO盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 6, + "mediumName": "MINA盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 7, + "mediumName": "DAHYUN盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 8, + "mediumName": "CHAEYOUNG盤", + "mediumFormat": "CD" + }, + { + "mediumNumber": 9, + "mediumName": "TZUYU盤", + "mediumFormat": "CD" + } + ], + "mediumCount": 9, + "disambiguation": "5th Anniversary Collection BOX", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan", + "Warner Music Japan" + ], + "format": "9xCD", + "monitored": false + }, + { + "id": 207, + "albumId": 32, + "foreignReleaseId": "8ddd43f0-859e-4cff-be7c-daf6806cc035", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "JIHYO盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 208, + "albumId": 32, + "foreignReleaseId": "ad8e0553-97de-499b-8010-85bd02c62859", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "TZUYU盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 209, + "albumId": 32, + "foreignReleaseId": "276bf831-8cae-49a0-bc50-479869d401ac", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "MOMO盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 210, + "albumId": 32, + "foreignReleaseId": "3d201058-deb0-4159-a82f-d9076a608036", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "MINA盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 211, + "albumId": 32, + "foreignReleaseId": "e1fbf96d-f83e-478c-be7d-f0f6dd5305d1", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "DAHYUN盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 212, + "albumId": 32, + "foreignReleaseId": "769a7006-763b-4cd8-8d1f-d389d52ec002", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "CHAEYOUNG盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 213, + "albumId": 32, + "foreignReleaseId": "42e74581-0ef3-4db9-8a20-ba8a3daa1cf0", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "初回限定盤A", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan", + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 214, + "albumId": 32, + "foreignReleaseId": "81bdf07f-61ad-4436-bfae-63cd1d9e700c", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "通常盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 215, + "albumId": 32, + "foreignReleaseId": "273b3ba1-88e8-4653-a542-c8b0489c1772", + "title": "Celebrate", + "status": "Official", + "duration": 0, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "CD" + } + ], + "mediumCount": 1, + "disambiguation": "SANA盤", + "country": [ + "Japan" + ], + "label": [ + "Warner Music Japan" + ], + "format": "CD", + "monitored": false + }, + { + "id": 216, + "albumId": 32, + "foreignReleaseId": "2442df5f-4090-452c-be7f-5885dffee8e2", + "title": "Celebrate", + "status": "Official", + "duration": 1818062, + "trackCount": 9, + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "Digital Media" + } + ], + "mediumCount": 1, + "disambiguation": "", + "country": [ + "Algeria", + "Angola", + "Anguilla", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Barbados", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Brazil", + "Brunei", + "Bulgaria", + "Burkina Faso", + "Cambodia", + "Cameroon", + "Canada", + "Cape Verde", + "Cayman Islands", + "Chad", + "Chile", + "China", + "Colombia", + "Congo", + "Costa Rica", + "Côte d'Ivoire", + "Croatia", + "Cyprus", + "Czech Republic", + "Denmark", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Estonia", + "Fiji", + "Finland", + "France", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Greece", + "Grenada", + "Guatemala", + "Guinea-Bissau", + "Guyana", + "Honduras", + "Hong Kong", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iraq", + "Ireland", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jordan", + "Kazakhstan", + "Kenya", + "Kuwait", + "Kyrgyzstan", + "Laos", + "Latvia", + "Lebanon", + "Liberia", + "Libya", + "Lithuania", + "Luxembourg", + "Macao", + "North Macedonia", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Mauritania", + "Mauritius", + "Mexico", + "Federated States of Micronesia", + "Moldova", + "Mongolia", + "Montserrat", + "Morocco", + "Mozambique", + "Myanmar", + "Namibia", + "Nepal", + "Netherlands", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Norway", + "Oman", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Poland", + "Portugal", + "Qatar", + "Romania", + "Rwanda", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Vincent and The Grenadines", + "Saudi Arabia", + "Senegal", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "South Africa", + "Spain", + "Sri Lanka", + "Suriname", + "Eswatini", + "Sweden", + "Switzerland", + "Taiwan", + "Tajikistan", + "Tanzania", + "Thailand", + "Tonga", + "Trinidad and Tobago", + "Tunisia", + "Turkey", + "Turkmenistan", + "Turks and Caicos Islands", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "United States", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Venezuela", + "Vietnam", + "British Virgin Islands", + "Yemen", + "Democratic Republic of the Congo", + "Zambia", + "Zimbabwe", + "Montenegro", + "Serbia", + "Kosovo" + ], + "label": [ + "Warner Music Japan" + ], + "format": "Digital Media", + "monitored": true + } + ], + "genres": [], + "media": [ + { + "mediumNumber": 1, + "mediumName": "", + "mediumFormat": "Digital Media" + } + ], + "artist": { + "artistMetadataId": 14, + "status": "continuing", + "ended": false, + "artistName": "TWICE", + "foreignArtistId": "8da127cc-c432-418f-b356-ef36210d82ac", + "tadbId": 0, + "discogsId": 0, + "overview": "Twice (Korean: 트와이스; RR: Teuwaiseu; Japanese: トゥワイス, Hepburn: To~uwaisu; commonly stylized in all caps as TWICE) is a South Korean girl group formed by JYP Entertainment. The group is composed of nine members: Nayeon, Jeongyeon, Momo, Sana, Jihyo, Mina, Dahyun, Chaeyoung, and Tzuyu. Twice was formed under the television program Sixteen (2015) and debuted on October 20, 2015, with the extended play (EP) The Story Begins.\nTwice rose to domestic fame in 2016 with their single \"Cheer Up\", which charted at number one on the Gaon Digital Chart, became the best-performing single of the year, and won \"Song of the Year\" at the Melon Music Awards and Mnet Asian Music Awards. Their next single, \"TT\", from their third EP Twicecoaster: Lane 1, topped the Gaon charts for four consecutive weeks. The EP was the highest selling Korean girl group album of 2016. Within 19 months after debut, Twice had already sold over 1.2 million units of their four EPs and special album. As of December 2020, the group has sold over 10 million albums cumulatively in South Korea and Japan, becoming the highest-selling K-Pop girl group of all time.The group debuted in Japan on June 28, 2017, under Warner Music Japan, with the release of a compilation album titled #Twice. The album charted at number 2 on the Oricon Albums Chart with the highest first-week album sales by a K-pop artist in Japan in two years. It was followed by the release of Twice's first original Japanese maxi single titled \"One More Time\" in October. Twice became the first Korean girl group to earn a platinum certification from the Recording Industry Association of Japan (RIAJ) for both an album and CD single in the same year. Twice ranked third in the Top Artist category of Billboard Japan's 2017 Year-end Rankings, and in 2019, they became the first Korean girl group to embark on a Japanese dome tour.\nTwice is the first female Korean act to simultaneously top both Billboard's World Albums and World Digital Song Sales charts with the release of their first studio album Twicetagram and its lead single \"Likey\" in 2017. With the release of their single \"Feel Special\" in 2019, Twice became the third female Korean act to chart into the Canadian Hot 100. After signing with Republic Records for American promotions as part of a partnership with JYP Entertainment, the group has charted into the US Billboard 200 with More & More and Eyes Wide Open in 2020 and Taste of Love and Formula of Love: O+T=<3 in 2021. Their first official English-language single, \"The Feels\", became their first song to enter the US Billboard Hot 100 and the UK Singles Chart, peaking at the 83rd and 80th positions of the charts, respectively. They have been dubbed the next \"Nation's Girl Group\", and their point choreography—including for \"Cheer Up\" (2016), \"TT\" (2016), \"Signal\" (2017), and \"What Is Love?\" (2018)—became dance crazes and viral memes imitated by many celebrities.", + "artistType": "Group", + "disambiguation": "South Korean girl group", + "links": [ + { + "url": "https://www.generasia.com/wiki/Twice", + "name": "generasia" + }, + { + "url": "http://twice.jype.com/", + "name": "jype" + }, + { + "url": "https://twitter.com/JYPETWICE", + "name": "twitter" + }, + { + "url": "https://www.facebook.com/JYPETWICE", + "name": "facebook" + }, + { + "url": "https://www.instagram.com/twicetagram/", + "name": "instagram" + }, + { + "url": "https://www.wikidata.org/wiki/Q20645861", + "name": "wikidata" + }, + { + "url": "http://fans.jype.com/twice", + "name": "jype" + }, + { + "url": "https://commons.wikimedia.org/wiki/File:Twice_performing_at_SAC_2016_02_(cropped).jpg", + "name": "wikimedia" + }, + { + "url": "https://www.discogs.com/artist/4786543", + "name": "discogs" + }, + { + "url": "https://www.last.fm/music/%ED%8A%B8%EC%99%80%EC%9D%B4%EC%8A%A4", + "name": "last" + }, + { + "url": "https://www.last.fm/music/TWICE", + "name": "last" + }, + { + "url": "https://commons.wikimedia.org/wiki/File:160507_Twice_guerrilla_concert.jpg", + "name": "wikimedia" + }, + { + "url": "https://open.spotify.com/artist/7n2Ycct7Beij7Dj7meI4X0", + "name": "spotify" + }, + { + "url": "http://www.twicejapan.com/", + "name": "twicejapan" + }, + { + "url": "https://www.instagram.com/jypetwice_japan/", + "name": "instagram" + }, + { + "url": "https://twitter.com/JYPETWICE_JAPAN", + "name": "twitter" + }, + { + "url": "https://itunes.apple.com/jp/artist/id1203816887", + "name": "apple" + }, + { + "url": "https://commons.wikimedia.org/wiki/File:(TV10)_%EC%97%AC%EC%9E%90%EC%B9%9C%EA%B5%AC%C2%B7%ED%8A%B8%EC%99%80%EC%9D%B4%EC%8A%A4%C2%B7%EB%B8%94%EB%9E%99%ED%95%91%ED%81%AC,_%EB%A0%88%EB%93%9C%EC%B9%B4%ED%8E%AB_%EA%B0%81%EC%96%91%EA%B0%81%EC%83%89_%ED%8C%A8%EC%85%98_%EC%97%B4%EC%A0%84_(2017_%EA%B3%A8%EB%93%A0%EB%94%94%EC%8A%A4%ED%81%AC_%EB%A0%88%EB%93%9C%EC%B9%B4%ED%8E%AB)_2m19s.jpg", + "name": "wikimedia" + }, + { + "url": "https://itunes.apple.com/us/artist/id1203816887", + "name": "apple" + }, + { + "url": "http://viaf.org/viaf/178150468353504172529", + "name": "viaf" + }, + { + "url": "https://www.deezer.com/artist/161553", + "name": "deezer" + }, + { + "url": "https://imvdb.com/n/twice", + "name": "imvdb" + }, + { + "url": "https://listen.tidal.com/artist/3577941", + "name": "tidal" + }, + { + "url": "https://www.youtube.com/TWICE", + "name": "youtube" + }, + { + "url": "https://www.youtube.com/twicejapan_official", + "name": "youtube" + }, + { + "url": "https://music.apple.com/mx/artist/1203816887", + "name": "apple" + }, + { + "url": "https://www.imdb.com/name/nm9652049/", + "name": "imdb" + }, + { + "url": "https://www.tiktok.com/@twice_tiktok_officialjp", + "name": "tiktok" + }, + { + "url": "https://music.youtube.com/channel/UCAq0pFGa2w9SjxOq0ZxKVIw", + "name": "youtube" + } + ], + "images": [ + { + "url": "http://assets.fanart.tv/fanart/music/8da127cc-c432-418f-b356-ef36210d82ac/musicbanner/twice-58fb678fb1219.jpg", + "coverType": "banner", + "extension": ".jpg" + }, + { + "url": "http://assets.fanart.tv/fanart/music/8da127cc-c432-418f-b356-ef36210d82ac/artistbackground/twice-619421e3c57cc.jpg", + "coverType": "fanart", + "extension": ".jpg" + }, + { + "url": "http://assets.fanart.tv/fanart/music/8da127cc-c432-418f-b356-ef36210d82ac/hdmusiclogo/twice-58d833d0a608a.png", + "coverType": "logo", + "extension": ".png" + }, + { + "url": "http://assets.fanart.tv/fanart/music/8da127cc-c432-418f-b356-ef36210d82ac/artistthumb/twice-58fb69c0c2b00.jpg", + "coverType": "poster", + "extension": ".jpg" + } + ], + "path": "/data/Library/Music/TWICE", + "qualityProfileId": 1, + "metadataProfileId": 1, + "monitored": true, + "monitorNewItems": "all", + "genres": [ + "Dance", + "Electronica", + "K-Pop", + "Pop", + "R&B" + ], + "cleanName": "twice", + "sortName": "twice", + "tags": [], + "added": "2022-07-30T19:32:06Z", + "ratings": { + "votes": 4, + "value": 9.5 + }, + "statistics": { + "albumCount": 0, + "trackFileCount": 0, + "trackCount": 0, + "totalTrackCount": 0, + "sizeOnDisk": 0, + "percentOfTracks": 0 + }, + "id": 9 + }, + "images": [ + { + "url": "/MediaCover/Albums/32/cover.jpg?lastWrite=637927379160000000", + "coverType": "cover", + "extension": ".jpg", + "remoteUrl": "https://imagecache.lidarr.audio/v1/caa/22bd49a1-f858-427d-94ee-1788b54fb508/32961181216-1200.jpg" + } + ], + "links": [], + "statistics": { + "trackFileCount": 9, + "trackCount": 9, + "totalTrackCount": 9, + "sizeOnDisk": 74968875, + "percentOfTracks": 100 + }, + "grabbed": false, + "id": 32 +} \ No newline at end of file diff --git a/src/modules/common/examples/request.json b/src/modules/common/examples/request.json new file mode 100644 index 000000000..486932600 --- /dev/null +++ b/src/modules/common/examples/request.json @@ -0,0 +1,47 @@ +{ + "id": 634649, + "mediaType": "movie", + "adult": false, + "genreIds": [ + 28, + 12, + 878 + ], + "originalLanguage": "en", + "originalTitle": "Spider-Man: No Way Home", + "overview": "Après les événements liés à l'affrontement avec Mysterio, l'identité secrète de Spider-Man a été révélée. Il est poursuivi par le gouvernement américain, qui l'accuse du meurtre de Mysterio, et traqué par les médias. Cet événement a également des conséquences terribles sur la vie de sa petite-amie M.J. et de son meilleur ami Ned. Désemparé, Peter Parker demande alors de l'aide au docteur Strange. Ce dernier lance un sort pour que tout le monde oublie que Peter est Spider-Man. Mais les choses ne se passent pas comme prévu, et cette action altère la stabilité de l'espace-temps. Cela ouvre le « multivers », un concept terrifiant dont ils ne savent quasiment rien...", + "popularity": 1643.549, + "releaseDate": "2021-12-15", + "title": "Spider-Man: No Way Home", + "video": false, + "voteAverage": 8, + "voteCount": 14510, + "backdropPath": "/ocUp7DJBIc8VJgLEw1prcyK1dYv.jpg", + "posterPath": "/3SyG7dq2q0ollxJ4pSsrqcfRmVj.jpg", + "mediaInfo": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 91, + "mediaType": "movie", + "tmdbId": 634649, + "tvdbId": null, + "imdbId": null, + "status": 5, + "status4k": 1, + "createdAt": "2021-11-15T15:15:57.000Z", + "updatedAt": "2022-08-01T08:40:19.000Z", + "lastSeasonChange": "2021-11-15T15:15:57.000Z", + "mediaAddedAt": "2021-12-23T12:04:39.000Z", + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 89, + "externalServiceId4k": null, + "externalServiceSlug": "634649", + "externalServiceSlug4k": null, + "ratingKey": "823", + "ratingKey4k": null, + "seasons": [], + "plexUrl": "https://app.plex.tv/desktop#!/server/719240db84d0795f30baa1c7283588fea536bb21/details?key=%2Flibrary%2Fmetadata%2F823", + "serviceUrl": "http://radarr:7878/movie/634649" + } +} \ No newline at end of file diff --git a/src/modules/common/examples/tvshow.json b/src/modules/common/examples/tvshow.json new file mode 100644 index 000000000..306b08e33 --- /dev/null +++ b/src/modules/common/examples/tvshow.json @@ -0,0 +1,110 @@ +{ + "seriesId": 37, + "episodeFileId": 7387, + "seasonNumber": 1, + "episodeNumber": 4, + "title": "Part IV", + "airDate": "2022-06-08", + "airDateUtc": "2022-06-08T07:00:00Z", + "overview": "Obi-Wan Kenobi plots a daring mission into enemy territory.", + "episodeFile": { + "seriesId": 37, + "seasonNumber": 1, + "relativePath": "Season 1/Obi-Wan.Kenobi.S01E04.1080p.WEB.h264-KOGi[rartv].mkv", + "path": "/tv/Obi-Wan Kenobi/Season 1/Obi-Wan.Kenobi.S01E04.1080p.WEB.h264-KOGi[rartv].mkv", + "size": 1893191174, + "dateAdded": "2022-06-08T07:32:27.158296Z", + "sceneName": "Obi-Wan.Kenobi.S01E04.1080p.WEB.h264-KOGi[rartv]", + "quality": { + "quality": { + "id": 3, + "name": "WEBDL-1080p", + "source": "web", + "resolution": 1080 + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "language": { + "id": 1, + "name": "English" + }, + "mediaInfo": { + "audioChannels": 5.1, + "audioCodec": "EAC3 Atmos", + "videoCodec": "h264" + }, + "originalFilePath": "Obi-Wan.Kenobi.S01E04.1080p.WEB.h264-KOGi[rarbg]/Obi-Wan.Kenobi.S01E04.1080p.WEB.h264-KOGi.mkv", + "qualityCutoffNotMet": false, + "id": 7387 + }, + "hasFile": true, + "monitored": true, + "unverifiedSceneNumbering": false, + "series": { + "title": "Obi-Wan Kenobi", + "sortTitle": "obiwan kenobi", + "seasonCount": 1, + "status": "ended", + "overview": "During the reign of the Empire, Obi-Wan Kenobi embarks on a crucial mission.", + "network": "Disney+", + "airTime": "03:00", + "images": [ + { + "coverType": "banner", + "url": "https://artworks.thetvdb.com/banners/v4/series/393199/banners/6290d38b8c283.jpg" + }, + { + "coverType": "poster", + "url": "https://artworks.thetvdb.com/banners/v4/series/393199/posters/629668351aca3.jpg" + }, + { + "coverType": "fanart", + "url": "https://artworks.thetvdb.com/banners/v4/series/393199/backgrounds/62912a0fe623d.jpg" + } + ], + "seasons": [ + { + "seasonNumber": 1, + "monitored": true + } + ], + "year": 2022, + "path": "/tv/Obi-Wan Kenobi", + "profileId": 1, + "languageProfileId": 1, + "seasonFolder": true, + "monitored": true, + "useSceneNumbering": false, + "runtime": 39, + "tvdbId": 393199, + "tvRageId": 0, + "tvMazeId": 52260, + "firstAired": "2022-05-27T00:00:00Z", + "lastInfoSync": "2022-07-22T03:36:34.392414Z", + "seriesType": "standard", + "cleanTitle": "obiwankenobi", + "imdbId": "tt8466564", + "titleSlug": "obi-wan-kenobi", + "certification": "TV-14", + "genres": [ + "Action", + "Adventure", + "Fantasy", + "Mini-Series", + "Science Fiction" + ], + "tags": [], + "added": "2022-05-03T20:22:10.47688Z", + "ratings": { + "votes": 0, + "value": 0 + }, + "qualityProfileId": 1, + "id": 37 + }, + "id": 1407 +} \ No newline at end of file From 439874e81139d4629944e39e471bd2fa158fa0f0 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:47:34 +0200 Subject: [PATCH 30/35] :lipstick: Calendar styling --- src/modules/calendar/CalendarModule.tsx | 239 ++++++++++++------------ 1 file changed, 120 insertions(+), 119 deletions(-) diff --git a/src/modules/calendar/CalendarModule.tsx b/src/modules/calendar/CalendarModule.tsx index cc9a03cc1..896faa940 100644 --- a/src/modules/calendar/CalendarModule.tsx +++ b/src/modules/calendar/CalendarModule.tsx @@ -12,6 +12,7 @@ import React, { useEffect, useState } from 'react'; import { Calendar } from '@mantine/dates'; import { IconCalendar as CalendarIcon } from '@tabler/icons'; import axios from 'axios'; +import { useDisclosure } from '@mantine/hooks'; import { useConfig } from '../../tools/state'; import { IModule } from '../ModuleTypes'; import { @@ -170,7 +171,7 @@ function DayComponent(props: any) { readarrmedias, }: { renderdate: Date; sonarrmedias: []; radarrmedias: []; lidarrmedias: []; readarrmedias: [] } = props; - const [opened, setOpened] = useState(false); + const [opened, { close, open }] = useDisclosure(false); const day = renderdate.getDate(); @@ -191,129 +192,129 @@ function DayComponent(props: any) { const date = new Date(media.inCinemas); return date.toDateString() === renderdate.toDateString(); }); - if ( - sonarrFiltered.length === 0 && - radarrFiltered.length === 0 && - lidarrFiltered.length === 0 && - readarrFiltered.length === 0 - ) { + const totalFiltered = [ + ...readarrFiltered, + ...lidarrFiltered, + ...sonarrFiltered, + ...radarrFiltered, + ]; + if (totalFiltered.length === 0) { return
{day}
; } return ( - { - setOpened(true); - }} + - {readarrFiltered.length > 0 && ( - - )} - {radarrFiltered.length > 0 && ( - - )} - {sonarrFiltered.length > 0 && ( - - )} - {lidarrFiltered.length > 0 && ( - - )} - setOpened(false)} - opened={opened} - > - + + + {readarrFiltered.length > 0 && ( + + )} + {radarrFiltered.length > 0 && ( + + )} + {sonarrFiltered.length > 0 && ( + + )} + {lidarrFiltered.length > 0 && ( + + )}
{day}
-
- - - {sonarrFiltered.map((media: any, index: number) => ( - - - {index < sonarrFiltered.length - 1 && } - - ))} - {radarrFiltered.length > 0 && sonarrFiltered.length > 0 && ( - - )} - {radarrFiltered.map((media: any, index: number) => ( - - - {index < radarrFiltered.length - 1 && } - - ))} - {sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && ( - - )} - {lidarrFiltered.map((media: any, index: number) => ( - - - {index < lidarrFiltered.length - 1 && } - - ))} - {lidarrFiltered.length > 0 && readarrFiltered.length > 0 && ( - - )} - {readarrFiltered.map((media: any, index: number) => ( - - - {index < readarrFiltered.length - 1 && } - - ))} - - -
-
+ + + + 1 ? totalFiltered.slice(0, 2).length * 150 : 200, + width: 400, + }} + > + {sonarrFiltered.map((media: any, index: number) => ( + + + {index < sonarrFiltered.length - 1 && } + + ))} + {radarrFiltered.length > 0 && sonarrFiltered.length > 0 && ( + + )} + {radarrFiltered.map((media: any, index: number) => ( + + + {index < radarrFiltered.length - 1 && } + + ))} + {sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && ( + + )} + {lidarrFiltered.map((media: any, index: number) => ( + + + {index < lidarrFiltered.length - 1 && } + + ))} + {lidarrFiltered.length > 0 && readarrFiltered.length > 0 && ( + + )} + {readarrFiltered.map((media: any, index: number) => ( + + + {index < readarrFiltered.length - 1 && } + + ))} + + + ); } From 9fa48360380d446ee7338ba4d691dc1c95016d16 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 13:52:07 +0200 Subject: [PATCH 31/35] :arrow_up: Upgrade to Mantine v5.1.0 (from v5.0.2) --- package.json | 20 +-- src/modules/downloads/DownloadsModule.tsx | 1 - src/modules/moduleWrapper.tsx | 2 - src/modules/overseerr/RequestModal.tsx | 15 +- yarn.lock | 188 +++++++++++----------- 5 files changed, 102 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index 818fbc35b..5cf103c2c 100644 --- a/package.json +++ b/package.json @@ -32,15 +32,15 @@ "@dnd-kit/utilities": "^3.2.0", "@emotion/react": "^11.10.0", "@emotion/server": "^11.10.0", - "@mantine/carousel": "^5.0.0", - "@mantine/core": "^5.0.2", - "@mantine/dates": "^5.0.2", - "@mantine/dropzone": "^5.0.2", - "@mantine/form": "^5.0.2", - "@mantine/hooks": "^5.0.2", - "@mantine/modals": "^5.0.3", - "@mantine/next": "^5.0.2", - "@mantine/notifications": "^5.0.2", + "@mantine/carousel": "^5.1.0", + "@mantine/core": "^5.1.0", + "@mantine/dates": "^5.1.0", + "@mantine/dropzone": "^5.1.0", + "@mantine/form": "^5.1.0", + "@mantine/hooks": "^5.1.0", + "@mantine/modals": "^5.1.0", + "@mantine/next": "^5.1.0", + "@mantine/notifications": "^5.1.0", "@mantine/prism": "^5.0.0", "@nivo/core": "^0.79.0", "@nivo/line": "^0.79.1", @@ -51,7 +51,7 @@ "cookies-next": "^2.1.1", "dayjs": "^1.11.4", "dockerode": "^3.3.2", - "embla-carousel-react": "^7.0.0-rc05", + "embla-carousel-react": "^7.0.0", "framer-motion": "^6.5.1", "js-file-download": "^0.4.12", "next": "12.1.6", diff --git a/src/modules/downloads/DownloadsModule.tsx b/src/modules/downloads/DownloadsModule.tsx index 769a8eeb3..2089cdef9 100644 --- a/src/modules/downloads/DownloadsModule.tsx +++ b/src/modules/downloads/DownloadsModule.tsx @@ -9,7 +9,6 @@ import { ScrollArea, Center, Image, - Stack, } from '@mantine/core'; import { IconDownload as Download } from '@tabler/icons'; import { useEffect, useState } from 'react'; diff --git a/src/modules/moduleWrapper.tsx b/src/modules/moduleWrapper.tsx index d6e08506b..fc23877dd 100644 --- a/src/modules/moduleWrapper.tsx +++ b/src/modules/moduleWrapper.tsx @@ -1,6 +1,5 @@ import { ActionIcon, - Box, Button, Card, Group, @@ -10,7 +9,6 @@ import { TextInput, useMantineColorScheme, } from '@mantine/core'; -import { useHover } from '@mantine/hooks'; import { IconAdjustments } from '@tabler/icons'; import { motion } from 'framer-motion'; import { useState } from 'react'; diff --git a/src/modules/overseerr/RequestModal.tsx b/src/modules/overseerr/RequestModal.tsx index e25637933..e31d1de9d 100644 --- a/src/modules/overseerr/RequestModal.tsx +++ b/src/modules/overseerr/RequestModal.tsx @@ -1,20 +1,9 @@ -import { - Alert, - Button, - Checkbox, - createStyles, - Group, - LoadingOverlay, - Modal, - Stack, - Table, -} from '@mantine/core'; -import { openConfirmModal } from '@mantine/modals'; +import { Alert, Button, Checkbox, createStyles, Group, Modal, Stack, Table } from '@mantine/core'; import { showNotification, updateNotification } from '@mantine/notifications'; import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons'; import axios from 'axios'; import Consola from 'consola'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useColorTheme } from '../../tools/color'; import { MovieResult } from './Movie.d'; import { MediaType, Result } from './SearchResult.d'; diff --git a/yarn.lock b/yarn.lock index 943f33277..ac758f17c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1084,126 +1084,127 @@ __metadata: languageName: node linkType: hard -"@mantine/carousel@npm:^5.0.0": - version: 5.0.0 - resolution: "@mantine/carousel@npm:5.0.0" +"@mantine/carousel@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/carousel@npm:5.1.0" dependencies: - "@mantine/utils": 5.0.0 + "@mantine/utils": 5.1.0 peerDependencies: - "@mantine/core": 5.0.0 - "@mantine/hooks": 5.0.0 - embla-carousel-react: 6.2.0 + "@mantine/core": 5.1.0 + "@mantine/hooks": 5.1.0 + embla-carousel-react: 7.0.0 react: ">=16.8.0" - checksum: 67930f5c4db077c250d40d1ecc641a0cd92e8223db3cb41a061fa3505d67fc8fdc7040bb74b4093512e053f4807d314e301d7d4c7039bad8b71a0dea771866d6 + checksum: 0aa6beda2c1c406ea1cf07ece919999a7591b838579c1a0b90f88d1491d75f0871418deb700976791e14d870543f7594ea28569af13b330444ed8c810ab47ac3 languageName: node linkType: hard -"@mantine/core@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/core@npm:5.0.2" +"@mantine/core@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/core@npm:5.1.0" dependencies: "@floating-ui/react-dom-interactions": 0.6.6 - "@mantine/styles": 5.0.2 - "@mantine/utils": 5.0.2 + "@mantine/styles": 5.1.0 + "@mantine/utils": 5.1.0 "@radix-ui/react-scroll-area": 1.0.0 react-textarea-autosize: 8.3.4 peerDependencies: - "@mantine/hooks": 5.0.2 + "@mantine/hooks": 5.1.0 react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 3677995cb6b31481b29ee56096ba9366d68062fb9e636e271685a2e0ec7df852231edbe111aded6b02e7ddf2c6abc20f6c6aead42a30d130d8687540406dcedd + checksum: a170d3a97c66fc78ade98a4ec01ff854e998d5af3acca46275c241bb74ed8e3980cdb71e40fed6b9ce1d210d3ec6dfc8bb71cd591b359d10f194dac3faa6f4b9 languageName: node linkType: hard -"@mantine/dates@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/dates@npm:5.0.2" +"@mantine/dates@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/dates@npm:5.1.0" dependencies: - "@mantine/utils": 5.0.2 + "@mantine/utils": 5.1.0 peerDependencies: - "@mantine/core": 5.0.2 - "@mantine/hooks": 5.0.2 + "@mantine/core": 5.1.0 + "@mantine/hooks": 5.1.0 dayjs: ">=1.0.0" react: ">=16.8.0" - checksum: 818fce70324347c870dd04354c9e4b29f7d4241f56b3d7fbac668de3b25efa29a265e2ba817a42bf7151548e89f17825b63047fb2694215ccad1c993a3b2a772 + checksum: 0352073ed3f553853f5024f319f170ff22f34317ad6fe4500f847828667763f9a1108b701c60754f159ddab35a91e68a29f60ee4f00f9b43fce6128f8d1d0d10 languageName: node linkType: hard -"@mantine/dropzone@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/dropzone@npm:5.0.2" +"@mantine/dropzone@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/dropzone@npm:5.1.0" dependencies: - "@mantine/utils": 5.0.2 + "@mantine/utils": 5.1.0 react-dropzone: 14.2.1 peerDependencies: - "@mantine/core": 5.0.2 - "@mantine/hooks": 5.0.2 + "@mantine/core": 5.1.0 + "@mantine/hooks": 5.1.0 react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 3837d3f4763a33407c197cf7ca3510b2d0051be5419bd48e421a0fc748545c409f49200ee18942932403bd770d76c6ac78770d1067b67dfcbe7837d6606b100a + checksum: 5a9c7fe0db1bf6af845a161c1620fb544fcebd376785abfb77d0e9cf2babef00068a5e8f32c7707f100ccf2f7caf6e75c817ec6d6fa8b5d470ebf749dcbd5624 languageName: node linkType: hard -"@mantine/form@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/form@npm:5.0.2" +"@mantine/form@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/form@npm:5.1.0" dependencies: + fast-deep-equal: ^3.1.3 klona: ^2.0.5 peerDependencies: react: ">=16.8.0" - checksum: db08b33a0e95a20fb4dffc22d58be7f5ece6bbd824b40fa1bccee310587b932f73492bdef1de8d8023777a8bf6d1c7fe76594dba319055b8865543b53f3c60e0 + checksum: 4727e7f8842918aa3adf58030d4f27083bb245661b540f0ad3d14d37fc14d1851e454f5462a78b773e3839aaf535d93ed4d4628782d04d0cb899a900d8e6b70b languageName: node linkType: hard -"@mantine/hooks@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/hooks@npm:5.0.2" +"@mantine/hooks@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/hooks@npm:5.1.0" peerDependencies: react: ">=16.8.0" - checksum: 792b2ce59bc5a3bfb14e3f12762eaeb1e9b7edf080d03e2515c68ddd6b54fb6923e9c6461e5bd1030c043801f241f09be15bae5d6236b58a4206203a4d0166c6 + checksum: de4c2c1fe408efddeda88c331242c14e6aa44f65a0d78fc9ca2812e07f9593f27dbc593afd4f320bc36e618bbb7d3cbe5869aebe6d7f2a8b9af8f342f1913a5a languageName: node linkType: hard -"@mantine/modals@npm:^5.0.3": - version: 5.0.3 - resolution: "@mantine/modals@npm:5.0.3" +"@mantine/modals@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/modals@npm:5.1.0" dependencies: - "@mantine/utils": 5.0.3 + "@mantine/utils": 5.1.0 peerDependencies: - "@mantine/core": 5.0.3 - "@mantine/hooks": 5.0.3 + "@mantine/core": 5.1.0 + "@mantine/hooks": 5.1.0 react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 4d38f867405a91421c7d84716cceeb34a766c476aa0159034342e1ee681aa990cd16863e55930a889b6149fdae1b43ba00703783d7d5d91a271db40c9d3c5ec9 + checksum: ab3d9d78f70b631bec7f0a89d0f4c5275b043fb147ac5db83c7be8ef5d80fa75e9a4560c470f8c888e13c00b1d533c5d8c89eb6c9ce19297c32178044946cb93 languageName: node linkType: hard -"@mantine/next@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/next@npm:5.0.2" +"@mantine/next@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/next@npm:5.1.0" dependencies: - "@mantine/ssr": 5.0.2 - "@mantine/styles": 5.0.2 + "@mantine/ssr": 5.1.0 + "@mantine/styles": 5.1.0 peerDependencies: next: "*" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: f03257fe69a36fc54e6d8eda2d4a791277eecc1642fe6ffccae6d252e728288ee30cd89acb16a35c58ac1b064f2e8ba7950b4f98a0f2b003d34ae6024c107159 + checksum: 422f53fa3b18525b2baf95988260782c0b4321fdb6d004f611830b456d492feaa53c40fb5f2ba8f546701ed8b608c8a01ff129d7f76c5dabacb164bf3af20529 languageName: node linkType: hard -"@mantine/notifications@npm:^5.0.2": - version: 5.0.2 - resolution: "@mantine/notifications@npm:5.0.2" +"@mantine/notifications@npm:^5.1.0": + version: 5.1.0 + resolution: "@mantine/notifications@npm:5.1.0" dependencies: - "@mantine/utils": 5.0.2 + "@mantine/utils": 5.1.0 react-transition-group: 4.4.2 peerDependencies: - "@mantine/core": 5.0.2 - "@mantine/hooks": 5.0.2 + "@mantine/core": 5.1.0 + "@mantine/hooks": 5.1.0 react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 6284e9e26fa64a20417eaf9593a935083bc95095a84d8ae98c32c22becec202419523d7bb71ecfb08e307d05d11565a8403da68da42252c867a351fa08435a63 + checksum: 0e86dd8f114b6b971f2ab72ac76e0faf9c592a59893e6ffbd0f22fef14abfe58610fc6bf3526b1258e95591d5b5472b0d7fef455a3caab5bca7b795d0fc4c545 languageName: node linkType: hard @@ -1222,24 +1223,24 @@ __metadata: languageName: node linkType: hard -"@mantine/ssr@npm:5.0.2": - version: 5.0.2 - resolution: "@mantine/ssr@npm:5.0.2" +"@mantine/ssr@npm:5.1.0": + version: 5.1.0 + resolution: "@mantine/ssr@npm:5.1.0" dependencies: - "@mantine/styles": 5.0.2 + "@mantine/styles": 5.1.0 html-react-parser: 1.4.12 peerDependencies: "@emotion/react": ">=11.9.0" "@emotion/server": ">=11.4.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 396f3da4cdd15dde1a6707350b428862127a66d204c5b625cab70b92729d709d42a4ffeeb8dfa267035648373f3f3e016d78e15ed9758b00266013475008e663 + checksum: 1b3f9d81eaccd7a43db21b496b2cb6e5ba019e96e05faf8109577330a54b6eb28b393204c1eb01639bde3c04ae503491b7913e4573806803388ed91947a0bfb0 languageName: node linkType: hard -"@mantine/styles@npm:5.0.2": - version: 5.0.2 - resolution: "@mantine/styles@npm:5.0.2" +"@mantine/styles@npm:5.1.0": + version: 5.1.0 + resolution: "@mantine/styles@npm:5.1.0" dependencies: clsx: 1.1.1 csstype: 3.0.9 @@ -1247,7 +1248,7 @@ __metadata: "@emotion/react": ">=11.9.0" react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: a602c1453007d52c2d779e3ff107b26f6072158c3d9ffda4be42276bdccfff4b7695cb48f41ead0aacd42af97d40d61b61b022c530200c60f8cf3db711721bee + checksum: 9d7dc4d55eeea09f86e205a33051d397da45c06ce213860d4f013b9770dde34256bd8056d76ceceb6993828f864df103b1967b2a9d9b0cbe29d7334ec2c30318 languageName: node linkType: hard @@ -1260,21 +1261,12 @@ __metadata: languageName: node linkType: hard -"@mantine/utils@npm:5.0.2": - version: 5.0.2 - resolution: "@mantine/utils@npm:5.0.2" +"@mantine/utils@npm:5.1.0": + version: 5.1.0 + resolution: "@mantine/utils@npm:5.1.0" peerDependencies: react: ">=16.8.0" - checksum: fe618eb37c8f900ea2ed7ba25a95c01d55a9cc6d3e82ee46d36446258e52a8828098a5d8bd0c53ca269bb876cffd4c8162110715da7e912f552a99b1f2f5ba22 - languageName: node - linkType: hard - -"@mantine/utils@npm:5.0.3": - version: 5.0.3 - resolution: "@mantine/utils@npm:5.0.3" - peerDependencies: - react: ">=16.8.0" - checksum: febde84bcb4369dad4f4476fbd864220608855a30921816ea7033689cc6c921212de2042ae5fc0bd446273874198c3775142c46dae6465f4173fa411798aff75 + checksum: f6d2dd28f97d9e2d09eea3db7d1f2e0a82c451bd7a0c80f9a38c421c7003052b822917d4128a055e39c5822c6de61ecb53728aa86ab726bf4dcda911ab47f650 languageName: node linkType: hard @@ -3515,21 +3507,21 @@ __metadata: languageName: node linkType: hard -"embla-carousel-react@npm:^7.0.0-rc05": - version: 7.0.0-rc05 - resolution: "embla-carousel-react@npm:7.0.0-rc05" +"embla-carousel-react@npm:^7.0.0": + version: 7.0.0 + resolution: "embla-carousel-react@npm:7.0.0" dependencies: - embla-carousel: 7.0.0-rc05 + embla-carousel: 7.0.0 peerDependencies: react: ^18.1.0 - checksum: d6d579b047e7ba106653c052e30b198f74288e7cfb501d3212e6516afa5417b9539415a546e38e21ba1fe97069db4c809be3317eaee2bd963bf530a6b73eef5c + checksum: d44b93901fb6a5be2236ce86115d7132a91f3e1943dc4d2cb0bccf045173008e947a35ebf0e345b90dc33ba06d8a0011913d45e2dbfd6cc47d58953bab96486e languageName: node linkType: hard -"embla-carousel@npm:7.0.0-rc05": - version: 7.0.0-rc05 - resolution: "embla-carousel@npm:7.0.0-rc05" - checksum: 7cfe080ab3bdfc013a7d4304a3deb6f2aeef34f1c8f613f5d5760995dcb91512787edac534830d5c22aaa803e6377b3964cf4d2a41eb519f1d6cd297d9a2cbee +"embla-carousel@npm:7.0.0": + version: 7.0.0 + resolution: "embla-carousel@npm:7.0.0" + checksum: e662d18caf4371c04673372bf0e9144aec4b97629bbf48eb623e938ef27bd9f8de0d0f5b344d67bfdc777cce011518b7e833ec74773338292701c7d7efacb779 languageName: node linkType: hard @@ -4643,15 +4635,15 @@ __metadata: "@dnd-kit/utilities": ^3.2.0 "@emotion/react": ^11.10.0 "@emotion/server": ^11.10.0 - "@mantine/carousel": ^5.0.0 - "@mantine/core": ^5.0.2 - "@mantine/dates": ^5.0.2 - "@mantine/dropzone": ^5.0.2 - "@mantine/form": ^5.0.2 - "@mantine/hooks": ^5.0.2 - "@mantine/modals": ^5.0.3 - "@mantine/next": ^5.0.2 - "@mantine/notifications": ^5.0.2 + "@mantine/carousel": ^5.1.0 + "@mantine/core": ^5.1.0 + "@mantine/dates": ^5.1.0 + "@mantine/dropzone": ^5.1.0 + "@mantine/form": ^5.1.0 + "@mantine/hooks": ^5.1.0 + "@mantine/modals": ^5.1.0 + "@mantine/next": ^5.1.0 + "@mantine/notifications": ^5.1.0 "@mantine/prism": ^5.0.0 "@next/bundle-analyzer": ^12.1.4 "@next/eslint-plugin-next": ^12.1.4 @@ -4670,7 +4662,7 @@ __metadata: cookies-next: ^2.1.1 dayjs: ^1.11.4 dockerode: ^3.3.2 - embla-carousel-react: ^7.0.0-rc05 + embla-carousel-react: ^7.0.0 eslint: ^8.20.0 eslint-config-airbnb: ^19.0.4 eslint-config-airbnb-typescript: ^17.0.0 From 20d61c8d2a6af7eca7c008a449a237356e8e9927 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 14:30:22 +0200 Subject: [PATCH 32/35] :package: Add package and fix bug in DownloadsModule --- package.json | 1 + src/components/Settings/ModuleEnabler.tsx | 1 + src/modules/downloads/DownloadsModule.tsx | 2 +- yarn.lock | 218 ++++++++++++++++++++-- 4 files changed, 210 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 5cf103c2c..27b16f743 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "prism-react-renderer": "^1.3.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "sharp": "^0.30.7", "systeminformation": "^5.12.1", "uuid": "^8.3.2", "yarn": "^1.22.19" diff --git a/src/components/Settings/ModuleEnabler.tsx b/src/components/Settings/ModuleEnabler.tsx index 61cfbac07..8de2a6107 100644 --- a/src/components/Settings/ModuleEnabler.tsx +++ b/src/components/Settings/ModuleEnabler.tsx @@ -13,6 +13,7 @@ export default function ModuleEnabler(props: any) { { diff --git a/src/modules/downloads/DownloadsModule.tsx b/src/modules/downloads/DownloadsModule.tsx index 2089cdef9..69fb53923 100644 --- a/src/modules/downloads/DownloadsModule.tsx +++ b/src/modules/downloads/DownloadsModule.tsx @@ -196,7 +196,7 @@ export default function DownloadComponent() { ) : (
- + No torrents found
)}
diff --git a/yarn.lock b/yarn.lock index ac758f17c..5527bb6e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,13 +2975,23 @@ __metadata: languageName: node linkType: hard -"color-name@npm:~1.1.4": +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 languageName: node linkType: hard +"color-string@npm:^1.9.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: ^1.0.0 + simple-swizzle: ^0.2.2 + checksum: c13fe7cff7885f603f49105827d621ce87f4571d78ba28ef4a3f1a104304748f620615e6bf065ecd2145d0d9dad83a3553f52bb25ede7239d18e9f81622f1cc5 + languageName: node + linkType: hard + "color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" @@ -2991,6 +3001,16 @@ __metadata: languageName: node linkType: hard +"color@npm:^4.2.3": + version: 4.2.3 + resolution: "color@npm:4.2.3" + dependencies: + color-convert: ^2.0.1 + color-string: ^1.9.0 + checksum: 0579629c02c631b426780038da929cca8e8d80a40158b09811a0112a107c62e10e4aad719843b791b1e658ab4e800558f2e87ca4522c8b32349d497ecb6adeb4 + languageName: node + linkType: hard + "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -3314,6 +3334,13 @@ __metadata: languageName: node linkType: hard +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -3373,6 +3400,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": + version: 2.0.1 + resolution: "detect-libc@npm:2.0.1" + checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 + languageName: node + linkType: hard + "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -4066,6 +4100,13 @@ __metadata: languageName: node linkType: hard +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 588c19847216421ed92befb521767b7018dc88f88b0576df98cb242f20961425e96a92cbece525ef28cc5becceae5d544ae0f5b9b5e2aa05acb13716ca5b3099 + languageName: node + linkType: hard + "expect@npm:^28.1.3": version: 28.1.3 resolution: "expect@npm:28.1.3" @@ -4418,6 +4459,13 @@ __metadata: languageName: node linkType: hard +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 14e448192a35c1e42efee94c9d01a10f42fe790375891a24b25261246ce9336ab9df5d274585aedd4568f7922246c2a78b8a8cd2571bfe99c693a9718e7dd0e3 + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -4682,6 +4730,7 @@ __metadata: prism-react-renderer: ^1.3.5 react: ^18.2.0 react-dom: ^18.2.0 + sharp: ^0.30.7 systeminformation: ^5.12.1 typescript: ^4.7.4 uuid: ^8.3.2 @@ -4884,6 +4933,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 + languageName: node + linkType: hard + "inline-style-parser@npm:0.1.1": version: 0.1.1 resolution: "inline-style-parser@npm:0.1.1" @@ -4923,6 +4979,13 @@ __metadata: languageName: node linkType: hard +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 977e64f54d91c8f169b59afcd80ff19227e9f5c791fa28fa2e5bce355cbaf6c2c356711b734656e80c9dd4a854dd7efcf7894402f1031dfc5de5d620775b4d5f + languageName: node + linkType: hard + "is-bigint@npm:^1.0.1": version: 1.0.4 resolution: "is-bigint@npm:1.0.4" @@ -5955,7 +6018,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.6, minimist@npm:~1.2.5": +"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.6, minimist@npm:~1.2.5": version: 1.2.6 resolution: "minimist@npm:1.2.6" checksum: d15428cd1e11eb14e1233bcfb88ae07ed7a147de251441d61158619dfb32c4d7e9061d09cab4825fdee18ecd6fce323228c8c47b5ba7cd20af378ca4048fb3fb @@ -6032,7 +6095,7 @@ __metadata: languageName: node linkType: hard -"mkdirp-classic@npm:^0.5.2": +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": version: 0.5.3 resolution: "mkdirp-classic@npm:0.5.3" checksum: 3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac @@ -6104,6 +6167,13 @@ __metadata: languageName: node linkType: hard +"napi-build-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "napi-build-utils@npm:1.0.2" + checksum: 06c14271ee966e108d55ae109f340976a9556c8603e888037145d6522726aebe89dd0c861b4b83947feaf6d39e79e08817559e8693deedc2c94e82c5cbd090c7 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -6182,6 +6252,24 @@ __metadata: languageName: node linkType: hard +"node-abi@npm:^3.3.0": + version: 3.24.0 + resolution: "node-abi@npm:3.24.0" + dependencies: + semver: ^7.3.5 + checksum: d90ab48802497b2203800cac71018668e99c246435395ca4f67afcabf689e7e81568ed36e8036bae79a052b63ea5707375bece6ca0a1d2e2b99bfafde7a5c9b2 + languageName: node + linkType: hard + +"node-addon-api@npm:^5.0.0": + version: 5.0.0 + resolution: "node-addon-api@npm:5.0.0" + dependencies: + node-gyp: latest + checksum: 7c5e2043ac37f6108784d94ed73a44ae6d3e68eb968de60680922fc6bc3d17fa69448c0feb4e0c9d3f4c74a0324822e566a8340a56916d9d6f23cb3e85620334 + languageName: node + linkType: hard + "node-domexception@npm:1.0.0": version: 1.0.0 resolution: "node-domexception@npm:1.0.0" @@ -6584,6 +6672,28 @@ __metadata: languageName: node linkType: hard +"prebuild-install@npm:^7.1.1": + version: 7.1.1 + resolution: "prebuild-install@npm:7.1.1" + dependencies: + detect-libc: ^2.0.0 + expand-template: ^2.0.3 + github-from-package: 0.0.0 + minimist: ^1.2.3 + mkdirp-classic: ^0.5.3 + napi-build-utils: ^1.0.1 + node-abi: ^3.3.0 + pump: ^3.0.0 + rc: ^1.2.7 + simple-get: ^4.0.0 + tar-fs: ^2.0.0 + tunnel-agent: ^0.6.0 + bin: + prebuild-install: bin.js + checksum: dbf96d0146b6b5827fc8f67f72074d2e19c69628b9a7a0a17d0fad1bf37e9f06922896972e074197fc00a52eae912993e6ef5a0d471652f561df5cb516f3f467 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -6704,6 +6814,20 @@ __metadata: languageName: node linkType: hard +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: ^0.6.0 + ini: ~1.3.0 + minimist: ^1.2.0 + strip-json-comments: ~2.0.1 + bin: + rc: ./cli.js + checksum: 2e26e052f8be2abd64e6d1dabfbd7be03f80ec18ccbc49562d31f617d0015fbdbcf0f9eed30346ea6ab789e0fdfe4337f033f8016efdbee0df5354751842080e + languageName: node + linkType: hard + "react-dom@npm:^18.2.0": version: 18.2.0 resolution: "react-dom@npm:18.2.0" @@ -6996,6 +7120,13 @@ __metadata: languageName: node linkType: hard +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 + languageName: node + linkType: hard + "safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" @@ -7003,13 +7134,6 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - "safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -7053,6 +7177,23 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.30.7": + version: 0.30.7 + resolution: "sharp@npm:0.30.7" + dependencies: + color: ^4.2.3 + detect-libc: ^2.0.1 + node-addon-api: ^5.0.0 + node-gyp: latest + prebuild-install: ^7.1.1 + semver: ^7.3.7 + simple-get: ^4.0.1 + tar-fs: ^2.1.1 + tunnel-agent: ^0.6.0 + checksum: bbc63ca3c7ea8a5bff32cd77022cfea30e25a03f5bd031e935924bf6cf0e11e3388e8b0e22b3137bf8816aa73407f1e4fbeb190f3a35605c27ffca9f32b91601 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -7087,6 +7228,33 @@ __metadata: languageName: node linkType: hard +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0, simple-get@npm:^4.0.1": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: ^6.0.0 + once: ^1.3.1 + simple-concat: ^1.0.0 + checksum: e4132fd27cf7af230d853fa45c1b8ce900cb430dd0a3c6d3829649fe4f2b26574c803698076c4006450efb0fad2ba8c5455fbb5755d4b0a5ec42d4f12b31d27e + languageName: node + linkType: hard + +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: ^0.3.1 + checksum: a7f3f2ab5c76c4472d5c578df892e857323e452d9f392e1b5cf74b74db66e6294a1e1b8b390b519fa1b96b5b613f2a37db6cffef52c3f1f8f3c5ea64eb2d54c0 + languageName: node + linkType: hard + "sirv@npm:^1.0.7": version: 1.0.19 resolution: "sirv@npm:1.0.19" @@ -7341,6 +7509,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + "style-to-js@npm:1.1.0": version: 1.1.0 resolution: "style-to-js@npm:1.1.0" @@ -7444,6 +7619,18 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": + version: 2.1.1 + resolution: "tar-fs@npm:2.1.1" + dependencies: + chownr: ^1.1.1 + mkdirp-classic: ^0.5.2 + pump: ^3.0.0 + tar-stream: ^2.1.4 + checksum: f5b9a70059f5b2969e65f037b4e4da2daf0fa762d3d232ffd96e819e3f94665dbbbe62f76f084f1acb4dbdcce16c6e4dac08d12ffc6d24b8d76720f4d9cf032d + languageName: node + linkType: hard + "tar-fs@npm:~2.0.1": version: 2.0.1 resolution: "tar-fs@npm:2.0.1" @@ -7456,7 +7643,7 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^2.0.0": +"tar-stream@npm:^2.0.0, tar-stream@npm:^2.1.4": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -7606,6 +7793,15 @@ __metadata: languageName: node linkType: hard +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: ^5.0.1 + checksum: 05f6510358f8afc62a057b8b692f05d70c1782b70db86d6a1e0d5e28a32389e52fa6e7707b6c5ecccacc031462e4bc35af85ecfe4bbc341767917b7cf6965711 + languageName: node + linkType: hard + "tweetnacl@npm:^0.14.3": version: 0.14.5 resolution: "tweetnacl@npm:0.14.5" From 659222643c0ef4a15256503c10614a76ae9ef9b9 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 14:49:06 +0200 Subject: [PATCH 33/35] =?UTF-8?q?=F0=9F=90=B3=20Revert=20docker=20image=20?= =?UTF-8?q?change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index af28768ba..4480896d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/linuxserver/baseimage-alpine:3.16 +FROM node:16-alpine WORKDIR /app ENV NEXT_TELEMETRY_DISABLED 1 From 528e899066664d4e48dd6310c3637ad497a831cc Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 8 Aug 2022 15:17:51 +0200 Subject: [PATCH 34/35] =?UTF-8?q?=F0=9F=90=9B=20Fix=20overseerr=20api=20ke?= =?UTF-8?q?y=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AppShelf/AddAppShelfItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 621ba2d54..699110782 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -152,7 +152,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & return ( <> -
+
void } & {(form.values.type === 'Sonarr' || form.values.type === 'Radarr' || form.values.type === 'Lidarr' || + form.values.type === 'Overseerr' || form.values.type === 'Readarr') && ( <> Date: Mon, 8 Aug 2022 15:43:04 +0200 Subject: [PATCH 35/35] :art: Small styling changes --- src/components/Settings/ModuleEnabler.tsx | 3 +-- src/modules/downloads/DownloadsModule.tsx | 1 - src/modules/overseerr/RequestModal.tsx | 23 ++++++++++++++++++++--- src/pages/_app.tsx | 14 ++++++++++++++ src/tools/styles.ts | 13 ------------- 5 files changed, 35 insertions(+), 19 deletions(-) delete mode 100644 src/tools/styles.ts diff --git a/src/components/Settings/ModuleEnabler.tsx b/src/components/Settings/ModuleEnabler.tsx index 8de2a6107..7b7378f32 100644 --- a/src/components/Settings/ModuleEnabler.tsx +++ b/src/components/Settings/ModuleEnabler.tsx @@ -12,8 +12,7 @@ export default function ModuleEnabler(props: any) { {modules.map((module) => ( { diff --git a/src/modules/downloads/DownloadsModule.tsx b/src/modules/downloads/DownloadsModule.tsx index 69fb53923..d81de9123 100644 --- a/src/modules/downloads/DownloadsModule.tsx +++ b/src/modules/downloads/DownloadsModule.tsx @@ -8,7 +8,6 @@ import { Skeleton, ScrollArea, Center, - Image, } from '@mantine/core'; import { IconDownload as Download } from '@tabler/icons'; import { useEffect, useState } from 'react'; diff --git a/src/modules/overseerr/RequestModal.tsx b/src/modules/overseerr/RequestModal.tsx index e31d1de9d..ee23e2678 100644 --- a/src/modules/overseerr/RequestModal.tsx +++ b/src/modules/overseerr/RequestModal.tsx @@ -82,8 +82,11 @@ export function MovieRequestModal({ This request will be automatically approved - + +