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

This commit is contained in:
Jakob Ankarhem
2021-02-07 16:33:18 +01:00
committed by GitHub
parent 8956cb3915
commit 53f6f59798
13 changed files with 358 additions and 32 deletions

View File

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

View File

@@ -112,6 +112,7 @@ interface AddSeriesOptions {
tvdbid: number; tvdbid: number;
title: string; title: string;
profileId: number; profileId: number;
languageProfileId?: number;
seasons: number[]; seasons: number[];
seasonFolder: boolean; seasonFolder: boolean;
rootFolderPath: string; rootFolderPath: string;
@@ -120,6 +121,11 @@ interface AddSeriesOptions {
searchNow?: boolean; searchNow?: boolean;
} }
export interface LanguageProfile {
id: number;
name: string;
}
class SonarrAPI extends ExternalAPI { class SonarrAPI extends ExternalAPI {
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string { static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${ return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
@@ -236,6 +242,7 @@ class SonarrAPI extends ExternalAPI {
tvdbId: options.tvdbid, tvdbId: options.tvdbid,
title: options.title, title: options.title,
profileId: options.profileId, profileId: options.profileId,
languageProfileId: options.languageProfileId,
seasons: this.buildSeasonList( seasons: this.buildSeasonList(
options.seasons, options.seasons,
series.seasons.map((season) => ({ 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( private buildSeasonList(
seasons: number[], seasons: number[],
existingSeasons?: SonarrSeason[] existingSeasons?: SonarrSeason[]

View File

@@ -78,6 +78,9 @@ export class MediaRequest {
@Column({ nullable: true }) @Column({ nullable: true })
public rootFolder: string; public rootFolder: string;
@Column({ nullable: true })
public languageProfileId: number;
constructor(init?: Partial<MediaRequest>) { constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init); Object.assign(this, init);
} }
@@ -559,6 +562,11 @@ export class MediaRequest {
? sonarrSettings.activeAnimeProfileId ? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId; : sonarrSettings.activeProfileId;
let languageProfile =
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
? sonarrSettings.activeAnimeLanguageProfileId
: sonarrSettings.activeLanguageProfileId;
if ( if (
this.rootFolder && this.rootFolder &&
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 // Run this asynchronously so we don't wait for it on the UI side
sonarr sonarr
.addSeries({ .addSeries({
profileId: qualityProfile, profileId: qualityProfile,
languageProfileId: languageProfile,
rootFolderPath: rootFolder, rootFolderPath: rootFolder,
title: series.name, title: series.name,
tvdbid: tvdbId, tvdbid: tvdbId,

View File

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

View File

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

View File

@@ -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"`);
}
}

View File

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

View File

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

View File

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

View File

@@ -21,12 +21,15 @@ const messages = defineMessages({
loadingprofiles: 'Loading profiles…', loadingprofiles: 'Loading profiles…',
loadingfolders: 'Loading folders…', loadingfolders: 'Loading folders…',
requestas: 'Request As', requestas: 'Request As',
languageprofile: 'Language Profile',
loadinglanguages: 'Loading languages…',
}); });
export type RequestOverrides = { export type RequestOverrides = {
server?: number; server?: number;
profile?: number; profile?: number;
folder?: string; folder?: string;
language?: number;
user?: User; user?: User;
}; };
@@ -69,6 +72,11 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
const [selectedFolder, setSelectedFolder] = useState<string>( const [selectedFolder, setSelectedFolder] = useState<string>(
defaultOverrides?.folder ?? '' defaultOverrides?.folder ?? ''
); );
const [selectedLanguage, setSelectedLanguage] = useState<number>(
defaultOverrides?.language ?? -1
);
const { const {
data: serverData, data: serverData,
isValidating, isValidating,
@@ -135,6 +143,13 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
? serverData.server.activeAnimeDirectory ? serverData.server.activeAnimeDirectory
: serverData.server.activeDirectory) : serverData.server.activeDirectory)
); );
const defaultLanguage = serverData.languageProfiles?.find(
(language) =>
language.id ===
(isAnime
? serverData.server.activeAnimeLanguageProfileId
: serverData.server.activeLanguageProfileId)
);
if ( if (
defaultProfile && defaultProfile &&
@@ -149,7 +164,15 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
defaultFolder.path !== selectedFolder && defaultFolder.path !== selectedFolder &&
(!defaultOverrides || defaultOverrides.folder === null) (!defaultOverrides || defaultOverrides.folder === null)
) { ) {
setSelectedFolder(defaultFolder?.path ?? ''); setSelectedFolder(defaultFolder.path ?? '');
}
if (
defaultLanguage &&
defaultLanguage.id !== selectedLanguage &&
(!defaultOverrides || defaultOverrides.language === null)
) {
setSelectedLanguage(defaultLanguage.id);
} }
} }
}, [serverData]); }, [serverData]);
@@ -178,10 +201,19 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
) { ) {
setSelectedFolder(defaultOverrides.folder); setSelectedFolder(defaultOverrides.folder);
} }
if (
defaultOverrides &&
defaultOverrides.language !== null &&
defaultOverrides.language !== undefined
) {
setSelectedLanguage(defaultOverrides.language);
}
}, [ }, [
defaultOverrides?.server, defaultOverrides?.server,
defaultOverrides?.folder, defaultOverrides?.folder,
defaultOverrides?.profile, defaultOverrides?.profile,
defaultOverrides?.language,
]); ]);
useEffect(() => { useEffect(() => {
@@ -191,9 +223,16 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
profile: selectedProfile !== -1 ? selectedProfile : undefined, profile: selectedProfile !== -1 ? selectedProfile : undefined,
server: selectedServer ?? undefined, server: selectedServer ?? undefined,
user: selectedUser ?? undefined, user: selectedUser ?? undefined,
language: selectedLanguage ?? undefined,
}); });
} }
}, [selectedFolder, selectedServer, selectedProfile, selectedUser]); }, [
selectedFolder,
selectedServer,
selectedProfile,
selectedUser,
selectedLanguage,
]);
if (!data && !error) { if (!data && !error) {
return ( return (
@@ -225,7 +264,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
{!!data && selectedServer !== null && ( {!!data && selectedServer !== null && (
<> <>
<div className="flex flex-col items-center justify-between md:flex-row"> <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"> <label htmlFor="server" className="text-label">
{intl.formatMessage(messages.destinationserver)} {intl.formatMessage(messages.destinationserver)}
</label> </label>
@@ -247,8 +286,8 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
))} ))}
</select> </select>
</div> </div>
<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"> <label htmlFor="profile" className="text-label">
{intl.formatMessage(messages.qualityprofile)} {intl.formatMessage(messages.qualityprofile)}
</label> </label>
<select <select
@@ -283,8 +322,12 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
))} ))}
</select> </select>
</div> </div>
<div className="flex-grow flex-shrink-0 w-full mb-2 md:w-1/3 md:mb-0"> <div
<label htmlFor="server" className="text-label"> 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)} {intl.formatMessage(messages.rootfolder)}
</label> </label>
<select <select
@@ -319,6 +362,50 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
))} ))}
</select> </select>
</div> </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> </div>
</> </>
)} )}

View File

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

View File

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

View File

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