mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(frontend): plex library scan
This commit is contained in:
@@ -816,6 +816,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
- in: query
|
||||||
|
name: start
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Status of Plex Sync
|
description: Status of Plex Sync
|
||||||
|
@@ -13,6 +13,14 @@ const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
|||||||
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
|
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
|
||||||
const plexRegex = new RegExp(/plex:\/\//);
|
const plexRegex = new RegExp(/plex:\/\//);
|
||||||
|
|
||||||
|
export interface SyncStatus {
|
||||||
|
running: boolean;
|
||||||
|
progress: number;
|
||||||
|
total: number;
|
||||||
|
currentLibrary: Library;
|
||||||
|
libraries: Library[];
|
||||||
|
}
|
||||||
|
|
||||||
class JobPlexSync {
|
class JobPlexSync {
|
||||||
private tmdb: TheMovieDb;
|
private tmdb: TheMovieDb;
|
||||||
private plexClient: PlexAPI;
|
private plexClient: PlexAPI;
|
||||||
@@ -161,7 +169,7 @@ class JobPlexSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public status() {
|
public status(): SyncStatus {
|
||||||
return {
|
return {
|
||||||
running: this.running,
|
running: this.running,
|
||||||
progress: this.progress,
|
progress: this.progress,
|
||||||
|
@@ -106,7 +106,7 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
|||||||
settingsRoutes.get('/plex/sync', (req, res) => {
|
settingsRoutes.get('/plex/sync', (req, res) => {
|
||||||
if (req.query.cancel) {
|
if (req.query.cancel) {
|
||||||
jobPlexSync.cancel();
|
jobPlexSync.cancel();
|
||||||
} else {
|
} else if (req.query.start) {
|
||||||
jobPlexSync.run();
|
jobPlexSync.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -45,7 +45,7 @@ const SettingsLayout: React.FC = ({ children }) => {
|
|||||||
regex: /^\/settings(\/main)?$/,
|
regex: /^\/settings(\/main)?$/,
|
||||||
})}
|
})}
|
||||||
{settingsLink({
|
{settingsLink({
|
||||||
text: 'Plex Settings',
|
text: 'Plex',
|
||||||
route: '/settings/plex',
|
route: '/settings/plex',
|
||||||
regex: /^\/settings\/plex/,
|
regex: /^\/settings\/plex/,
|
||||||
})}
|
})}
|
||||||
|
@@ -1,16 +1,51 @@
|
|||||||
import React, { useState } from 'react';
|
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 type { SyncStatus } from '../../../server/job/plexsync';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } 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 { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
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)',
|
||||||
|
servernamePlaceholder: 'Plex Server Name',
|
||||||
|
hostname: 'Hostname/IP',
|
||||||
|
port: 'Port',
|
||||||
|
save: 'Save Changes',
|
||||||
|
saving: 'Saving...',
|
||||||
|
plexlibraries: 'Plex Libraries',
|
||||||
|
plexlibrariesDescription:
|
||||||
|
'These are the libraries Overseerr will scan for titles. If you see no libraries listed, you will need to run at least one sync by clicking the button below. You must first configure and save your plex connection settings before you will be able to retrieve your libraries.',
|
||||||
|
syncing: 'Syncing',
|
||||||
|
sync: 'Sync Plex Libraries',
|
||||||
|
manualscan: 'Manual Library Scan',
|
||||||
|
manualscanDescription:
|
||||||
|
"Normally, this will only be run once every 6 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, 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',
|
||||||
|
});
|
||||||
|
|
||||||
const SettingsPlex: React.FC = () => {
|
const SettingsPlex: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const { data, error, revalidate } = useSWR<PlexSettings>(
|
const { data, error, revalidate } = useSWR<PlexSettings>(
|
||||||
'/api/v1/settings/plex'
|
'/api/v1/settings/plex'
|
||||||
);
|
);
|
||||||
|
const { data: dataSync, revalidate: revalidateSync } = useSWR<SyncStatus>(
|
||||||
|
'/api/v1/settings/plex/sync',
|
||||||
|
{
|
||||||
|
refreshInterval: 1000,
|
||||||
|
}
|
||||||
|
);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
@@ -21,6 +56,7 @@ const SettingsPlex: React.FC = () => {
|
|||||||
},
|
},
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
|
setSubmitError(null);
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/settings/plex', {
|
await axios.post('/api/v1/settings/plex', {
|
||||||
@@ -30,7 +66,7 @@ const SettingsPlex: React.FC = () => {
|
|||||||
|
|
||||||
revalidate();
|
revalidate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSubmitError(e.message);
|
setSubmitError(e.response.data.message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
@@ -55,6 +91,24 @@ const SettingsPlex: React.FC = () => {
|
|||||||
revalidate();
|
revalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startScan = async () => {
|
||||||
|
await axios.get('/api/v1/settings/plex/sync', {
|
||||||
|
params: {
|
||||||
|
start: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
revalidateSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelScan = async () => {
|
||||||
|
await axios.get('/api/v1/settings/plex/sync', {
|
||||||
|
params: {
|
||||||
|
cancel: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
revalidateSync();
|
||||||
|
};
|
||||||
|
|
||||||
const toggleLibrary = async (libraryId: string) => {
|
const toggleLibrary = async (libraryId: string) => {
|
||||||
setIsSyncing(true);
|
setIsSyncing(true);
|
||||||
if (activeLibraries.includes(libraryId)) {
|
if (activeLibraries.includes(libraryId)) {
|
||||||
@@ -84,29 +138,34 @@ const SettingsPlex: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
|
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
|
||||||
Plex Settings
|
<FormattedMessage {...messages.plexsettings} />
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
|
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
|
||||||
Configure the settings for your Plex server. Overseerr uses your Plex
|
<FormattedMessage {...messages.plexsettingsDescription} />
|
||||||
server to scan your library at an interval and see what content is
|
|
||||||
available.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<div className="mt-6 sm:mt-5">
|
<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">
|
<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
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
Server Name (Automatically set)
|
<FormattedMessage {...messages.servername} />
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="Plex Server Name (will be set automatically)"
|
placeholder={intl.formatMessage(
|
||||||
|
messages.servernamePlaceholder
|
||||||
|
)}
|
||||||
value={data?.name}
|
value={data?.name}
|
||||||
readOnly
|
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-cool-gray-700 border border-cool-gray-500"
|
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-cool-gray-700 border border-cool-gray-500"
|
||||||
@@ -119,7 +178,7 @@ const SettingsPlex: React.FC = () => {
|
|||||||
htmlFor="hostname"
|
htmlFor="hostname"
|
||||||
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
Hostname/IP
|
<FormattedMessage {...messages.hostname} />
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
<div className="max-w-lg flex rounded-md shadow-sm">
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
@@ -139,7 +198,7 @@ const SettingsPlex: React.FC = () => {
|
|||||||
htmlFor="port"
|
htmlFor="port"
|
||||||
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
Port
|
<FormattedMessage {...messages.port} />
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
|
<div className="max-w-lg rounded-md shadow-sm sm:max-w-xs">
|
||||||
@@ -159,21 +218,20 @@ const SettingsPlex: React.FC = () => {
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
<Button buttonType="primary" type="submit" disabled={isUpdating}>
|
<Button buttonType="primary" type="submit" disabled={isUpdating}>
|
||||||
{isUpdating ? 'Saving...' : 'Save Changes'}
|
{isUpdating
|
||||||
|
? intl.formatMessage(messages.saving)
|
||||||
|
: intl.formatMessage(messages.save)}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
|
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
|
||||||
Plex Libraries
|
<FormattedMessage {...messages.plexlibraries} />
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
|
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
|
||||||
These are the libraries Overseerr will scan for titles. If you see
|
<FormattedMessage {...messages.plexlibrariesDescription} />
|
||||||
no libraries listed, you will need to run at least one sync by
|
|
||||||
clicking the button below. You must first configure and save your
|
|
||||||
plex connection settings before you will be able to retrieve your
|
|
||||||
libraries.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Button onClick={() => syncLibraries()} disabled={isSyncing}>
|
<Button onClick={() => syncLibraries()} disabled={isSyncing}>
|
||||||
@@ -189,7 +247,9 @@ const SettingsPlex: React.FC = () => {
|
|||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{isSyncing ? 'Syncing...' : 'Sync Plex Libraries'}
|
{isSyncing
|
||||||
|
? intl.formatMessage(messages.syncing)
|
||||||
|
: intl.formatMessage(messages.sync)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ul className="mt-6 grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
<ul className="mt-6 grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
@@ -203,7 +263,107 @@ const SettingsPlex: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div className="mt-10">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
|
||||||
|
<FormattedMessage {...messages.manualscan} />
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
|
||||||
|
<FormattedMessage {...messages.manualscanDescription} />
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="bg-cool-gray-800 p-4 rounded-md">
|
||||||
|
<div className="w-full h-8 rounded-full bg-cool-gray-600 mb-6 relative">
|
||||||
|
{dataSync?.running && (
|
||||||
|
<div
|
||||||
|
className="h-8 rounded-full bg-indigo-600 transition-all ease-in-out duration-200"
|
||||||
|
style={{
|
||||||
|
width: `${Math.round(
|
||||||
|
(dataSync.progress / dataSync.total) * 100
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-0 text-sm w-full h-8 flex items-center justify-center">
|
||||||
|
<span>
|
||||||
|
{dataSync?.running
|
||||||
|
? `${dataSync.progress} of ${dataSync.total}`
|
||||||
|
: 'Not running'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col sm:flex-row">
|
||||||
|
{dataSync?.running && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center mr-0 mb-2 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.libraries.slice(
|
||||||
|
dataSync.libraries.findIndex(
|
||||||
|
(library) =>
|
||||||
|
library.id === dataSync.currentLibrary.id
|
||||||
|
) + 1
|
||||||
|
).length,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -26,21 +26,44 @@
|
|||||||
"components.MovieDetails.status": "Status",
|
"components.MovieDetails.status": "Status",
|
||||||
"components.MovieDetails.unavailable": "Unavailable",
|
"components.MovieDetails.unavailable": "Unavailable",
|
||||||
"components.MovieDetails.userrating": "User Rating",
|
"components.MovieDetails.userrating": "User Rating",
|
||||||
|
"components.MovieDetails.viewrequest": "View Request",
|
||||||
"components.PendingRequest.approve": "Approve",
|
"components.PendingRequest.approve": "Approve",
|
||||||
"components.PendingRequest.decline": "Decline",
|
"components.PendingRequest.decline": "Decline",
|
||||||
"components.PendingRequest.pendingdescription": "This title was requested by {username} ({email}) on {date}",
|
"components.PendingRequest.pendingdescription": "This title was requested by {username} ({email}) on {date}",
|
||||||
"components.PendingRequest.pendingtitle": "Pending Request",
|
"components.PendingRequest.pendingtitle": "Pending Request",
|
||||||
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
|
"components.RequestModal.cancelrequest": "This will remove your request. Are you sure you want to continue?",
|
||||||
"components.RequestModal.requestadmin": "Your request will be immediately approved.",
|
"components.RequestModal.requestadmin": "Your request will be immediately approved.",
|
||||||
|
"components.Settings.cancelscan": "Cancel Scan",
|
||||||
|
"components.Settings.currentlibrary": "Current Library: {name}",
|
||||||
|
"components.Settings.hostname": "Hostname/IP",
|
||||||
|
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
||||||
|
"components.Settings.manualscan": "Manual Library Scan",
|
||||||
|
"components.Settings.manualscanDescription": "Normally, this will only be run once every 6 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one time full manual library scan is recommended!",
|
||||||
|
"components.Settings.notrunning": "Not Running",
|
||||||
|
"components.Settings.plexlibraries": "Plex Libraries",
|
||||||
|
"components.Settings.plexlibrariesDescription": "These are the libraries Overseerr will scan for titles. If you see no libraries listed, you will need to run at least one sync by clicking the button below. You must first configure and save your plex connection settings before you will be able to retrieve your libraries.",
|
||||||
|
"components.Settings.plexsettings": "Plex Settings",
|
||||||
|
"components.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.",
|
||||||
|
"components.Settings.port": "Port",
|
||||||
|
"components.Settings.save": "Save Changes",
|
||||||
|
"components.Settings.saving": "Saving...",
|
||||||
|
"components.Settings.servername": "Server Name (Automatically Set)",
|
||||||
|
"components.Settings.servernamePlaceholder": "Plex Server Name",
|
||||||
|
"components.Settings.startscan": "Start Scan",
|
||||||
|
"components.Settings.sync": "Sync Plex Libraries",
|
||||||
|
"components.Settings.syncing": "Syncing",
|
||||||
|
"components.TvDetails.approverequests": "Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||||
"components.TvDetails.available": "Available",
|
"components.TvDetails.available": "Available",
|
||||||
"components.TvDetails.cancelrequest": "Cancel Request",
|
"components.TvDetails.cancelrequest": "Cancel Request",
|
||||||
"components.TvDetails.cast": "Cast",
|
"components.TvDetails.cast": "Cast",
|
||||||
|
"components.TvDetails.declinerequests": "Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}",
|
||||||
"components.TvDetails.originallanguage": "Original Language",
|
"components.TvDetails.originallanguage": "Original Language",
|
||||||
"components.TvDetails.overview": "Overview",
|
"components.TvDetails.overview": "Overview",
|
||||||
"components.TvDetails.overviewunavailable": "Overview unavailable",
|
"components.TvDetails.overviewunavailable": "Overview unavailable",
|
||||||
"components.TvDetails.pending": "Pending",
|
"components.TvDetails.pending": "Pending",
|
||||||
"components.TvDetails.recommendations": "Recommendations",
|
"components.TvDetails.recommendations": "Recommendations",
|
||||||
"components.TvDetails.request": "Request",
|
"components.TvDetails.request": "Request",
|
||||||
|
"components.TvDetails.requestmore": "Request More",
|
||||||
"components.TvDetails.similar": "Similar Series",
|
"components.TvDetails.similar": "Similar Series",
|
||||||
"components.TvDetails.status": "Status",
|
"components.TvDetails.status": "Status",
|
||||||
"components.TvDetails.unavailable": "Unavailable",
|
"components.TvDetails.unavailable": "Unavailable",
|
||||||
|
@@ -26,21 +26,44 @@
|
|||||||
"components.MovieDetails.status": "状態",
|
"components.MovieDetails.status": "状態",
|
||||||
"components.MovieDetails.unavailable": "",
|
"components.MovieDetails.unavailable": "",
|
||||||
"components.MovieDetails.userrating": "ユーザー評価",
|
"components.MovieDetails.userrating": "ユーザー評価",
|
||||||
|
"components.MovieDetails.viewrequest": "",
|
||||||
"components.PendingRequest.approve": "",
|
"components.PendingRequest.approve": "",
|
||||||
"components.PendingRequest.decline": "",
|
"components.PendingRequest.decline": "",
|
||||||
"components.PendingRequest.pendingdescription": "",
|
"components.PendingRequest.pendingdescription": "",
|
||||||
"components.PendingRequest.pendingtitle": "",
|
"components.PendingRequest.pendingtitle": "",
|
||||||
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
|
"components.RequestModal.cancelrequest": "このリクエストをキャンセルしてよろしいですか?",
|
||||||
"components.RequestModal.requestadmin": "このリクエストが今すぐ承認致します。よろしいですか?",
|
"components.RequestModal.requestadmin": "このリクエストが今すぐ承認致します。よろしいですか?",
|
||||||
|
"components.Settings.cancelscan": "",
|
||||||
|
"components.Settings.currentlibrary": "",
|
||||||
|
"components.Settings.hostname": "",
|
||||||
|
"components.Settings.librariesRemaining": "",
|
||||||
|
"components.Settings.manualscan": "",
|
||||||
|
"components.Settings.manualscanDescription": "",
|
||||||
|
"components.Settings.notrunning": "",
|
||||||
|
"components.Settings.plexlibraries": "",
|
||||||
|
"components.Settings.plexlibrariesDescription": "",
|
||||||
|
"components.Settings.plexsettings": "",
|
||||||
|
"components.Settings.plexsettingsDescription": "",
|
||||||
|
"components.Settings.port": "",
|
||||||
|
"components.Settings.save": "",
|
||||||
|
"components.Settings.saving": "",
|
||||||
|
"components.Settings.servername": "",
|
||||||
|
"components.Settings.servernamePlaceholder": "",
|
||||||
|
"components.Settings.startscan": "",
|
||||||
|
"components.Settings.sync": "",
|
||||||
|
"components.Settings.syncing": "",
|
||||||
|
"components.TvDetails.approverequests": "",
|
||||||
"components.TvDetails.available": "",
|
"components.TvDetails.available": "",
|
||||||
"components.TvDetails.cancelrequest": "",
|
"components.TvDetails.cancelrequest": "",
|
||||||
"components.TvDetails.cast": "",
|
"components.TvDetails.cast": "",
|
||||||
|
"components.TvDetails.declinerequests": "",
|
||||||
"components.TvDetails.originallanguage": "",
|
"components.TvDetails.originallanguage": "",
|
||||||
"components.TvDetails.overview": "",
|
"components.TvDetails.overview": "",
|
||||||
"components.TvDetails.overviewunavailable": "",
|
"components.TvDetails.overviewunavailable": "",
|
||||||
"components.TvDetails.pending": "",
|
"components.TvDetails.pending": "",
|
||||||
"components.TvDetails.recommendations": "",
|
"components.TvDetails.recommendations": "",
|
||||||
"components.TvDetails.request": "",
|
"components.TvDetails.request": "",
|
||||||
|
"components.TvDetails.requestmore": "",
|
||||||
"components.TvDetails.similar": "",
|
"components.TvDetails.similar": "",
|
||||||
"components.TvDetails.status": "",
|
"components.TvDetails.status": "",
|
||||||
"components.TvDetails.unavailable": "",
|
"components.TvDetails.unavailable": "",
|
||||||
|
Reference in New Issue
Block a user