mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-27 09:49:13 +01:00
🔀 Merge pull request #326 from ajnart/overseerr-integration
✨ Overseerr integration
This commit is contained in:
7
.github/workflows/docker_dev.yml
vendored
7
.github/workflows/docker_dev.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.16
|
||||
FROM node:16-alpine
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
21
package.json
21
package.json
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
57
src/modules/common/examples/book.json
Normal file
57
src/modules/common/examples/book.json
Normal 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
|
||||
}
|
||||
70
src/modules/common/examples/movie.json
Normal file
70
src/modules/common/examples/movie.json
Normal 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
|
||||
}
|
||||
3324
src/modules/common/examples/multiplemovies.json
Normal file
3324
src/modules/common/examples/multiplemovies.json
Normal file
File diff suppressed because it is too large
Load Diff
409
src/modules/common/examples/multipletvshows.json
Normal file
409
src/modules/common/examples/multipletvshows.json
Normal 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. C’est là que vit la famille Cody. Profession: criminels. L’irruption parmi eux de Joshua, un neveu éloigné, offre à la police le moyen de les infiltrer. Il ne reste plus à Joshua qu’à choisir son camp...",
|
||||
"popularity": 11.839,
|
||||
"releaseDate": "2010-06-03",
|
||||
"title": "Animal Kingdom",
|
||||
"video": false,
|
||||
"voteAverage": 6.8,
|
||||
"voteCount": 643,
|
||||
"backdropPath": "/dxOv6K3LNbZfQaGDyx7Tp94Koy.jpg",
|
||||
"posterPath": "/qrVjc5JcaujL58SMMW9lqrp3bBX.jpg"
|
||||
},
|
||||
{
|
||||
"id": 95731,
|
||||
"firstAirDate": "2020-09-25",
|
||||
"genreIds": [
|
||||
99
|
||||
],
|
||||
"mediaType": "tv",
|
||||
"name": "Au cœur de Disney's Animal Kingdom",
|
||||
"originCountry": [],
|
||||
"originalLanguage": "en",
|
||||
"originalName": "Magic of Disney's Animal Kingdom",
|
||||
"overview": "Au cœur d’Animal Kingdom narrée par Josh Gad, une célébrité parmi les fans de Disney, nous emmène en coulisses découvrir la magie de deux des animations animalières les plus visitées au monde : le parc à thème de Disney, Animal Kingdom, et The Seas with Nemo & Friends à Epcot. Les spectateurs s’approchent au plus près de créatures parmi les plus rares et les plus belles de la planète et rencontrent les experts en soins animaliers qui ont tissé des liens stupéfiants avec les 5 000 et plus animaux du parc. Chacun des huit épisodes plonge au cœur de l’endroit le plus magique sur Terre, dévoilant les multiples facettes de sa conception et de sa gestion.",
|
||||
"popularity": 3.367,
|
||||
"voteAverage": 8,
|
||||
"voteCount": 4,
|
||||
"backdropPath": "/gMTMnd54VVAbGiodBqMTGCjM3b2.jpg",
|
||||
"posterPath": "/gvNTeRAfu4KN3dD5HUO4Nbnri07.jpg"
|
||||
},
|
||||
{
|
||||
"id": 120862,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
35,
|
||||
18,
|
||||
10749
|
||||
],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "The Animal Kingdom",
|
||||
"overview": "Tom Collier, jeune éditeur, a entretenu une liaison passionnée et intellectuelle avec une dessinatrice, Daisy Sage. Celle-ci ayant mis un terme à leur relation, il a fait la connaissance de Cecilia, qu'il a rapidement décidé d'épouser. Alors que les fiançailles sont annoncées, Daisy, toujours amoureuse, fait son retour, mais trop tard. Le mariage a lieu. Sous l'influence de Cecilia, Tom Collier, qui était un éditeur intègre et exigeant, fait de plus en plus de concessions commerciales. Daisy, elle demeure fidèle à elle-même. Tom Collier, se retrouve a évoluer, par amour pour sa femme, dans un milieu de conventions bourgeoises qui ne l'intéressent pas.",
|
||||
"popularity": 2.102,
|
||||
"releaseDate": "1932-12-28",
|
||||
"title": "The Animal Kingdom",
|
||||
"video": false,
|
||||
"voteAverage": 6.3,
|
||||
"voteCount": 13,
|
||||
"backdropPath": "/5P1Hx46wvCVx9D9yT8M5rdUIHZB.jpg",
|
||||
"posterPath": "/3sLWwNvS77xynAGLkbiHVXlO3UH.jpg"
|
||||
},
|
||||
{
|
||||
"id": 311015,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
99
|
||||
],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Disney Parks: Disney's Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 1.208,
|
||||
"releaseDate": "2010-01-01",
|
||||
"title": "Disney Parks: Disney's Animal Kingdom",
|
||||
"video": true,
|
||||
"voteAverage": 9,
|
||||
"voteCount": 2,
|
||||
"backdropPath": null,
|
||||
"posterPath": "/93OEKY5vnKqGFbOyHtUAdcEz8NV.jpg"
|
||||
},
|
||||
{
|
||||
"id": 291774,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Kenya 3D: Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2013-03-08",
|
||||
"title": "Kenya 3D: Animal Kingdom",
|
||||
"video": false,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": null
|
||||
},
|
||||
{
|
||||
"id": 640253,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "it",
|
||||
"originalTitle": "Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2016-11-12",
|
||||
"title": "Animal Kingdom",
|
||||
"video": false,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": "/vJFK5cCcIh4X4op0oeK5iY2ibPv.jpg"
|
||||
},
|
||||
{
|
||||
"id": 507434,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
27
|
||||
],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2017-02-25",
|
||||
"title": "Animal Kingdom",
|
||||
"video": false,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": "/8QxSJRLLw2m8ymrFsC2xJ26yd1n.jpg",
|
||||
"posterPath": "/s77Q92boNGgkT2J5se3gwq5N8Xp.jpg"
|
||||
},
|
||||
{
|
||||
"id": 775877,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Disney's Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2004-05-12",
|
||||
"title": "Disney's Animal Kingdom",
|
||||
"video": true,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": null
|
||||
},
|
||||
{
|
||||
"id": 318575,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
99
|
||||
],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Nature: Love in the Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.655,
|
||||
"releaseDate": "2013-11-06",
|
||||
"title": "Nature: Love in the Animal Kingdom",
|
||||
"video": true,
|
||||
"voteAverage": 9.5,
|
||||
"voteCount": 2,
|
||||
"backdropPath": "/vx2dfrXPTn0dKoyIqCEgrGvzwkd.jpg",
|
||||
"posterPath": "/1fd53UCxtLAItNI5jMtVetFuw6v.jpg"
|
||||
},
|
||||
{
|
||||
"id": 743266,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Animal Kingdom: Great Are Thy Works",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "1993-01-01",
|
||||
"title": "Animal Kingdom: Great Are Thy Works",
|
||||
"video": false,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": "/vjnsGLvymjG7dAIbjwzgFCdbhl6.jpg"
|
||||
},
|
||||
{
|
||||
"id": 828152,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
99
|
||||
],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Disney's Animal Kingdom: Alive with Magic",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2017-06-27",
|
||||
"title": "Disney's Animal Kingdom: Alive with Magic",
|
||||
"video": false,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": "/amzVT8T9Ju3KLCDnBq4Rhf3LO8j.jpg"
|
||||
},
|
||||
{
|
||||
"id": 280391,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
12,
|
||||
35,
|
||||
16
|
||||
],
|
||||
"originalLanguage": "fr",
|
||||
"originalTitle": "Pourquoi j'ai pas mangé mon père",
|
||||
"overview": "L’histoire trépidante d’Édouard, fils aîné du roi des simiens, qui, considéré à sa naissance comme trop malingre, est rejeté par sa tribu. Il grandit loin d’eux, auprès de son ami Ian, et, incroyablement ingénieux, il découvre le feu, la chasse, l’habitat moderne, l’amour et même… l’espoir. Généreux, il veut tout partager, révolutionne l’ordre établi, et mène son peuple avec éclat et humour vers la véritable humanité… celle où on ne mange pas son père.",
|
||||
"popularity": 12.971,
|
||||
"releaseDate": "2015-04-08",
|
||||
"title": "Pourquoi j'ai pas mangé mon père",
|
||||
"video": false,
|
||||
"voteAverage": 5.3,
|
||||
"voteCount": 303,
|
||||
"backdropPath": "/msDLrSt7Ozpe6oOg4XJrsQJd2IE.jpg",
|
||||
"posterPath": "/efpzs2g1uRNcP8wPbIKSRPPH0aC.jpg"
|
||||
},
|
||||
{
|
||||
"id": 775559,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "A New species of Theme Park: Disney’s Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "1998-04-14",
|
||||
"title": "A New species of Theme Park: Disney’s Animal Kingdom",
|
||||
"video": true,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": null
|
||||
},
|
||||
{
|
||||
"id": 775831,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Disney Animal Kingdom Villas: A Village Comes to Life",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2007-06-14",
|
||||
"title": "Disney Animal Kingdom Villas: A Village Comes to Life",
|
||||
"video": true,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": null
|
||||
},
|
||||
{
|
||||
"id": 432906,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
99
|
||||
],
|
||||
"originalLanguage": "en",
|
||||
"originalTitle": "Out in Nature: Homosexual Behaviour in the Animal Kingdom",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2001-09-07",
|
||||
"title": "Out in Nature: Homosexual Behaviour in the Animal Kingdom",
|
||||
"video": false,
|
||||
"voteAverage": 6.8,
|
||||
"voteCount": 4,
|
||||
"backdropPath": null,
|
||||
"posterPath": "/jjxhR9ZxZ3vhauK8IDR6wIBlCLI.jpg"
|
||||
},
|
||||
{
|
||||
"id": 128887,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [
|
||||
16,
|
||||
35
|
||||
],
|
||||
"originalLanguage": "ja",
|
||||
"originalTitle": "クレヨンしんちゃん オタケべ!カスカベ野生王国",
|
||||
"overview": "",
|
||||
"popularity": 5.365,
|
||||
"releaseDate": "2009-04-18",
|
||||
"title": "クレヨンしんちゃん オタケべ!カスカベ野生王国",
|
||||
"video": false,
|
||||
"voteAverage": 8.5,
|
||||
"voteCount": 10,
|
||||
"backdropPath": "/azvwXB25Wvbx2Cou3Th7lbnjrqP.jpg",
|
||||
"posterPath": "/h7LipCtdCyBOKR1By5wSP2Ufy3c.jpg"
|
||||
},
|
||||
{
|
||||
"id": 579733,
|
||||
"mediaType": "movie",
|
||||
"adult": false,
|
||||
"genreIds": [],
|
||||
"originalLanguage": "no",
|
||||
"originalTitle": "Dyreriket",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"releaseDate": "2018-05-01",
|
||||
"title": "Dyreriket",
|
||||
"video": false,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": null,
|
||||
"posterPath": null
|
||||
},
|
||||
{
|
||||
"id": 111612,
|
||||
"firstAirDate": "2018-10-12",
|
||||
"genreIds": [
|
||||
10764
|
||||
],
|
||||
"mediaType": "tv",
|
||||
"name": "坂上どうぶつ王国",
|
||||
"originCountry": [
|
||||
"JP"
|
||||
],
|
||||
"originalLanguage": "ja",
|
||||
"originalName": "坂上どうぶつ王国",
|
||||
"overview": "",
|
||||
"popularity": 1.186,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": "/op8bK5R76L9QpwcVTnYG7nKXKsU.jpg",
|
||||
"posterPath": "/2VPq9RYaDohOT8YqTibKZMMT2Ue.jpg"
|
||||
},
|
||||
{
|
||||
"id": 156216,
|
||||
"firstAirDate": "2022-01-17",
|
||||
"genreIds": [
|
||||
16
|
||||
],
|
||||
"mediaType": "tv",
|
||||
"name": "动物王国的故事",
|
||||
"originCountry": [
|
||||
"CN"
|
||||
],
|
||||
"originalLanguage": "zh",
|
||||
"originalName": "动物王国的故事",
|
||||
"overview": "",
|
||||
"popularity": 0.6,
|
||||
"voteAverage": 0,
|
||||
"voteCount": 0,
|
||||
"backdropPath": "/uxIJQnjzIQn2MGHk17nNhoIEkxU.jpg",
|
||||
"posterPath": "/v90bqYZRUT30n22DdwahmW18LFn.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
832
src/modules/common/examples/music.json
Normal file
832
src/modules/common/examples/music.json
Normal 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
|
||||
}
|
||||
47
src/modules/common/examples/request.json
Normal file
47
src/modules/common/examples/request.json
Normal 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"
|
||||
}
|
||||
}
|
||||
110
src/modules/common/examples/tvshow.json
Normal file
110
src/modules/common/examples/tvshow.json
Normal 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
|
||||
}
|
||||
@@ -237,7 +237,6 @@ export function DashdotComponent() {
|
||||
: ''
|
||||
}`}
|
||||
frameBorder="0"
|
||||
allowTransparency
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './ping';
|
||||
export * from './search';
|
||||
export * from './weather';
|
||||
export * from './docker';
|
||||
export * from './overseerr';
|
||||
|
||||
@@ -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
248
src/modules/overseerr/Movie.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
14
src/modules/overseerr/OverseerrModule.tsx
Normal file
14
src/modules/overseerr/OverseerrModule.tsx
Normal 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;
|
||||
}
|
||||
240
src/modules/overseerr/RequestModal.tsx
Normal file
240
src/modules/overseerr/RequestModal.tsx
Normal 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
65
src/modules/overseerr/SearchResult.d.ts
vendored
Normal 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
295
src/modules/overseerr/TvShow.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
72
src/modules/overseerr/example.json
Normal file
72
src/modules/overseerr/example.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
src/modules/overseerr/index.ts
Normal file
1
src/modules/overseerr/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { OverseerrModule } from './OverseerrModule';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
126
src/pages/api/modules/overseerr/[id].tsx
Normal file
126
src/pages/api/modules/overseerr/[id].tsx
Normal 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',
|
||||
});
|
||||
};
|
||||
43
src/pages/api/modules/overseerr/index.ts
Normal file
43
src/pages/api/modules/overseerr/index.ts
Normal 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',
|
||||
});
|
||||
};
|
||||
@@ -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' },
|
||||
// },
|
||||
// };
|
||||
@@ -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
404
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user