🔀 Merge pull request #326 from ajnart/overseerr-integration

 Overseerr integration
This commit is contained in:
Thomas Camlong
2022-08-08 16:00:26 +02:00
committed by GitHub
32 changed files with 6741 additions and 455 deletions

View File

@@ -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-,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

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.16
FROM node:16-alpine
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED 1

View File

@@ -32,30 +32,33 @@
"@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/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",
"@tabler/icons": "^1.78.0",
"add": "^2.0.6",
"axios": "^0.27.2",
"consola": "^2.15.3",
"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",
"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"

View File

@@ -18,10 +18,10 @@ 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';
import { useDebouncedValue } from '@mantine/hooks';
import { useConfig } from '../../tools/state';
import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
@@ -152,7 +152,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
return (
<>
<Center>
<Center mb="lg">
<Image
height={120}
width={120}
@@ -253,6 +253,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') && (
<>
<TextInput

View File

@@ -12,7 +12,7 @@ export default function ModuleEnabler(props: any) {
{modules.map((module) => (
<Checkbox
key={module.title}
size="lg"
size="md"
checked={config.modules?.[module.title]?.enabled ?? false}
label={`${module.title}`}
onChange={(e) => {

View File

@@ -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 <div>{day}</div>;
}
return (
<Box
onClick={() => {
setOpened(true);
}}
<Popover
position="bottom"
withArrow
withinPortal
radius="lg"
shadow="sm"
transition="pop"
onClose={close}
opened={opened}
>
{readarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
left: 8,
}}
color="red"
children={null}
/>
)}
{radarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
left: 8,
}}
color="yellow"
children={null}
/>
)}
{sonarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
right: 8,
}}
color="blue"
children={null}
/>
)}
{lidarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
right: 8,
}}
color="green"
children={undefined}
/>
)}
<Popover
position="bottom"
withinPortal
radius="lg"
shadow="xl"
transition="pop"
styles={{
dropdown: {
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.1), 0 14px 11px rgba(0, 0, 0, 0.1)',
},
}}
width="auto"
onClose={() => setOpened(false)}
opened={opened}
>
<Popover.Target>
<Popover.Target>
<Box onClick={open}>
{readarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
left: 8,
}}
color="red"
children={null}
/>
)}
{radarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
left: 8,
}}
color="yellow"
children={null}
/>
)}
{sonarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
top: 8,
right: 8,
}}
color="blue"
children={null}
/>
)}
{lidarrFiltered.length > 0 && (
<Indicator
size={10}
withBorder
style={{
position: 'absolute',
bottom: 8,
right: 8,
}}
color="green"
children={undefined}
/>
)}
<div>{day}</div>
</Popover.Target>
<Popover.Dropdown>
<ScrollArea style={{ height: 400 }}>
{sonarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<SonarrMediaDisplay media={media} />
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{radarrFiltered.length > 0 && sonarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{radarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<RadarrMediaDisplay media={media} />
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{lidarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<LidarrMediaDisplay media={media} />
{index < lidarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
{lidarrFiltered.length > 0 && readarrFiltered.length > 0 && (
<Divider variant="dashed" my="xl" />
)}
{readarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<ReadarrMediaDisplay media={media} />
{index < readarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
</ScrollArea>
</Popover.Dropdown>
</Popover>
</Box>
</Box>
</Popover.Target>
<Popover.Dropdown>
<ScrollArea
offsetScrollbars
scrollbarSize={5}
style={{
height:
totalFiltered.slice(0, 2).length > 1 ? totalFiltered.slice(0, 2).length * 150 : 200,
width: 400,
}}
>
{sonarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<SonarrMediaDisplay media={media} />
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
</React.Fragment>
))}
{radarrFiltered.length > 0 && sonarrFiltered.length > 0 && (
<Divider variant="dashed" size="sm" my="xl" />
)}
{radarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<RadarrMediaDisplay media={media} />
{index < radarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
</React.Fragment>
))}
{sonarrFiltered.length > 0 && lidarrFiltered.length > 0 && (
<Divider variant="dashed" size="sm" my="xl" />
)}
{lidarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<LidarrMediaDisplay media={media} />
{index < lidarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
</React.Fragment>
))}
{lidarrFiltered.length > 0 && readarrFiltered.length > 0 && (
<Divider variant="dashed" size="sm" my="xl" />
)}
{readarrFiltered.map((media: any, index: number) => (
<React.Fragment key={index}>
<ReadarrMediaDisplay media={media} />
{index < readarrFiltered.length - 1 && <Divider variant="dashed" size="sm" my="xl" />}
</React.Fragment>
))}
</ScrollArea>
</Popover.Dropdown>
</Popover>
);
}

View File

@@ -1,103 +1,45 @@
import {
Image,
Group,
Title,
Badge,
Text,
ActionIcon,
Anchor,
ScrollArea,
createStyles,
Stack,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { IconLink as Link } 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[];
seasonNumber?: number;
plexUrl?: string;
episodeNumber?: number;
[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 (
<Group position="apart">
<Text>
{media.poster && (
<Image
width={phone ? 250 : 100}
height={phone ? 400 : 160}
style={{
float: 'right',
}}
radius="md"
fit="cover"
src={media.poster}
alt={media.title}
/>
)}
<Stack style={{ minWidth: phone ? 450 : '65vw' }}>
<Group noWrap mr="sm" className={classes.overview}>
<Title order={3}>{media.title}</Title>
{media.imdbId && (
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
<ActionIcon>
<Link />
</ActionIcon>
</Anchor>
)}
</Group>
{media.artist && (
<Text
style={{
textAlign: 'center',
color: 'gray',
}}
>
New release from {media.artist}
</Text>
)}
{media.episodeNumber && media.seasonNumber && (
<Text
style={{
textAlign: 'center',
color: 'gray',
}}
>
Season {media.seasonNumber} episode {media.episodeNumber}
</Text>
)}
</Stack>
<Stack>
<ScrollArea style={{ height: 280, maxWidth: 700 }}>{media.overview}</ScrollArea>
<Group align="center" position="center" spacing="xs">
{media.genres.slice(-5).map((genre: string, i: number) => (
<Badge size="sm" key={i}>
{genre}
</Badge>
))}
</Group>
</Stack>
</Text>
</Group>
<MediaDisplay
media={{
...media,
genres: [],
overview: media.overview ?? '',
title: media.title ?? media.name ?? media.originalName ?? undefined,
poster: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${media.posterPath}`,
seasonNumber: media.mediaInfo?.seasons.length ?? undefined,
episodetitle: media.title ?? undefined,
plexUrl: media.mediaInfo?.plexUrl ?? undefined,
voteAverage: media.voteAverage?.toString() ?? undefined,
overseerrResult: media,
type: 'overseer',
}}
/>
);
}
@@ -118,11 +60,14 @@ export function ReadarrMediaDisplay(props: any) {
return (
<MediaDisplay
media={{
...media,
title: media.title,
poster: fullLink,
artist: media.author.authorName,
overview: media.overview,
genres: media.genres,
artist: media.authorTitle,
overview: `new book release by ${media.authorTitle}`,
genres: media.genres ?? [],
voteAverage: media.ratings.value.toString() ?? undefined,
type: 'book',
}}
/>
);
@@ -145,6 +90,7 @@ export function LidarrMediaDisplay(props: any) {
return (
<MediaDisplay
media={{
type: 'music',
title: media.title,
poster: fullLink,
artist: media.artist.artistName,
@@ -158,16 +104,17 @@ export function LidarrMediaDisplay(props: any) {
export function RadarrMediaDisplay(props: any) {
const { media }: { media: any } = props;
// Find a poster CoverType
const poster = media.images.find((image: any) => image.coverType === 'poster');
// Return a movie poster containting the title and the description
return (
<MediaDisplay
media={{
imdbId: media.imdbId,
title: media.title,
overview: media.overview,
poster: poster.url,
genres: media.genres,
...media,
title: media.title ?? media.originalTitle,
overview: media.overview ?? '',
genres: media.genres ?? [],
poster: media.images.find((image: any) => image.coverType === 'poster')?.url ?? undefined,
voteAverage: media.ratings.tmdb.value.toString() ?? undefined,
imdbId: media.imdbId ?? undefined,
type: 'movie',
}}
/>
);
@@ -181,14 +128,106 @@ export function SonarrMediaDisplay(props: any) {
return (
<MediaDisplay
media={{
imdbId: media.series.imdbId,
title: media.series.title,
overview: media.series.overview,
poster: poster.url,
genres: media.series.genres,
seasonNumber: media.seasonNumber,
episodeNumber: media.episodeNumber,
...media,
genres: media.series.genres ?? [],
overview: media.overview ?? media.series.overview ?? '',
title: media.series.title ?? undefined,
poster: poster ? poster.url : undefined,
episodeNumber: media.episodeNumber ?? undefined,
seasonNumber: media.seasonNumber ?? undefined,
episodetitle: media.title ?? undefined,
imdbId: media.series.imdbId ?? undefined,
voteAverage: media.series.ratings.value.toString() ?? undefined,
type: 'tvshow',
}}
/>
);
}
export function MediaDisplay({ media }: { media: IMedia }) {
const [opened, setOpened] = useState(false);
return (
<Group mr="xs" align="stretch" noWrap style={{ maxHeight: 200 }}>
<Image src={media.poster} height={200} width={150} radius="md" fit="cover" />
<Stack justify="space-around">
<Stack spacing="sm">
<Text lineClamp={2}>
<Title order={5}>{media.title}</Title>
</Text>
<Group spacing="xs">
{media.type === 'tvshow' && (
<Badge variant="dot" size="xs" radius="md" color="blue">
s{media.seasonNumber}e{media.episodeNumber} - {media.episodetitle}
</Badge>
)}
{media.type === 'music' && (
<Badge variant="dot" size="xs" radius="md" color="green">
{media.artist}
</Badge>
)}
{media.type === 'movie' && (
<Badge variant="dot" size="xs" radius="md" color="orange">
Radarr
</Badge>
)}
{media.type === 'book' && (
<Badge variant="dot" size="xs" radius="md" color="red">
Readarr
</Badge>
)}
{media.genres.slice(0, 2).map((genre) => (
<Badge size="xs" radius="md" key={genre}>
{genre}
</Badge>
))}
</Group>
<Text color="dimmed" size="xs" lineClamp={4}>
{media.overview}
</Text>
</Stack>
<Group grow>
{media.plexUrl && (
<Button
component="a"
target="_blank"
variant="outline"
href={media.plexUrl}
size="sm"
rightIcon={<IconPlayerPlay size={15} />}
>
Play
</Button>
)}
{media.imdbId && (
<Button
component="a"
target="_blank"
href={`https://www.imdb.com/title/${media.imdbId}`}
variant="outline"
size="sm"
rightIcon={<IconExternalLink size={15} />}
>
IMDb
</Button>
)}
{media.type === 'overseer' && (
<>
<RequestModal
base={media.overseerrResult as Result}
opened={opened}
setOpened={setOpened}
/>
<Button
onClick={() => setOpened(true)}
size="sm"
rightIcon={<IconDownload size={15} />}
>
Request
</Button>
</>
)}
</Group>
</Stack>
</Group>
);
}

View File

@@ -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
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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. Cest là que vit la famille Cody. Profession: criminels. Lirruption 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 dAnimal 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 sapprochent 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 lendroit 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": "Lhistoire 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 deux, auprès de son ami Ian, et, incroyablement ingénieux, il découvre le feu, la chasse, lhabitat moderne, lamour et même… lespoir. Généreux, il veut tout partager, révolutionne lordre é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: Disneys Animal Kingdom",
"overview": "",
"popularity": 0.6,
"releaseDate": "1998-04-14",
"title": "A New species of Theme Park: Disneys 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"
}
]
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -237,7 +237,6 @@ export function DashdotComponent() {
: ''
}`}
frameBorder="0"
allowTransparency
/>
))}
</div>

View File

@@ -8,8 +8,6 @@ import {
Skeleton,
ScrollArea,
Center,
Image,
Stack,
} from '@mantine/core';
import { IconDownload as Download } from '@tabler/icons';
import { useEffect, useState } from 'react';
@@ -189,23 +187,17 @@ export default function DownloadComponent() {
});
return (
<Stack mt="xl">
<ScrollArea sx={{ height: 300 }}>
{rows.length > 0 ? (
<Table highlightOnHover>
<thead>{ths}</thead>
<tbody>{rows}</tbody>
</Table>
) : (
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Image
fit="cover"
height={300}
src="https://danjohnvelasco.github.io/images/empty.png"
/>
</Center>
)}
</ScrollArea>
</Stack>
<ScrollArea mt="xl" sx={{ height: 300, width: '100%' }}>
{rows.length > 0 ? (
<Table highlightOnHover>
<thead>{ths}</thead>
<tbody>{rows}</tbody>
</Table>
) : (
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Title order={3}>No torrents found</Title>
</Center>
)}
</ScrollArea>
);
}

View File

@@ -6,3 +6,4 @@ export * from './ping';
export * from './search';
export * from './weather';
export * from './docker';
export * from './overseerr';

View File

@@ -1,6 +1,5 @@
import {
ActionIcon,
Box,
Button,
Card,
Group,
@@ -10,9 +9,9 @@ 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';
import { useConfig } from '../tools/state';
import { IModule } from './ModuleTypes';
@@ -148,7 +147,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 +157,6 @@ export function ModuleWrapper(props: any) {
<Card
{...props}
key={module.title}
ref={ref}
hidden={!isShown}
withBorder
radius="lg"
@@ -170,10 +168,17 @@ export function ModuleWrapper(props: any) {
${(config.settings.appOpacity || 100) / 100}`,
}}
>
<Group position="apart">
<ModuleMenu module={module} hovered={hovered} />
<motion.div
onHoverStart={() => {
setHovering(true);
}}
onHoverEnd={() => {
setHovering(false);
}}
>
<ModuleMenu module={module} hovered={hovering} />
<module.component />
</Group>
</motion.div>
</Card>
);
}
@@ -185,6 +190,7 @@ export function ModuleMenu(props: any) {
<>
{module.options && (
<Menu
key={module.title}
withinPortal
width="lg"
shadow="xl"
@@ -192,27 +198,23 @@ export function ModuleMenu(props: any) {
closeOnItemClick={false}
radius="md"
position="left"
styles={{
dropdown: {
// Add shadow and elevation to the body
boxShadow: '0 0 14px 14px rgba(0, 0, 0, 0.05)',
},
}}
>
<Menu.Target>
<Box
<motion.div
style={{
position: 'absolute',
top: 12,
right: 12,
top: 15,
right: 15,
alignSelf: 'flex-end',
}}
animate={{
opacity: hovered === true ? 1 : 0,
}}
>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: hovered ? 1 : 0 }}>
<ActionIcon>
<IconAdjustments />
</ActionIcon>
</motion.div>
</Box>
<ActionIcon>
<IconAdjustments />
</ActionIcon>
</motion.div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Settings</Menu.Label>

248
src/modules/overseerr/Movie.d.ts vendored Normal file
View File

@@ -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[];
}

View File

@@ -0,0 +1,14 @@
import { IconEyeglass } from '@tabler/icons';
import { OverseerrMediaDisplay } from '../common';
import { IModule } from '../ModuleTypes';
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;
}

View File

@@ -0,0 +1,240 @@
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 { 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;
opened: boolean;
setOpened: (opened: boolean) => void;
}
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 function RequestModal({ base, opened, setOpened }: RequestModalProps) {
const [result, setResult] = useState<MovieResult | TvShowResult>();
const { secondaryColor } = useColorTheme();
function getResults(base: Result) {
axios.get(`/api/modules/overseerr/${base.id}?type=${base.mediaType}`).then((res) => {
setResult(res.data);
});
}
if (opened && !result) {
getResults(base);
}
if (!result || !opened) {
return null;
}
return base.mediaType === 'movie' ? (
<MovieRequestModal result={result as MovieResult} opened={opened} setOpened={setOpened} />
) : (
<TvRequestModal result={result as TvShowResult} opened={opened} setOpened={setOpened} />
);
}
export function MovieRequestModal({
result,
opened,
setOpened,
}: {
result: MovieResult;
opened: boolean;
setOpened: (opened: boolean) => void;
}) {
const { secondaryColor } = useColorTheme();
return (
<Modal
onClose={() => setOpened(false)}
radius="lg"
size="lg"
trapFocus
zIndex={150}
withinPortal
opened={opened}
title={
<Group>
<IconDownload />
Ask for {result.title}
</Group>
}
>
<Stack>
<Alert
icon={<IconAlertCircle size={16} />}
title="Using API key"
color={secondaryColor}
radius="md"
variant="filled"
>
This request will be automatically approved
</Alert>
<Group>
<Button variant="outline" color="gray" onClick={() => setOpened(false)}>
Cancel
</Button>
<Button
variant="outline"
onClick={() => {
askForMedia(MediaType.Movie, result.id, result.title, []);
}}
>
Request
</Button>
</Group>
</Stack>
</Modal>
);
}
export function TvRequestModal({
result,
opened,
setOpened,
}: {
result: TvShowResult;
opened: boolean;
setOpened: (opened: boolean) => void;
}) {
const [selection, setSelection] = useState<TvShowResultSeason[]>(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 (
<tr key={element.id} className={cx({ [classes.rowSelected]: selected })}>
<td>
<Checkbox
key={element.id}
checked={selection.includes(element)}
onChange={() => toggleRow(element)}
transitionDuration={0}
/>
</td>
<td>{element.name}</td>
<td>{element.episodeCount}</td>
</tr>
);
});
const { secondaryColor } = useColorTheme();
return (
<Modal
onClose={() => setOpened(false)}
radius="lg"
size="lg"
opened={opened}
title={
<Group>
<IconDownload />
Ask for {result.name ?? result.originalName ?? 'a TV show'}
</Group>
}
>
<Stack>
<Alert
icon={<IconAlertCircle size={16} />}
title="Using API key"
color={secondaryColor}
radius="md"
variant="filled"
>
This request will be automatically approved
</Alert>
<Table captionSide="bottom" highlightOnHover>
<caption>Tick the seasons that you want to be downloaded</caption>
<thead>
<tr>
<th>
<Checkbox
onChange={toggleAll}
checked={selection.length === result.seasons.length}
indeterminate={selection.length > 0 && selection.length !== result.seasons.length}
transitionDuration={0}
/>
</th>
<th>Season</th>
<th>Number of episodes</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
<Group>
<Button variant="outline" color="gray" onClick={() => setOpened(false)}>
Cancel
</Button>
<Button
variant="outline"
disabled={selection.length === 0}
onClick={() => {
askForMedia(
MediaType.Tv,
result.id,
result.name,
selection.map((s) => s.seasonNumber)
);
}}
>
Request
</Button>
</Group>
</Stack>
</Modal>
);
}
function askForMedia(type: MediaType, id: number, name: string, seasons?: number[]) {
Consola.info(`Requesting ${type} ${id} ${name}`);
showNotification({
title: 'Request',
id: id.toString(),
message: `Requesting media ${name}`,
color: 'orange',
loading: true,
autoClose: false,
disallowClose: true,
icon: <IconAlertCircle />,
});
axios
.post(`/api/modules/overseerr/${id}`, { type, seasons })
.then(() => {
updateNotification({
id: id.toString(),
title: '',
color: 'green',
message: ` ${name} requested`,
icon: <IconCheck />,
autoClose: 2000,
});
})
.catch((err) => {
updateNotification({
id: id.toString(),
color: 'red',
title: 'There was an error',
message: err.message,
autoClose: 2000,
});
});
}

65
src/modules/overseerr/SearchResult.d.ts vendored Normal file
View File

@@ -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',
}

295
src/modules/overseerr/TvShow.d.ts vendored Normal file
View File

@@ -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[];
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1 @@
export { OverseerrModule } from './OverseerrModule';

View File

@@ -1,15 +1,19 @@
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,
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 '../common';
const useStyles = createStyles((theme) => ({
hide: {
@@ -23,47 +27,71 @@ 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 [icon, setIcon] = useState(<Search />);
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<any[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [icon, setIcon] = useState(<Search />);
const [results, setResults] = useState<any[]>([]);
const [opened, setOpened] = useState(false);
const textInput = useRef<HTMLInputElement>();
// 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<any[]>([]);
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 ', '')}`)
.then((res) => {
setOverseerrResults(res.data.results ?? []);
setLoading(false);
});
setLoading(true);
} 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 = (
<div className={classes.hide}>
<Kbd>Ctrl</Kbd>
<span style={{ margin: '0 5px' }}>+</span>
<Kbd>K</Kbd>
</div>
);
// 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,90 @@ 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(<BrandYoutube size={22} />);
} else if (isTorrent) {
setIcon(<Download size={22} />);
} else {
setIcon(<Search size={22} />);
switch (query.substring(0, 3)) {
case '!yt':
setIcon(<BrandYoutube />);
break;
case '!t ':
setIcon(<Download />);
break;
case '!os':
setIcon(<IconMovie />);
break;
default:
setIcon(<Search />);
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(3)}`);
} else if (isTorrent) {
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
} 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);
})}
>
<Autocomplete
autoFocus
variant="filled"
data={autocompleteData}
icon={icon}
ref={textInput}
rightSectionWidth={90}
rightSection={rightSection}
<Popover
opened={OverseerrResults.length > 0 && opened}
position="bottom"
withArrow
withinPortal
shadow="md"
radius="md"
size="md"
styles={{ rightSection: { pointerEvents: 'none' } }}
placeholder="Search the web..."
{...props}
{...form.getInputProps('query')}
/>
zIndex={100}
trapFocus
transition="pop-top-right"
>
<Popover.Target>
<Autocomplete
onFocusCapture={() => setOpened(true)}
autoFocus
variant="filled"
data={autocompleteData}
icon={icon}
ref={textInput}
rightSectionWidth={90}
rightSection={
<div className={classes.hide}>
<Kbd>Ctrl</Kbd>
<span style={{ margin: '0 5px' }}>+</span>
<Kbd>K</Kbd>
</div>
}
radius="md"
size="md"
styles={{ rightSection: { pointerEvents: 'none' } }}
placeholder="Search the web..."
{...props}
{...form.getInputProps('query')}
/>
</Popover.Target>
<Popover.Dropdown onMouseLeave={() => setOpened(false)}>
<ScrollArea style={{ height: 400, width: 400 }} offsetScrollbars>
{OverseerrResults.slice(0, 5).map((result, index) => (
<React.Fragment key={index}>
<OverseerrMediaDisplay key={result.id} media={result} />
{index < OverseerrResults.length - 1 && <Divider variant="dashed" my="xl" />}
</React.Fragment>
))}
</ScrollArea>
</Popover.Dropdown>
</Popover>
</form>
);
}

View File

@@ -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 { ColorTheme } from '../tools/color';
@@ -44,6 +45,20 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch
<MantineProvider
theme={{
...theme,
components: {
Checkbox: {
styles: {
input: { cursor: 'pointer' },
label: { cursor: 'pointer' },
},
},
Switch: {
styles: {
input: { cursor: 'pointer' },
label: { cursor: 'pointer' },
},
},
},
primaryColor,
primaryShade,
colorScheme,
@@ -52,9 +67,11 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch
withNormalizeCSS
>
<NotificationsProvider limit={4} position="bottom-left">
<ConfigProvider>
<Component {...pageProps} />
</ConfigProvider>
<ModalsProvider>
<ConfigProvider>
<Component {...pageProps} />
</ConfigProvider>
</ModalsProvider>
</NotificationsProvider>
</MantineProvider>
</ColorTheme.Provider>

View File

@@ -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',
});
};

View File

@@ -0,0 +1,43 @@
import axios from 'axios';
import { getCookie } from 'cookies-next';
import { NextApiRequest, NextApiResponse } from 'next';
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 } = 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([]);
}
if (!service || !query || service === undefined || !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 === 'GET') {
return Get(req, res);
}
return res.status(405).json({
statusCode: 405,
message: 'Method not allowed',
});
};

View File

@@ -1,13 +0,0 @@
import { MantineProviderProps } from '@mantine/core';
//TODO: Migarate this to v5.0
// export const styles: MantineProviderProps['styles'] = {
// Checkbox: {
// input: { cursor: 'pointer' },
// label: { cursor: 'pointer' },
// },
// Switch: {
// input: { cursor: 'pointer' },
// label: { cursor: 'pointer' },
// },
// };

View File

@@ -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 | undefined, form?: any) {
@@ -104,6 +106,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' },
];
@@ -167,7 +172,7 @@ export const MatchingImages: {
export interface serviceItem {
id: string;
name: string;
type: string;
type: ServiceType;
url: string;
icon: string;
category?: string;

404
yarn.lock
View File

@@ -1084,112 +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/next@npm:^5.0.2":
version: 5.0.2
resolution: "@mantine/next@npm:5.0.2"
"@mantine/modals@npm:^5.1.0":
version: 5.1.0
resolution: "@mantine/modals@npm:5.1.0"
dependencies:
"@mantine/ssr": 5.0.2
"@mantine/styles": 5.0.2
"@mantine/utils": 5.1.0
peerDependencies:
"@mantine/core": 5.1.0
"@mantine/hooks": 5.1.0
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: ab3d9d78f70b631bec7f0a89d0f4c5275b043fb147ac5db83c7be8ef5d80fa75e9a4560c470f8c888e13c00b1d533c5d8c89eb6c9ce19297c32178044946cb93
languageName: node
linkType: hard
"@mantine/next@npm:^5.1.0":
version: 5.1.0
resolution: "@mantine/next@npm:5.1.0"
dependencies:
"@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
@@ -1208,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
@@ -1233,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
@@ -1246,12 +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
checksum: f6d2dd28f97d9e2d09eea3db7d1f2e0a82c451bd7a0c80f9a38c421c7003052b822917d4128a055e39c5822c6de61ecb53728aa86ab726bf4dcda911ab47f650
languageName: node
linkType: hard
@@ -2960,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"
@@ -2976,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"
@@ -3016,6 +3051,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"
@@ -3292,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"
@@ -3351,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"
@@ -3485,21 +3541,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
@@ -4044,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"
@@ -4396,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"
@@ -4613,14 +4683,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/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
@@ -4635,10 +4706,11 @@ __metadata:
"@typescript-eslint/parser": ^5.30.7
add: ^2.0.6
axios: ^0.27.2
consola: ^2.15.3
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
@@ -4658,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
@@ -4860,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"
@@ -4899,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"
@@ -5931,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
@@ -6008,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
@@ -6080,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"
@@ -6158,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"
@@ -6560,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"
@@ -6680,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"
@@ -6972,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"
@@ -6979,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"
@@ -7029,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"
@@ -7063,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"
@@ -7317,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"
@@ -7420,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"
@@ -7432,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:
@@ -7582,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"