feat(all): add initial Jellyfin/Emby support

This commit is contained in:
Aiden Vigue
2021-02-15 05:13:19 -05:00
parent 9ce88abcc8
commit a6ec2d5220
31 changed files with 2194 additions and 44 deletions

View File

@@ -0,0 +1,269 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button';
import { Field, Form, Formik } from 'formik';
import * as Yup from 'yup';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({
username: 'Username',
password: 'Password',
host: 'Jellyfin URL',
validationhostrequired: 'Jellyfin URL required',
validationhostformat: 'Valid URL required',
validationusernamerequired: 'Username required',
validationpasswordrequired: 'Password required',
loginerror: 'Something went wrong while trying to sign in.',
credentialerror: 'The username or password is incorrect.',
signingin: 'Signing in…',
signin: 'Sign In',
initialsigningin: 'Connecting…',
initialsignin: 'Connect',
forgotpassword: 'Forgot Password?',
});
interface JellyfinLoginProps {
revalidate: () => void;
initial?: boolean;
}
const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
revalidate,
initial,
}) => {
const toasts = useToasts();
const intl = useIntl();
const settings = useSettings();
if (initial) {
const LoginSchema = Yup.object().shape({
host: Yup.string()
.url(intl.formatMessage(messages.validationhostformat))
.required(intl.formatMessage(messages.validationhostrequired)),
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string().required(
intl.formatMessage(messages.validationpasswordrequired)
),
});
return (
<Formik
initialValues={{
username: '',
password: '',
host: '',
}}
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/auth/jellyfin', {
username: values.username,
password: values.password,
hostname: values.host,
});
} catch (e) {
toasts.addToast(
intl.formatMessage(
e.message == 'Request failed with status code 401'
? messages.credentialerror
: messages.loginerror
),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
revalidate();
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => (
<Form>
<div className="sm:border-t sm:border-gray-800">
<label htmlFor="host" className="text-label">
{intl.formatMessage(messages.host)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="flex rounded-md shadow-sm">
<Field
id="host"
name="host"
type="text"
placeholder={intl.formatMessage(messages.host)}
/>
</div>
{errors.host && touched.host && (
<div className="error">{errors.host}</div>
)}
</div>
<label htmlFor="username" className="text-label">
{intl.formatMessage(messages.username)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="flex rounded-md shadow-sm">
<Field
id="username"
name="username"
type="text"
placeholder={intl.formatMessage(messages.username)}
/>
</div>
{errors.username && touched.username && (
<div className="error">{errors.username}</div>
)}
</div>
<label htmlFor="password" className="text-label">
{intl.formatMessage(messages.password)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="shadow-sm flexrounded-md">
<Field
id="password"
name="password"
type="password"
placeholder={intl.formatMessage(messages.password)}
/>
</div>
{errors.password && touched.password && (
<div className="error">{errors.password}</div>
)}
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</Button>
</span>
</div>
</div>
</Form>
)}
</Formik>
);
} else {
const LoginSchema = Yup.object().shape({
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string().required(
intl.formatMessage(messages.validationpasswordrequired)
),
});
return (
<Formik
initialValues={{
username: '',
password: '',
}}
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/auth/jellyfin', {
username: values.username,
password: values.password,
});
} catch (e) {
toasts.addToast(
intl.formatMessage(
e.message == 'Request failed with status code 401'
? messages.credentialerror
: messages.loginerror
),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
revalidate();
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => {
return (
<>
<Form>
<div className="sm:border-t sm:border-gray-800">
<label htmlFor="username" className="text-label">
{intl.formatMessage(messages.username)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="username"
name="username"
type="text"
placeholder={intl.formatMessage(messages.username)}
/>
</div>
{errors.username && touched.username && (
<div className="error">{errors.username}</div>
)}
</div>
<label htmlFor="password" className="text-label">
{intl.formatMessage(messages.password)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="password"
name="password"
type="password"
placeholder={intl.formatMessage(messages.password)}
/>
</div>
{errors.password && touched.password && (
<div className="error">{errors.password}</div>
)}
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-between">
<span className="inline-flex rounded-md shadow-sm">
<Button
as="a"
buttonType="ghost"
href={
settings.currentSettings.jfHost +
'/web/#!/forgotpassword.html'
}
>
{intl.formatMessage(messages.forgotpassword)}
</Button>
</span>
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</Button>
</span>
</div>
</div>
</Form>
</>
);
}}
</Formik>
);
}
};
export default JellyfinLogin;

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import PlexLoginButton from '../PlexLoginButton';
import JellyfinLogin from './JellyfinLogin';
import { useUser } from '../../hooks/useUser';
import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
@@ -16,6 +17,7 @@ const messages = defineMessages({
signin: 'Sign In',
signinheader: 'Sign in to continue',
signinwithplex: 'Use your Plex account',
signinwithjellyfin: 'Use your Jellyfin account',
signinwithoverseerr: 'Use your {applicationTitle} account',
});
@@ -134,14 +136,20 @@ const Login: React.FC = () => {
onClick={() => handleClick(0)}
disabled={!settings.currentSettings.localLogin}
>
{intl.formatMessage(messages.signinwithplex)}
{settings.currentSettings.mediaServerType == 'PLEX'
? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(messages.signinwithjellyfin)}
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8">
<PlexLoginButton
isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)}
/>
{settings.currentSettings.mediaServerType == 'PLEX' ? (
<PlexLoginButton
isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)}
/>
) : (
<JellyfinLogin revalidate={revalidate} />
)}
</div>
</AccordionContent>
{settings.currentSettings.localLogin && (

View File

@@ -72,6 +72,8 @@ const messages = defineMessages({
downloadstatus: 'Download Status',
playonplex: 'Play on Plex',
play4konplex: 'Play 4K on Plex',
playonjellyfin: 'Play on Jellyfin',
play4konjellyfin: 'Play 4K on Jellyfin',
markavailable: 'Mark as Available',
mark4kavailable: 'Mark 4K as Available',
});
@@ -385,11 +387,33 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div>
<div className="flex flex-col flex-1 mt-4 text-center text-white lg:mr-4 lg:mt-0 lg:text-left">
<div className="mb-2 space-x-2">
<span className="ml-2 lg:ml-0">
{data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && (
<span className="ml-2 lg:ml-0">
<StatusBadge
status={data.mediaInfo?.status}
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
plexUrl={
data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl
}
plexUrl4k={
data.mediaInfo?.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k
}
/>
</span>
)}
<span>
<StatusBadge
status={data.mediaInfo?.status}
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl}
status={data.mediaInfo?.status4k}
is4k
inProgress={(data.mediaInfo?.downloadStatus4k ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl}
plexUrl4k={
data.mediaInfo?.plexUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE))
? data.mediaInfo.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k
: undefined
}
/>
</span>
{settings.currentSettings.movie4kEnabled &&

View File

@@ -3,7 +3,7 @@ import PlexOAuth from '../../utils/plex';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
signinwithplex: 'Sign In',
signinwithplex: 'Sign In with Plex',
loading: 'Loading…',
signingin: 'Signing in…',
});

View File

@@ -0,0 +1,283 @@
import React, { useState } from 'react';
import LoadingSpinner from '../Common/LoadingSpinner';
import type { JellyfinSettings } from '../../../server/lib/settings';
import useSWR from 'swr';
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';
const messages = defineMessages({
jellyfinsettings: 'Jellyfin Settings',
jellyfinsettingsDescription:
'Configure the settings for your Jellyfin server. Overseerr scans your Jellyfin libraries to see what content is available.',
timeout: 'Timeout',
save: 'Save Changes',
saving: 'Saving…',
jellyfinlibraries: 'Jellyfin Libraries',
jellyfinlibrariesDescription:
'The libraries Overseerr scans for titles. Click the button below if no libraries are listed.',
syncing: 'Syncing',
syncJellyfin: 'Sync Libraries',
manualscanJellyfin: 'Manual Library Scan',
manualscanDescriptionJellyfin:
"Normally, this will only be run once every 24 hours. Overseerr will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
notrunning: 'Not Running',
currentlibrary: 'Current Library: {name}',
librariesRemaining: 'Libraries Remaining: {count}',
startscan: 'Start Scan',
cancelscan: 'Cancel Scan',
});
interface Library {
id: string;
name: string;
enabled: boolean;
}
interface SyncStatus {
running: boolean;
progress: number;
total: number;
currentLibrary?: Library;
libraries: Library[];
}
interface SettingsJellyfinProps {
onComplete?: () => void;
}
const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({ onComplete }) => {
const [isSyncing, setIsSyncing] = useState(false);
const {
data: data,
error: error,
revalidate: revalidate,
} = useSWR<JellyfinSettings>('/api/v1/settings/jellyfin');
const { data: dataSync, revalidate: revalidateSync } = useSWR<SyncStatus>(
'/api/v1/settings/jellyfin/sync',
{
refreshInterval: 1000,
}
);
const intl = useIntl();
const activeLibraries =
data?.libraries
.filter((library) => library.enabled)
.map((library) => library.id) ?? [];
const syncLibraries = async () => {
setIsSyncing(true);
const params: { sync: boolean; enable?: string } = {
sync: true,
};
if (activeLibraries.length > 0) {
params.enable = activeLibraries.join(',');
}
await axios.get('/api/v1/settings/jellyfin/library', {
params,
});
setIsSyncing(false);
revalidate();
};
const startScan = async () => {
await axios.post('/api/v1/settings/jellyfin/sync', {
start: true,
});
revalidateSync();
};
const cancelScan = async () => {
await axios.post('/api/v1/settings/jellyfin/sync', {
cancel: true,
});
revalidateSync();
};
const toggleLibrary = async (libraryId: string) => {
setIsSyncing(true);
if (activeLibraries.includes(libraryId)) {
const params: { enable?: string } = {};
if (activeLibraries.length > 1) {
params.enable = activeLibraries
.filter((id) => id !== libraryId)
.join(',');
}
await axios.get('/api/v1/settings/jellyfin/library', {
params,
});
} else {
await axios.get('/api/v1/settings/jellyfin/library', {
params: {
enable: [...activeLibraries, libraryId].join(','),
},
});
}
if (onComplete) {
onComplete();
}
setIsSyncing(false);
revalidate();
};
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<>
<div className="mb-6">
<h3 className="heading">
<FormattedMessage {...messages.jellyfinlibraries} />
</h3>
<p className="description">
<FormattedMessage {...messages.jellyfinlibrariesDescription} />
</p>
</div>
<div className="section">
<Button onClick={() => syncLibraries()} disabled={isSyncing}>
<svg
className={`${isSyncing ? 'animate-spin' : ''} w-5 h-5 mr-1`}
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
{isSyncing
? intl.formatMessage(messages.syncing)
: intl.formatMessage(messages.syncJellyfin)}
</Button>
<ul className="grid grid-cols-1 gap-5 mt-6 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
{data?.libraries.map((library) => (
<LibraryItem
name={library.name}
isEnabled={library.enabled}
key={`setting-library-${library.id}`}
onToggle={() => toggleLibrary(library.id)}
/>
))}
</ul>
</div>
<div className="mt-10 mb-6">
<h3 className="heading">
<FormattedMessage {...messages.manualscanJellyfin} />
</h3>
<p className="description">
<FormattedMessage {...messages.manualscanDescriptionJellyfin} />
</p>
</div>
<div className="section">
<div className="p-4 bg-gray-800 rounded-md">
<div className="relative w-full h-8 mb-6 overflow-hidden bg-gray-600 rounded-full">
{dataSync?.running && (
<div
className="h-8 transition-all duration-200 ease-in-out bg-indigo-600"
style={{
width: `${Math.round(
(dataSync.progress / dataSync.total) * 100
)}%`,
}}
/>
)}
<div className="absolute inset-0 flex items-center justify-center w-full h-8 text-sm">
<span>
{dataSync?.running
? `${dataSync.progress} of ${dataSync.total}`
: 'Not running'}
</span>
</div>
</div>
<div className="flex flex-col w-full sm:flex-row">
{dataSync?.running && (
<>
{dataSync.currentLibrary && (
<div className="flex items-center mb-2 mr-0 sm:mb-0 sm:mr-2">
<Badge>
<FormattedMessage
{...messages.currentlibrary}
values={{ name: dataSync.currentLibrary.name }}
/>
</Badge>
</div>
)}
<div className="flex items-center">
<Badge badgeType="warning">
<FormattedMessage
{...messages.librariesRemaining}
values={{
count: dataSync.currentLibrary
? dataSync.libraries.slice(
dataSync.libraries.findIndex(
(library) =>
library.id === dataSync.currentLibrary?.id
) + 1
).length
: 0,
}}
/>
</Badge>
</div>
</>
)}
<div className="flex-1 text-right">
{!dataSync?.running && (
<Button buttonType="warning" onClick={() => startScan()}>
<svg
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<FormattedMessage {...messages.startscan} />
</Button>
)}
{dataSync?.running && (
<Button buttonType="danger" onClick={() => cancelScan()}>
<svg
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<FormattedMessage {...messages.cancelscan} />
</Button>
)}
</div>
</div>
</div>
</div>
</>
);
};
export default SettingsJellyfin;

View File

@@ -8,6 +8,7 @@ const messages = defineMessages({
settings: 'Settings',
menuGeneralSettings: 'General Settings',
menuPlexSettings: 'Plex',
menuJellyfinSettings: 'Jellyfin',
menuServices: 'Services',
menuNotifications: 'Notifications',
menuLogs: 'Logs',
@@ -36,6 +37,11 @@ const SettingsLayout: React.FC = ({ children }) => {
route: '/settings/plex',
regex: /^\/settings\/plex/,
},
{
text: intl.formatMessage(messages.menuJellyfinSettings),
route: '/settings/jellyfin',
regex: /^\/settings\/jellyfin/,
},
{
text: intl.formatMessage(messages.menuServices),
route: '/settings/services',

View File

@@ -35,6 +35,7 @@ const messages = defineMessages({
toastSettingsSuccess: 'Settings successfully saved!',
toastSettingsFailure: 'Something went wrong while saving settings.',
defaultPermissions: 'Default User Permissions',
useJellyfin: 'Use Jellyfin as Media Server',
hideAvailable: 'Hide Available Media',
csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip:
@@ -122,6 +123,7 @@ const SettingsMain: React.FC = () => {
region: data?.region,
originalLanguage: data?.originalLanguage,
trustProxy: data?.trustProxy,
useJellyfin: data?.mediaServerType == 'JELLYFIN' ? true : false,
}}
enableReinitialize
validationSchema={MainSettingsSchema}
@@ -137,6 +139,7 @@ const SettingsMain: React.FC = () => {
region: values.region,
originalLanguage: values.originalLanguage,
trustProxy: values.trustProxy,
mediaServerType: values.useJellyfin ? 'JELLYFIN' : 'PLEX',
});
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
@@ -360,6 +363,21 @@ const SettingsMain: React.FC = () => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="useJellyfin" className="checkbox-label">
<span>{intl.formatMessage(messages.useJellyfin)}</span>
</label>
<div className="form-input">
<Field
type="checkbox"
id="useJellyfin"
name="useJellyfin"
onChange={() => {
setFieldValue('useJellyfin', !values.useJellyfin);
}}
/>
</div>
</div>
<div
role="group"
aria-labelledby="group-label"

View File

@@ -0,0 +1,89 @@
import React, { useEffect, useState } from 'react';
import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
import JellyfinLogin from '../Login/JellyfinLogin';
import axios from 'axios';
import { defineMessages, FormattedMessage } from 'react-intl';
import LoadingSpinner from '../Common/LoadingSpinner';
const messages = defineMessages({
welcome: 'Welcome to Overseerr',
signinMessage: 'Get started by logging in with an account',
signinWithJellyfin: 'Use Jellyfin',
});
interface LoginWithMediaServerProps {
onComplete: () => void;
}
const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
const [mediaServerType, setMediaServerType] = useState<string>('');
const { user, revalidate } = useUser();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to login. If we get a success message, we will
// ask swr to revalidate the user which _shouid_ come back with a valid user.
useEffect(() => {
const login = async () => {
const response = await axios.post('/api/v1/auth/login', {
authToken: authToken,
});
if (response.data?.email) {
revalidate();
}
};
if (authToken && mediaServerType == 'PLEX') {
login();
}
}, [authToken, mediaServerType, revalidate]);
useEffect(() => {
if (user) {
onComplete();
}
}, [user, onComplete]);
return (
<div>
{mediaServerType == '' ? (
<React.Fragment>
<div className="flex justify-center mb-2 text-xl font-bold">
<FormattedMessage {...messages.welcome} />
</div>
<div className="flex justify-center pb-6 mb-2 text-sm">
<FormattedMessage {...messages.signinMessage} />
</div>
<div className="flex items-center justify-center pb-4">
<PlexLoginButton
onAuthToken={(authToken) => {
setMediaServerType('PLEX');
setAuthToken(authToken);
}}
/>
</div>
<hr className="m-auto border-gray-600 w-60"></hr>
<span className="block w-full rounded-md shadow-sm">
<button
type="button"
onClick={() => {
setMediaServerType('JELLYFIN');
}}
className="jellyfin-button"
>
<FormattedMessage {...messages.signinWithJellyfin} />
</button>
</span>
</React.Fragment>
) : mediaServerType == 'JELLYFIN' ? (
<JellyfinLogin initial={true} revalidate={revalidate} />
) : (
<LoadingSpinner></LoadingSpinner>
)}
</div>
);
};
export default SetupLogin;

View File

@@ -3,8 +3,9 @@ import React, { useState } from 'react';
import Button from '../Common/Button';
import ImageFader from '../Common/ImageFader';
import SettingsPlex from '../Settings/SettingsPlex';
import SettingsJellyfin from '../Settings/SettingsJellyfin';
import SettingsServices from '../Settings/SettingsServices';
import LoginWithPlex from './LoginWithPlex';
import SetupLogin from './SetupLogin';
import SetupSteps from './SetupSteps';
import axios from 'axios';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -18,8 +19,8 @@ const messages = defineMessages({
finish: 'Finish Setup',
finishing: 'Finishing…',
continue: 'Continue',
loginwithplex: 'Login with Plex',
configureplex: 'Configure Plex',
authorize: 'Authorize',
connectmediaserver: 'Connect Media Server',
configureservices: 'Configure Services',
tip: 'Tip',
syncingbackground:
@@ -30,7 +31,8 @@ const Setup: React.FC = () => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [plexSettingsComplete, setPlexSettingsComplete] = useState(false);
const [msSettingsComplete, setMSSettingsComplete] = useState(false);
const [mediaServerType, setMediaServerType] = useState('');
const router = useRouter();
const finishSetup = async () => {
@@ -45,6 +47,12 @@ const Setup: React.FC = () => {
}
};
const getMediaServerType = async () => {
const MainSettings = await axios.get('/api/v1/settings/main');
setMediaServerType(MainSettings.data.mediaServerType);
return;
};
return (
<div className="relative flex flex-col justify-center min-h-screen py-12 bg-gray-900">
<PageTitle title={intl.formatMessage(messages.setup)} />
@@ -75,13 +83,13 @@ const Setup: React.FC = () => {
>
<SetupSteps
stepNumber={1}
description={intl.formatMessage(messages.loginwithplex)}
description={intl.formatMessage(messages.authorize)}
active={currentStep === 1}
completed={currentStep > 1}
/>
<SetupSteps
stepNumber={2}
description={intl.formatMessage(messages.configureplex)}
description={intl.formatMessage(messages.connectmediaserver)}
active={currentStep === 2}
completed={currentStep > 2}
/>
@@ -95,11 +103,23 @@ const Setup: React.FC = () => {
</nav>
<div className="w-full p-4 mt-10 text-white bg-gray-800 bg-opacity-50 border border-gray-600 rounded-md">
{currentStep === 1 && (
<LoginWithPlex onComplete={() => setCurrentStep(2)} />
<SetupLogin
onComplete={() => {
getMediaServerType().then(() => {
setCurrentStep(2);
});
}}
/>
)}
{currentStep === 2 && (
<div>
<SettingsPlex onComplete={() => setPlexSettingsComplete(true)} />
{mediaServerType == 'PLEX' ? (
<SettingsPlex onComplete={() => setMSSettingsComplete(true)} />
) : (
<SettingsJellyfin
onComplete={() => setMSSettingsComplete(true)}
/>
)}
<div className="mt-4 text-sm text-gray-500">
<span className="mr-2">
<Badge>{intl.formatMessage(messages.tip)}</Badge>
@@ -111,7 +131,7 @@ const Setup: React.FC = () => {
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
disabled={!plexSettingsComplete}
disabled={!msSettingsComplete}
onClick={() => setCurrentStep(3)}
>
<FormattedMessage {...messages.continue} />

View File

@@ -69,6 +69,8 @@ const messages = defineMessages({
downloadstatus: 'Download Status',
playonplex: 'Play on Plex',
play4konplex: 'Play 4K on Plex',
playonjellyfin: 'Play on Jellyfin',
play4konjellyfin: 'Play 4K on Jellyfin',
markavailable: 'Mark as Available',
mark4kavailable: 'Mark 4K as Available',
allseasonsmarkedavailable: '* All seasons will be marked as available.',
@@ -406,11 +408,32 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
</div>
<div className="flex flex-col flex-1 mt-4 text-center text-white lg:mr-4 lg:mt-0 lg:text-left">
<div className="mb-2 space-x-2">
<span className="ml-2 lg:ml-0">
{data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && (
<span className="ml-2 lg:ml-0">
<StatusBadge
status={data.mediaInfo?.status}
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
plexUrl={
data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl
}
plexUrl4k={
data.mediaInfo?.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k
}
/>
</span>
)}
<span>
<StatusBadge
status={data.mediaInfo?.status}
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl}
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl}
plexUrl4k={
data.mediaInfo?.plexUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_TV))
? data.mediaInfo.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k
: undefined
}
/>
</span>
{settings.currentSettings.series4kEnabled &&