Merge branch 'develop'

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

@ -88,6 +88,15 @@
"contributions": [
"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>",

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

@ -12,6 +12,9 @@ RUN yarn cache clean
FROM node:12.18-alpine
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app
WORKDIR /app
@ -21,3 +24,5 @@ COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
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>
<img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr">
<!-- 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 -->
</p>
@ -104,6 +104,7 @@ Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_COND
<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/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>
</table>

@ -1478,6 +1478,29 @@ paths:
application/json:
schema:
$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:
get:
summary: Returns the currently logged in user

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

@ -76,11 +76,9 @@ class RadarrAPI {
}
};
public addMovie = async (
options: RadarrMovieOptions
): Promise<RadarrMovie> => {
public addMovie = async (options: RadarrMovieOptions): Promise<void> => {
try {
const response = await this.axios.post<RadarrMovie>(`/movie`, {
await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title,
qualityProfileId: options.qualityProfileId,
profileId: options.profileId,
@ -94,15 +92,15 @@ class RadarrAPI {
searchForMovie: options.searchNow,
},
});
return response.data;
} catch (e) {
logger.error('Something went wrong adding a movie to Radarr', {
label: 'Radarr',
message: e.message,
options,
});
throw new Error(`[Radarr] Failed to add movie: ${e.message}`);
logger.error(
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
{
label: 'Radarr',
errorMessage: 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;
ip: string;
port: number;
useSsl?: boolean;
libraries: Library[];
}
@ -109,6 +110,7 @@ class Settings {
name: '',
ip: '127.0.0.1',
port: 32400,
useSsl: false,
libraries: [],
},
radarr: [],

@ -17,6 +17,8 @@ import { scheduledJobs } from '../job/schedule';
import { Permission } from '../lib/permissions';
import { isAuthenticated } from '../middleware/auth';
import { merge } from 'lodash';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
const settingsRoutes = Router();
@ -431,4 +433,26 @@ settingsRoutes.post('/notifications/email', (req, res) => {
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;

@ -4,6 +4,7 @@ declare module 'plex-api' {
hostname: string;
port: number;
token?: string;
https?: boolean;
authenticator: {
authenticate: (
_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 { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Permission, useUser } from '../../hooks/useUser';
const messages = defineMessages({
alphawarning:
@ -14,6 +15,7 @@ const messages = defineMessages({
const Layout: React.FC = ({ children }) => {
const [isSidebarOpen, setSidebarOpen] = useState(false);
const { hasPermission } = useUser();
const router = useRouter();
return (
@ -57,7 +59,7 @@ const Layout: React.FC = ({ children }) => {
>
<div className="pt-2 pb-6 md:py-6">
<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="flex">
<div className="flex-shrink-0">
@ -85,7 +87,7 @@ const Layout: React.FC = ({ children }) => {
target="_blank"
rel="noreferrer"
>
Github &rarr;
GitHub &rarr;
</a>
</p>
</div>

@ -11,6 +11,7 @@ import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
createradarr: 'Create New Radarr Server',
editradarr: 'Edit Radarr Server',
validationNameRequired: 'You must provide a server name',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
@ -74,6 +75,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
rootFolders: [],
});
const RadarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
),
hostname: Yup.string().required(
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 type { PlexSettings } from '../../../server/lib/settings';
import useSWR from 'swr';
import { useFormik } from 'formik';
import { Formik, Field } from 'formik';
import Button from '../Common/Button';
import axios from 'axios';
import LibraryItem from './LibraryItem';
import Badge from '../Common/Badge';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import * as Yup from 'yup';
const messages = defineMessages({
plexsettings: 'Plex Settings',
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.',
servername: 'Server Name (Automatically Set)',
servername: 'Server Name (Automatically set after you save)',
servernamePlaceholder: 'Plex Server Name',
hostname: 'Hostname/IP',
port: 'Port',
ssl: 'SSL',
save: 'Save Changes',
saving: 'Saving...',
plexlibraries: 'Plex Libraries',
@ -32,6 +34,8 @@ const messages = defineMessages({
librariesRemaining: 'Libraries Remaining: {count}',
startscan: 'Start Scan',
cancelscan: 'Cancel Scan',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
});
interface Library {
@ -64,33 +68,15 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
}
);
const [isSyncing, setIsSyncing] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
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();
if (onComplete) {
onComplete();
}
} catch (e) {
setSubmitError(e.response.data.message);
} finally {
setIsUpdating(false);
}
},
const PlexSettingsSchema = Yup.object().shape({
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired)
),
});
const activeLibraries =
@ -164,91 +150,154 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<FormattedMessage {...messages.plexsettingsDescription} />
</p>
</div>
<form onSubmit={formik.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"
/>
<Formik
initialValues={{
hostname: data?.ip,
port: data?.port,
useSsl: data?.useSsl,
}}
enableReinitialize
validationSchema={PlexSettingsSchema}
onSubmit={async (values) => {
setSubmitError(null);
try {
await axios.post('/api/v1/settings/plex', {
ip: values.hostname,
port: Number(values.port),
useSsl: values.useSsl,
} as PlexSettings);
revalidate();
if (onComplete) {
onComplete();
}
} catch (e) {
setSubmitError(e.response.data.message);
}
}}
>
{({
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: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">
<input
type="text"
id="hostname"
name="hostname"
placeholder="127.0.0.1"
value={formik.values.hostname}
onChange={formik.handleChange}
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 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="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
{intl.formatMessage(messages.ssl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="useSsl"
name="useSsl"
onChange={() => {
setFieldValue('useSsl', !values.useSsl);
}}
className="form-checkbox h-6 w-6 rounded-md text-indigo-600 transition duration-150 ease-in-out"
/>
</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">
<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">
<input
type="text"
id="port"
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 className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</div>
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<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>
</form>
);
}}
</Formik>
<div className="mt-10">
<h3 className="text-lg leading-6 font-medium text-gray-200">
<FormattedMessage {...messages.plexlibraries} />

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

@ -135,9 +135,17 @@
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!",
"components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key",
"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.validationProfileRequired": "You must select a profile",
"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.apiKey": "API Key",
"components.Settings.SonarrModal.apiKeyPlaceholder": "Your Sonarr API Key",
@ -165,6 +173,7 @@
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!",
"components.Settings.SonarrModal.validationApiKeyRequired": "You must provide an API key",
"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.validationProfileRequired": "You must select a profile",
"components.Settings.SonarrModal.validationRootFolderRequired": "You must select a root folder",
@ -210,7 +219,7 @@
"components.Settings.runnow": "Run Now",
"components.Settings.save": "Save Changes",
"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.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",
@ -218,6 +227,8 @@
"components.Settings.startscan": "Start Scan",
"components.Settings.sync": "Sync Plex Libraries",
"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.configureservices": "Configure Services",
"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