Merge branch 'develop'

pull/570/head
sct 4 years ago
commit 7d2a187865

@ -88,6 +88,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "saltydk",
"name": "salty",
"avatar_url": "https://avatars1.githubusercontent.com/u/6587950?v=4",
"profile": "https://github.com/saltydk",
"contributions": [
"infra"
]
} }
], ],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>", "badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

@ -37,4 +37,5 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
repository: sctx/overseerr repository: sctx/overseerr
build_args: COMMIT_TAG=${{ github.sha }}
tags: develop tags: develop

@ -12,6 +12,9 @@ RUN yarn cache clean
FROM node:12.18-alpine FROM node:12.18-alpine
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
@ -21,3 +24,5 @@ COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
CMD yarn start CMD yarn start
EXPOSE 3000

@ -16,7 +16,7 @@
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a> <a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"> <img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-9-orange.svg"/></a> <a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-10-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
</p> </p>
@ -104,6 +104,7 @@ Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_COND
<tr> <tr>
<td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4" width="100px;" alt=""/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4" width="100px;" alt=""/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4" width="100px;" alt=""/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td> <td align="center"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4" width="100px;" alt=""/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/saltydk"><img src="https://avatars1.githubusercontent.com/u/6587950?v=4" width="100px;" alt=""/><br /><sub><b>salty</b></sub></a><br /><a href="#infra-saltydk" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr> </tr>
</table> </table>

@ -1478,6 +1478,29 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/DiscordSettings' $ref: '#/components/schemas/DiscordSettings'
/settings/about:
get:
summary: Return current about stats
description: Returns current server stats in JSON format
tags:
- settings
responses:
'200':
description: Returned about settings
content:
application/json:
schema:
type: object
properties:
version:
type: string
example: '1.0.0'
totalRequests:
type: number
example: 100
totalMediaItems:
type: number
example: 100
/auth/me: /auth/me:
get: get:
summary: Returns the currently logged in user summary: Returns the currently logged in user

@ -62,6 +62,7 @@ class PlexAPI {
this.plexClient = new NodePlexAPI({ this.plexClient = new NodePlexAPI({
hostname: settings.plex.ip, hostname: settings.plex.ip,
port: settings.plex.port, port: settings.plex.port,
https: settings.plex.useSsl,
token: plexToken, token: plexToken,
authenticator: { authenticator: {
authenticate: ( authenticate: (

@ -76,11 +76,9 @@ class RadarrAPI {
} }
}; };
public addMovie = async ( public addMovie = async (options: RadarrMovieOptions): Promise<void> => {
options: RadarrMovieOptions
): Promise<RadarrMovie> => {
try { try {
const response = await this.axios.post<RadarrMovie>(`/movie`, { await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title, title: options.title,
qualityProfileId: options.qualityProfileId, qualityProfileId: options.qualityProfileId,
profileId: options.profileId, profileId: options.profileId,
@ -94,15 +92,15 @@ class RadarrAPI {
searchForMovie: options.searchNow, searchForMovie: options.searchNow,
}, },
}); });
return response.data;
} catch (e) { } catch (e) {
logger.error('Something went wrong adding a movie to Radarr', { logger.error(
label: 'Radarr', 'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
message: e.message, {
options, label: 'Radarr',
}); errorMessage: e.message,
throw new Error(`[Radarr] Failed to add movie: ${e.message}`); options,
}
);
} }
}; };

@ -0,0 +1,5 @@
export interface SettingsAboutResponse {
version: string;
totalRequests: number;
totalMediaItems: number;
}

@ -14,6 +14,7 @@ export interface PlexSettings {
machineId?: string; machineId?: string;
ip: string; ip: string;
port: number; port: number;
useSsl?: boolean;
libraries: Library[]; libraries: Library[];
} }
@ -109,6 +110,7 @@ class Settings {
name: '', name: '',
ip: '127.0.0.1', ip: '127.0.0.1',
port: 32400, port: 32400,
useSsl: false,
libraries: [], libraries: [],
}, },
radarr: [], radarr: [],

@ -17,6 +17,8 @@ import { scheduledJobs } from '../job/schedule';
import { Permission } from '../lib/permissions'; import { Permission } from '../lib/permissions';
import { isAuthenticated } from '../middleware/auth'; import { isAuthenticated } from '../middleware/auth';
import { merge } from 'lodash'; import { merge } from 'lodash';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
const settingsRoutes = Router(); const settingsRoutes = Router();
@ -431,4 +433,26 @@ settingsRoutes.post('/notifications/email', (req, res) => {
res.status(200).json(settings.notifications.agents.email); res.status(200).json(settings.notifications.agents.email);
}); });
settingsRoutes.get('/about', async (req, res) => {
const mediaRepository = getRepository(Media);
const mediaRequestRepository = getRepository(MediaRequest);
const totalMediaItems = await mediaRepository.count();
const totalRequests = await mediaRequestRepository.count();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require('../../package.json');
let finalVersion = version;
if (version === '0.1.0') {
finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`;
}
return res.status(200).json({
version: finalVersion,
totalMediaItems,
totalRequests,
});
});
export default settingsRoutes; export default settingsRoutes;

@ -4,6 +4,7 @@ declare module 'plex-api' {
hostname: string; hostname: string;
port: number; port: number;
token?: string; token?: string;
https?: boolean;
authenticator: { authenticator: {
authenticate: ( authenticate: (
_plexApi: PlexAPI, _plexApi: PlexAPI,

@ -0,0 +1,40 @@
import React from 'react';
import { withProperties } from '../../../utils/typeHelpers';
interface ListItemProps {
title: string;
}
const ListItem: React.FC<ListItemProps> = ({ title, children }) => {
return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt className="text-sm font-medium text-gray-200">{title}</dt>
<dd className="mt-1 flex text-sm text-gray-400 sm:mt-0 sm:col-span-2">
<span className="flex-grow">{children}</span>
</dd>
</div>
);
};
interface ListProps {
title: string;
subTitle?: string;
}
const List: React.FC<ListProps> = ({ title, subTitle, children }) => {
return (
<>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-100">{title}</h3>
{subTitle && (
<p className="mt-1 max-w-2xl text-sm text-gray-300">{subTitle}</p>
)}
</div>
<div className="mt-5 border-t border-gray-800">
<dl className="divide-y divide-gray-800">{children}</dl>
</div>
</>
);
};
export default withProperties(List, { Item: ListItem });

@ -6,6 +6,7 @@ import Notifications from './Notifications';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage } from 'react-intl';
import { Permission, useUser } from '../../hooks/useUser';
const messages = defineMessages({ const messages = defineMessages({
alphawarning: alphawarning:
@ -14,6 +15,7 @@ const messages = defineMessages({
const Layout: React.FC = ({ children }) => { const Layout: React.FC = ({ children }) => {
const [isSidebarOpen, setSidebarOpen] = useState(false); const [isSidebarOpen, setSidebarOpen] = useState(false);
const { hasPermission } = useUser();
const router = useRouter(); const router = useRouter();
return ( return (
@ -57,7 +59,7 @@ const Layout: React.FC = ({ children }) => {
> >
<div className="pt-2 pb-6 md:py-6"> <div className="pt-2 pb-6 md:py-6">
<div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8"> <div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8">
{router.pathname === '/' && ( {router.pathname === '/' && hasPermission(Permission.ADMIN) && (
<div className="rounded-md bg-indigo-700 p-4 mt-2"> <div className="rounded-md bg-indigo-700 p-4 mt-2">
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@ -85,7 +87,7 @@ const Layout: React.FC = ({ children }) => {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
Github &rarr; GitHub &rarr;
</a> </a>
</p> </p>
</div> </div>

@ -11,6 +11,7 @@ import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
createradarr: 'Create New Radarr Server', createradarr: 'Create New Radarr Server',
editradarr: 'Edit Radarr Server', editradarr: 'Edit Radarr Server',
validationNameRequired: 'You must provide a server name',
validationHostnameRequired: 'You must provide a hostname/IP', validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port', validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key', validationApiKeyRequired: 'You must provide an API key',
@ -74,6 +75,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
rootFolders: [], rootFolders: [],
}); });
const RadarrSettingsSchema = Yup.object().shape({ const RadarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
),
hostname: Yup.string().required( hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired) intl.formatMessage(messages.validationHostnameRequired)
), ),

@ -0,0 +1,80 @@
import React from 'react';
import useSWR from 'swr';
import Error from '../../../pages/_error';
import List from '../../Common/List';
import LoadingSpinner from '../../Common/LoadingSpinner';
import { SettingsAboutResponse } from '../../../../server/interfaces/api/settingsInterfaces';
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
const messages = defineMessages({
overseerrinformation: 'Overseerr Information',
version: 'Version',
totalmedia: 'Total Media',
totalrequests: 'Total Requests',
gettingsupport: 'Getting Support',
githubdiscussions: 'GitHub Discussions',
clickheretojoindiscord: 'Click here to join our Discord server.',
});
const SettingsAbout: React.FC = () => {
const intl = useIntl();
const { data, error } = useSWR<SettingsAboutResponse>(
'/api/v1/settings/about'
);
if (error) {
return <Error statusCode={500} />;
}
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <LoadingSpinner />;
}
return (
<>
<div className="mb-8">
<List title={intl.formatMessage(messages.overseerrinformation)}>
<List.Item title={intl.formatMessage(messages.version)}>
{data.version}
</List.Item>
<List.Item title={intl.formatMessage(messages.totalmedia)}>
<FormattedNumber value={data.totalMediaItems} />
</List.Item>
<List.Item title={intl.formatMessage(messages.totalrequests)}>
<FormattedNumber value={data.totalRequests} />
</List.Item>
</List>
</div>
<div className="mb-8">
<List title={intl.formatMessage(messages.gettingsupport)}>
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
<a
href="https://github.com/sct/overseerr/discussions"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://github.com/sct/overseerr/discussions
</a>
</List.Item>
<List.Item title="Discord">
<a
href="https://discord.gg/PkCWJSeCk7"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
{intl.formatMessage(messages.clickheretojoindiscord)}
</a>
</List.Item>
</List>
</div>
</>
);
};
export default SettingsAbout;

@ -2,21 +2,23 @@ import React, { useState } from 'react';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
import type { PlexSettings } from '../../../server/lib/settings'; import type { PlexSettings } from '../../../server/lib/settings';
import useSWR from 'swr'; import useSWR from 'swr';
import { useFormik } from 'formik'; import { Formik, Field } from 'formik';
import Button from '../Common/Button'; import Button from '../Common/Button';
import axios from 'axios'; import axios from 'axios';
import LibraryItem from './LibraryItem'; import LibraryItem from './LibraryItem';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages({
plexsettings: 'Plex Settings', plexsettings: 'Plex Settings',
plexsettingsDescription: plexsettingsDescription:
'Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.', 'Configure the settings for your Plex server. Overseerr uses your Plex server to scan your library at an interval and see what content is available.',
servername: 'Server Name (Automatically Set)', servername: 'Server Name (Automatically set after you save)',
servernamePlaceholder: 'Plex Server Name', servernamePlaceholder: 'Plex Server Name',
hostname: 'Hostname/IP', hostname: 'Hostname/IP',
port: 'Port', port: 'Port',
ssl: 'SSL',
save: 'Save Changes', save: 'Save Changes',
saving: 'Saving...', saving: 'Saving...',
plexlibraries: 'Plex Libraries', plexlibraries: 'Plex Libraries',
@ -32,6 +34,8 @@ const messages = defineMessages({
librariesRemaining: 'Libraries Remaining: {count}', librariesRemaining: 'Libraries Remaining: {count}',
startscan: 'Start Scan', startscan: 'Start Scan',
cancelscan: 'Cancel Scan', cancelscan: 'Cancel Scan',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
}); });
interface Library { interface Library {
@ -64,33 +68,15 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
} }
); );
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
const formik = useFormik({
initialValues: {
hostname: data?.ip,
port: data?.port,
},
enableReinitialize: true,
onSubmit: async (values) => {
setSubmitError(null);
setIsUpdating(true);
try {
await axios.post('/api/v1/settings/plex', {
ip: values.hostname,
port: Number(values.port),
} as PlexSettings);
revalidate(); const PlexSettingsSchema = Yup.object().shape({
if (onComplete) { hostname: Yup.string().required(
onComplete(); intl.formatMessage(messages.validationHostnameRequired)
} ),
} catch (e) { port: Yup.number().required(
setSubmitError(e.response.data.message); intl.formatMessage(messages.validationPortRequired)
} finally { ),
setIsUpdating(false);
}
},
}); });
const activeLibraries = const activeLibraries =
@ -164,91 +150,154 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<FormattedMessage {...messages.plexsettingsDescription} /> <FormattedMessage {...messages.plexsettingsDescription} />
</p> </p>
</div> </div>
<form onSubmit={formik.handleSubmit}> <Formik
<div className="mt-6 sm:mt-5"> initialValues={{
{submitError && ( hostname: data?.ip,
<div className="bg-red-700 text-white p-4 rounded-md mb-6"> port: data?.port,
{submitError} useSsl: data?.useSsl,
</div> }}
)} enableReinitialize
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5"> validationSchema={PlexSettingsSchema}
<label onSubmit={async (values) => {
htmlFor="name" setSubmitError(null);
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2" try {
> await axios.post('/api/v1/settings/plex', {
<FormattedMessage {...messages.servername} /> ip: values.hostname,
</label> port: Number(values.port),
<div className="mt-1 sm:mt-0 sm:col-span-2"> useSsl: values.useSsl,
<div className="max-w-lg flex rounded-md shadow-sm"> } as PlexSettings);
<input
type="text" revalidate();
id="name" if (onComplete) {
name="name" onComplete();
placeholder={intl.formatMessage( }
messages.servernamePlaceholder } catch (e) {
)} setSubmitError(e.response.data.message);
value={data?.name} }
readOnly }}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500" >
/> {({
errors,
touched,
values,
handleSubmit,
setFieldValue,
isSubmitting,
}) => {
return (
<form onSubmit={handleSubmit}>
<div className="mt-6 sm:mt-5">
{submitError && (
<div className="bg-red-700 text-white p-4 rounded-md mb-6">
{submitError}
</div>
)}
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.servername} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<input
type="text"
id="name"
name="name"
placeholder={intl.formatMessage(
messages.servernamePlaceholder
)}
value={data?.name}
readOnly
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
</div>
</div>
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.hostname} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<Field
type="text"
id="hostname"
name="hostname"
placeholder="127.0.0.1"
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.hostname && touched.hostname && (
<div className="text-red-500 mt-2">{errors.hostname}</div>
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
<FormattedMessage {...messages.port} />
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
<Field
type="text"
id="port"
name="port"
placeholder="32400"
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.port && touched.port && (
<div className="text-red-500 mt-2">{errors.port}</div>
)}
</div>
</div>
</div> </div>
</div> <div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
</div> <label
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5"> htmlFor="ssl"
<label className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
htmlFor="hostname" >
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2" {intl.formatMessage(messages.ssl)}
> </label>
<FormattedMessage {...messages.hostname} /> <div className="mt-1 sm:mt-0 sm:col-span-2">
</label> <Field
<div className="mt-1 sm:mt-0 sm:col-span-2"> type="checkbox"
<div className="max-w-lg flex rounded-md shadow-sm"> id="useSsl"
<input name="useSsl"
type="text" onChange={() => {
id="hostname" setFieldValue('useSsl', !values.useSsl);
name="hostname" }}
placeholder="127.0.0.1" className="form-checkbox h-6 w-6 rounded-md text-indigo-600 transition duration-150 ease-in-out"
value={formik.values.hostname} />
onChange={formik.handleChange} </div>
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div> </div>
</div> <div className="mt-8 border-t border-gray-700 pt-5">
</div> <div className="flex justify-end">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"> <span className="ml-3 inline-flex rounded-md shadow-sm">
<label <Button
htmlFor="port" buttonType="primary"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2" type="submit"
> disabled={isSubmitting}
<FormattedMessage {...messages.port} /> >
</label> {isSubmitting
<div className="mt-1 sm:mt-0 sm:col-span-2"> ? intl.formatMessage(messages.saving)
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs"> : intl.formatMessage(messages.save)}
<input </Button>
type="text" </span>
id="port" </div>
name="port"
placeholder="32400"
value={formik.values.port}
onChange={formik.handleChange}
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div> </div>
</div> </form>
</div> );
</div> }}
<div className="mt-8 border-t border-gray-700 pt-5"> </Formik>
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button buttonType="primary" type="submit" disabled={isUpdating}>
{isUpdating
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</form>
<div className="mt-10"> <div className="mt-10">
<h3 className="text-lg leading-6 font-medium text-gray-200"> <h3 className="text-lg leading-6 font-medium text-gray-200">
<FormattedMessage {...messages.plexlibraries} /> <FormattedMessage {...messages.plexlibraries} />

@ -11,6 +11,7 @@ import { useIntl, defineMessages } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
createsonarr: 'Create New Sonarr Server', createsonarr: 'Create New Sonarr Server',
editsonarr: 'Edit Sonarr Server', editsonarr: 'Edit Sonarr Server',
validationNameRequired: 'You must provide a server name',
validationHostnameRequired: 'You must provide a hostname/IP', validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port', validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key', validationApiKeyRequired: 'You must provide an API key',
@ -73,6 +74,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
rootFolders: [], rootFolders: [],
}); });
const SonarrSettingsSchema = Yup.object().shape({ const SonarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
),
hostname: Yup.string().required( hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired) intl.formatMessage(messages.validationHostnameRequired)
), ),

@ -135,9 +135,17 @@
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!", "components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!",
"components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key", "components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key",
"components.Settings.RadarrModal.validationHostnameRequired": "You must provide a hostname/IP", "components.Settings.RadarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.RadarrModal.validationNameRequired": "You must provide a server name",
"components.Settings.RadarrModal.validationPortRequired": "You must provide a port", "components.Settings.RadarrModal.validationPortRequired": "You must provide a port",
"components.Settings.RadarrModal.validationProfileRequired": "You must select a profile", "components.Settings.RadarrModal.validationProfileRequired": "You must select a profile",
"components.Settings.RadarrModal.validationRootFolderRequired": "You must select a root folder", "components.Settings.RadarrModal.validationRootFolderRequired": "You must select a root folder",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Click here to join our Discord server.",
"components.Settings.SettingsAbout.gettingsupport": "Getting Support",
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions",
"components.Settings.SettingsAbout.overseerrinformation": "Overseerr Information",
"components.Settings.SettingsAbout.totalmedia": "Total Media",
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
"components.Settings.SettingsAbout.version": "Version",
"components.Settings.SonarrModal.add": "Add Server", "components.Settings.SonarrModal.add": "Add Server",
"components.Settings.SonarrModal.apiKey": "API Key", "components.Settings.SonarrModal.apiKey": "API Key",
"components.Settings.SonarrModal.apiKeyPlaceholder": "Your Sonarr API Key", "components.Settings.SonarrModal.apiKeyPlaceholder": "Your Sonarr API Key",
@ -165,6 +173,7 @@
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!", "components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!",
"components.Settings.SonarrModal.validationApiKeyRequired": "You must provide an API key", "components.Settings.SonarrModal.validationApiKeyRequired": "You must provide an API key",
"components.Settings.SonarrModal.validationHostnameRequired": "You must provide a hostname/IP", "components.Settings.SonarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.SonarrModal.validationNameRequired": "You must provide a server name",
"components.Settings.SonarrModal.validationPortRequired": "You must provide a port", "components.Settings.SonarrModal.validationPortRequired": "You must provide a port",
"components.Settings.SonarrModal.validationProfileRequired": "You must select a profile", "components.Settings.SonarrModal.validationProfileRequired": "You must select a profile",
"components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder", "components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder",
@ -210,7 +219,7 @@
"components.Settings.runnow": "Run Now", "components.Settings.runnow": "Run Now",
"components.Settings.save": "Save Changes", "components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving...", "components.Settings.saving": "Saving...",
"components.Settings.servername": "Server Name (Automatically Set)", "components.Settings.servername": "Server Name (Automatically set after you save)",
"components.Settings.servernamePlaceholder": "Plex Server Name", "components.Settings.servernamePlaceholder": "Plex Server Name",
"components.Settings.sonarrSettingsDescription": "Set up your Sonarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD and one for 4K). Administrators can override which server is used for new requests.", "components.Settings.sonarrSettingsDescription": "Set up your Sonarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD and one for 4K). Administrators can override which server is used for new requests.",
"components.Settings.sonarrsettings": "Sonarr Settings", "components.Settings.sonarrsettings": "Sonarr Settings",
@ -218,6 +227,8 @@
"components.Settings.startscan": "Start Scan", "components.Settings.startscan": "Start Scan",
"components.Settings.sync": "Sync Plex Libraries", "components.Settings.sync": "Sync Plex Libraries",
"components.Settings.syncing": "Syncing…", "components.Settings.syncing": "Syncing…",
"components.Settings.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.validationPortRequired": "You must provide a port",
"components.Setup.configureplex": "Configure Plex", "components.Setup.configureplex": "Configure Plex",
"components.Setup.configureservices": "Configure Services", "components.Setup.configureservices": "Configure Services",
"components.Setup.continue": "Continue", "components.Setup.continue": "Continue",

@ -0,0 +1,17 @@
import { NextPage } from 'next';
import React from 'react';
import SettingsAbout from '../../components/Settings/SettingsAbout';
import SettingsLayout from '../../components/Settings/SettingsLayout';
import useRouteGuard from '../../hooks/useRouteGuard';
import { Permission } from '../../hooks/useUser';
const SettingsAboutPage: NextPage = () => {
useRouteGuard(Permission.MANAGE_SETTINGS);
return (
<SettingsLayout>
<SettingsAbout />
</SettingsLayout>
);
};
export default SettingsAboutPage;
Loading…
Cancel
Save