feat(requests): add language profile support (#860)

pull/862/head
Jakob Ankarhem 4 years ago committed by GitHub
parent 8956cb3915
commit 53f6f59798
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -376,9 +376,16 @@ components:
activeDirectory:
type: string
example: '/tv/'
activeLanguageProfileId:
type: number
example: 1
nullable: true
activeAnimeProfileId:
type: number
nullable: true
activeAnimeLanguageProfileId:
type: number
nullable: true
activeAnimeProfileName:
type: string
example: 720p/1080p
@ -3062,6 +3069,8 @@ paths:
type: number
rootFolder:
type: string
languageProfileId:
type: number
required:
- mediaType
- mediaId

@ -112,6 +112,7 @@ interface AddSeriesOptions {
tvdbid: number;
title: string;
profileId: number;
languageProfileId?: number;
seasons: number[];
seasonFolder: boolean;
rootFolderPath: string;
@ -120,6 +121,11 @@ interface AddSeriesOptions {
searchNow?: boolean;
}
export interface LanguageProfile {
id: number;
name: string;
}
class SonarrAPI extends ExternalAPI {
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
@ -236,6 +242,7 @@ class SonarrAPI extends ExternalAPI {
tvdbId: options.tvdbid,
title: options.title,
profileId: options.profileId,
languageProfileId: options.languageProfileId,
seasons: this.buildSeasonList(
options.seasons,
series.seasons.map((season) => ({
@ -321,6 +328,28 @@ class SonarrAPI extends ExternalAPI {
}
}
public async getLanguageProfiles(): Promise<LanguageProfile[]> {
try {
const data = await this.getRolling<LanguageProfile[]>(
'/v3/languageprofile',
undefined,
3600
);
return data;
} catch (e) {
logger.error(
'Something went wrong while retrieving Sonarr language profiles.',
{
label: 'Sonarr API',
message: e.message,
}
);
throw new Error('Failed to get language profiles');
}
}
private buildSeasonList(
seasons: number[],
existingSeasons?: SonarrSeason[]

@ -78,6 +78,9 @@ export class MediaRequest {
@Column({ nullable: true })
public rootFolder: string;
@Column({ nullable: true })
public languageProfileId: number;
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
@ -559,6 +562,11 @@ export class MediaRequest {
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId;
let languageProfile =
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
? sonarrSettings.activeAnimeLanguageProfileId
: sonarrSettings.activeLanguageProfileId;
if (
this.rootFolder &&
this.rootFolder !== '' &&
@ -577,10 +585,24 @@ export class MediaRequest {
});
}
if (
this.languageProfileId &&
this.languageProfileId !== languageProfile
) {
languageProfile = this.languageProfileId;
logger.info(
`Request has an override Language Profile: ${languageProfile}`,
{
label: 'Media Request',
}
);
}
// Run this asynchronously so we don't wait for it on the UI side
sonarr
.addSeries({
profileId: qualityProfile,
languageProfileId: languageProfile,
rootFolderPath: rootFolder,
title: series.name,
tvdbid: tvdbId,

@ -1,4 +1,5 @@
import { RadarrProfile, RadarrRootFolder } from '../../api/radarr';
import { LanguageProfile } from '../../api/sonarr';
export interface ServiceCommonServer {
id: number;
@ -7,12 +8,15 @@ export interface ServiceCommonServer {
isDefault: boolean;
activeProfileId: number;
activeDirectory: string;
activeLanguageProfileId?: number;
activeAnimeProfileId?: number;
activeAnimeDirectory?: string;
activeAnimeLanguageProfileId?: number;
}
export interface ServiceCommonServerWithDetails {
server: ServiceCommonServer;
profiles: RadarrProfile[];
rootFolders: Partial<RadarrRootFolder>[];
languageProfiles?: LanguageProfile[];
}

@ -45,6 +45,8 @@ export interface SonarrSettings extends DVRSettings {
activeAnimeProfileId?: number;
activeAnimeProfileName?: string;
activeAnimeDirectory?: string;
activeAnimeLanguageProfileId?: number;
activeLanguageProfileId?: number;
enableSeasonFolders: boolean;
}

@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddLanguageProfileId1612571545781 implements MigrationInterface {
name = 'AddLanguageProfileId1612571545781';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
}
}

@ -250,6 +250,7 @@ requestRoutes.post(
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
languageProfileId: req.body.languageProfileId,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({

@ -90,6 +90,8 @@ serviceRoutes.get('/sonarr', async (req, res) => {
activeProfileId: sonarr.activeProfileId,
activeAnimeProfileId: sonarr.activeAnimeProfileId,
activeAnimeDirectory: sonarr.activeAnimeDirectory,
activeLanguageProfileId: sonarr.activeLanguageProfileId,
activeAnimeLanguageProfileId: sonarr.activeAnimeLanguageProfileId,
})
);
@ -119,8 +121,10 @@ serviceRoutes.get<{ sonarrId: string }>(
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
});
try {
const profiles = await sonarr.getProfiles();
const rootFolders = await sonarr.getRootFolders();
const languageProfiles = await sonarr.getLanguageProfiles();
return res.status(200).json({
server: {
@ -132,6 +136,9 @@ serviceRoutes.get<{ sonarrId: string }>(
activeProfileId: sonarrSettings.activeProfileId,
activeAnimeProfileId: sonarrSettings.activeAnimeProfileId,
activeAnimeDirectory: sonarrSettings.activeAnimeDirectory,
activeLanguageProfileId: sonarrSettings.activeLanguageProfileId,
activeAnimeLanguageProfileId:
sonarrSettings.activeAnimeLanguageProfileId,
},
profiles: profiles.map((profile) => ({
id: profile.id,
@ -143,7 +150,11 @@ serviceRoutes.get<{ sonarrId: string }>(
path: folder.path,
totalSpace: folder.totalSpace,
})),
languageProfiles: languageProfiles,
} as ServiceCommonServerWithDetails);
} catch (e) {
next({ status: 500, message: e.message });
}
}
);

@ -46,6 +46,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
const profiles = await sonarr.getProfiles();
const folders = await sonarr.getRootFolders();
const languageProfiles = await sonarr.getLanguageProfiles();
return res.status(200).json({
profiles,
@ -53,6 +54,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
id: folder.id,
path: folder.path,
})),
languageProfiles,
});
} catch (e) {
logger.error('Failed to test Sonarr', {

@ -21,12 +21,15 @@ const messages = defineMessages({
loadingprofiles: 'Loading profiles…',
loadingfolders: 'Loading folders…',
requestas: 'Request As',
languageprofile: 'Language Profile',
loadinglanguages: 'Loading languages…',
});
export type RequestOverrides = {
server?: number;
profile?: number;
folder?: string;
language?: number;
user?: User;
};
@ -69,6 +72,11 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
const [selectedFolder, setSelectedFolder] = useState<string>(
defaultOverrides?.folder ?? ''
);
const [selectedLanguage, setSelectedLanguage] = useState<number>(
defaultOverrides?.language ?? -1
);
const {
data: serverData,
isValidating,
@ -135,6 +143,13 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
? serverData.server.activeAnimeDirectory
: serverData.server.activeDirectory)
);
const defaultLanguage = serverData.languageProfiles?.find(
(language) =>
language.id ===
(isAnime
? serverData.server.activeAnimeLanguageProfileId
: serverData.server.activeLanguageProfileId)
);
if (
defaultProfile &&
@ -149,7 +164,15 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
defaultFolder.path !== selectedFolder &&
(!defaultOverrides || defaultOverrides.folder === null)
) {
setSelectedFolder(defaultFolder?.path ?? '');
setSelectedFolder(defaultFolder.path ?? '');
}
if (
defaultLanguage &&
defaultLanguage.id !== selectedLanguage &&
(!defaultOverrides || defaultOverrides.language === null)
) {
setSelectedLanguage(defaultLanguage.id);
}
}
}, [serverData]);
@ -178,10 +201,19 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
) {
setSelectedFolder(defaultOverrides.folder);
}
if (
defaultOverrides &&
defaultOverrides.language !== null &&
defaultOverrides.language !== undefined
) {
setSelectedLanguage(defaultOverrides.language);
}
}, [
defaultOverrides?.server,
defaultOverrides?.folder,
defaultOverrides?.profile,
defaultOverrides?.language,
]);
useEffect(() => {
@ -191,9 +223,16 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
profile: selectedProfile !== -1 ? selectedProfile : undefined,
server: selectedServer ?? undefined,
user: selectedUser ?? undefined,
language: selectedLanguage ?? undefined,
});
}
}, [selectedFolder, selectedServer, selectedProfile, selectedUser]);
}, [
selectedFolder,
selectedServer,
selectedProfile,
selectedUser,
selectedLanguage,
]);
if (!data && !error) {
return (
@ -225,7 +264,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
{!!data && selectedServer !== null && (
<>
<div className="flex flex-col items-center justify-between md:flex-row">
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/3 md:pr-4 md:mb-0">
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/4 md:pr-4 md:mb-0">
<label htmlFor="server" className="text-label">
{intl.formatMessage(messages.destinationserver)}
</label>
@ -247,8 +286,8 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
))}
</select>
</div>
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/3 md:pr-4 md:mb-0">
<label htmlFor="server" className="text-label">
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/4 md:pr-4 md:mb-0">
<label htmlFor="profile" className="text-label">
{intl.formatMessage(messages.qualityprofile)}
</label>
<select
@ -283,8 +322,12 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
))}
</select>
</div>
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/3 md:mb-0">
<label htmlFor="server" className="text-label">
<div
className={`flex-grow flex-shrink-0 w-full mb-2 md:w-1/4 md:mb-0 ${
type === 'tv' ? 'md:pr-4' : ''
}`}
>
<label htmlFor="folder" className="text-label">
{intl.formatMessage(messages.rootfolder)}
</label>
<select
@ -319,6 +362,50 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
))}
</select>
</div>
{type === 'tv' && (
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/4 md:mb-0">
<label htmlFor="language" className="text-label">
{intl.formatMessage(messages.languageprofile)}
</label>
<select
id="language"
name="language"
value={selectedLanguage}
onChange={(e) =>
setSelectedLanguage(parseInt(e.target.value))
}
onBlur={(e) =>
setSelectedLanguage(parseInt(e.target.value))
}
className="block w-full py-2 pl-3 pr-10 mt-1 text-base leading-6 text-white transition duration-150 ease-in-out bg-gray-800 border-gray-700 rounded-md form-select focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5"
>
{isValidating && (
<option value="">
{intl.formatMessage(messages.loadinglanguages)}
</option>
)}
{!isValidating &&
serverData &&
serverData.languageProfiles?.map((language) => (
<option
key={`folder-list${language.id}`}
value={language.id}
>
{language.name}
{isAnime &&
serverData.server.activeAnimeLanguageProfileId ===
language.id
? ` ${intl.formatMessage(messages.default)}`
: !isAnime &&
serverData.server.activeLanguageProfileId ===
language.id
? ` ${intl.formatMessage(messages.default)}`
: ''}
</option>
))}
</select>
</div>
)}
</div>
</>
)}

@ -103,6 +103,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
serverId: requestOverrides?.server,
profileId: requestOverrides?.profile,
rootFolder: requestOverrides?.folder,
languageProfileId: requestOverrides?.language,
userId: requestOverrides?.user?.id,
seasons: selectedSeasons,
});
@ -151,6 +152,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
serverId: requestOverrides.server,
profileId: requestOverrides.profile,
rootFolder: requestOverrides.folder,
languageProfileId: requestOverrides.language,
userId: requestOverrides?.user?.id,
};
}
@ -569,6 +571,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
folder: editRequest.rootFolder,
profile: editRequest.profileId,
server: editRequest.serverId,
language: editRequest.languageProfileId,
}
: undefined
}

@ -16,7 +16,8 @@ const messages = defineMessages({
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
validationRootFolderRequired: 'You must select a root folder',
validationProfileRequired: 'You must select a profile',
validationProfileRequired: 'You must select a quality profile',
validationLanguageProfileRequired: 'You must select a language profile',
toastSonarrTestSuccess: 'Sonarr connection established!',
toastSonarrTestFailure: 'Failed to connect to Sonarr.',
saving: 'Saving…',
@ -35,17 +36,22 @@ const messages = defineMessages({
baseUrl: 'Base URL',
baseUrlPlaceholder: 'Example: /sonarr',
qualityprofile: 'Quality Profile',
languageprofile: 'Language Profile',
rootfolder: 'Root Folder',
animequalityprofile: 'Anime Quality Profile',
animelanguageprofile: 'Anime Language Profile',
animerootfolder: 'Anime Root Folder',
seasonfolders: 'Season Folders',
server4k: '4K Server',
selectQualityProfile: 'Select quality profile',
selectRootFolder: 'Select root folder',
selectLanguageProfile: 'Select language profile',
loadingprofiles: 'Loading quality profiles…',
testFirstQualityProfiles: 'Test connection to load quality profiles',
loadingrootfolders: 'Loading root folders…',
testFirstRootFolders: 'Test connection to load root folders',
loadinglanguageprofiles: 'Loading language profiles…',
testFirstLanguageProfiles: 'Test connection to load language profiles',
syncEnabled: 'Enable Sync',
externalUrl: 'External URL',
externalUrlPlaceholder: 'External URL pointing to your Sonarr server',
@ -65,6 +71,10 @@ interface TestResponse {
id: number;
path: string;
}[];
languageProfiles: {
id: number;
name: string;
}[];
}
interface SonarrModalProps {
@ -86,6 +96,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
const [testResponse, setTestResponse] = useState<TestResponse>({
profiles: [],
rootFolders: [],
languageProfiles: [],
});
const SonarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
@ -106,6 +117,9 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
activeLanguageProfileId: Yup.number().required(
intl.formatMessage(messages.validationLanguageProfileRequired)
),
externalUrl: Yup.string()
.url(intl.formatMessage(messages.validationApplicationUrl))
.test(
@ -224,8 +238,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
apiKey: sonarr?.apiKey,
baseUrl: sonarr?.baseUrl,
activeProfileId: sonarr?.activeProfileId,
activeLanguageProfileId: sonarr?.activeLanguageProfileId,
rootFolder: sonarr?.activeDirectory,
activeAnimeProfileId: sonarr?.activeAnimeProfileId,
activeAnimeLanguageProfileId: sonarr?.activeAnimeLanguageProfileId,
activeAnimeRootFolder: sonarr?.activeAnimeDirectory,
isDefault: sonarr?.isDefault ?? false,
is4k: sonarr?.is4k ?? false,
@ -252,11 +268,17 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
useSsl: values.ssl,
baseUrl: values.baseUrl,
activeProfileId: Number(values.activeProfileId),
activeLanguageProfileId: values.activeLanguageProfileId
? Number(values.activeLanguageProfileId)
: undefined,
activeProfileName: profileName,
activeDirectory: values.rootFolder,
activeAnimeProfileId: values.activeAnimeProfileId
? Number(values.activeAnimeProfileId)
: undefined,
activeAnimeLanguageProfileId: values.activeAnimeLanguageProfileId
? Number(values.activeAnimeLanguageProfileId)
: undefined,
activeAnimeProfileName: animeProfileName ?? undefined,
activeAnimeDirectory: values.activeAnimeRootFolder,
is4k: values.is4k,
@ -559,6 +581,54 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="activeLanguageProfileId"
className="text-label"
>
{intl.formatMessage(messages.languageprofile)}
<span className="text-red-500">*</span>
</label>
<div className="form-input">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
as="select"
id="activeLanguageProfileId"
name="activeLanguageProfileId"
disabled={!isValidated || isTesting}
>
<option value="">
{isTesting
? intl.formatMessage(
messages.loadinglanguageprofiles
)
: !isValidated
? intl.formatMessage(
messages.testFirstLanguageProfiles
)
: intl.formatMessage(
messages.selectLanguageProfile
)}
</option>
{testResponse.languageProfiles.length > 0 &&
testResponse.languageProfiles.map((language) => (
<option
key={`loaded-profile-${language.id}`}
value={language.id}
>
{language.name}
</option>
))}
</Field>
</div>
{errors.activeLanguageProfileId &&
touched.activeLanguageProfileId && (
<div className="error">
{errors.activeLanguageProfileId}
</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="activeAnimeProfileId" className="text-label">
{intl.formatMessage(messages.animequalityprofile)}
@ -635,6 +705,53 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="activeAnimeLanguageProfileId"
className="text-label"
>
{intl.formatMessage(messages.animerootfolder)}
</label>
<div className="form-input">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
as="select"
id="activeAnimeLanguageProfileId"
name="activeAnimeLanguageProfileId"
disabled={!isValidated || isTesting}
>
<option value="">
{isTesting
? intl.formatMessage(
messages.loadinglanguageprofiles
)
: !isValidated
? intl.formatMessage(
messages.testFirstLanguageProfiles
)
: intl.formatMessage(
messages.selectLanguageProfile
)}
</option>
{testResponse.languageProfiles.length > 0 &&
testResponse.languageProfiles.map((language) => (
<option
key={`loaded-profile-${language.id}`}
value={language.id}
>
{language.name}
</option>
))}
</Field>
</div>
{errors.activeAnimeLanguageProfileId &&
touched.activeAnimeLanguageProfileId && (
<div className="error">
{errors.activeAnimeLanguageProfileId}
</div>
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="enableSeasonFolders"

@ -169,7 +169,9 @@
"components.RequestModal.AdvancedRequester.animenote": "* This series is an anime.",
"components.RequestModal.AdvancedRequester.default": "(Default)",
"components.RequestModal.AdvancedRequester.destinationserver": "Destination Server",
"components.RequestModal.AdvancedRequester.languageprofile": "Language Profile",
"components.RequestModal.AdvancedRequester.loadingfolders": "Loading folders…",
"components.RequestModal.AdvancedRequester.loadinglanguages": "Loading languages…",
"components.RequestModal.AdvancedRequester.loadingprofiles": "Loading profiles…",
"components.RequestModal.AdvancedRequester.qualityprofile": "Quality Profile",
"components.RequestModal.AdvancedRequester.requestas": "Request As",
@ -391,6 +393,7 @@
"components.Settings.SettingsJobsCache.process": "Process",
"components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SonarrModal.add": "Add Server",
"components.Settings.SonarrModal.animelanguageprofile": "Anime Language Profile",
"components.Settings.SonarrModal.animequalityprofile": "Anime Quality Profile",
"components.Settings.SonarrModal.animerootfolder": "Anime Root Folder",
"components.Settings.SonarrModal.apiKey": "API Key",
@ -403,6 +406,8 @@
"components.Settings.SonarrModal.externalUrl": "External URL",
"components.Settings.SonarrModal.externalUrlPlaceholder": "External URL pointing to your Sonarr server",
"components.Settings.SonarrModal.hostname": "Hostname",
"components.Settings.SonarrModal.languageprofile": "Language Profile",
"components.Settings.SonarrModal.loadinglanguageprofiles": "Loading language profiles…",
"components.Settings.SonarrModal.loadingprofiles": "Loading quality profiles…",
"components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…",
"components.Settings.SonarrModal.port": "Port",
@ -412,6 +417,7 @@
"components.Settings.SonarrModal.save": "Save Changes",
"components.Settings.SonarrModal.saving": "Saving…",
"components.Settings.SonarrModal.seasonfolders": "Season Folders",
"components.Settings.SonarrModal.selectLanguageProfile": "Select language profile",
"components.Settings.SonarrModal.selectQualityProfile": "Select quality profile",
"components.Settings.SonarrModal.selectRootFolder": "Select root folder",
"components.Settings.SonarrModal.server4k": "4K Server",
@ -420,6 +426,7 @@
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.syncEnabled": "Enable Sync",
"components.Settings.SonarrModal.test": "Test",
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles",
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
"components.Settings.SonarrModal.testFirstRootFolders": "Test connection to load root folders",
"components.Settings.SonarrModal.testing": "Testing…",
@ -431,6 +438,7 @@
"components.Settings.SonarrModal.validationBaseUrlLeadingSlash": "Base URL must have a leading slash",
"components.Settings.SonarrModal.validationBaseUrlTrailingSlash": "Base URL must not end in a trailing slash",
"components.Settings.SonarrModal.validationHostnameRequired": "You must provide a hostname/IP",
"components.Settings.SonarrModal.validationLanguageProfileRequired": "You must select a language profile",
"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 quality profile",

Loading…
Cancel
Save