Merge branch 'develop'

This commit is contained in:
sct
2020-12-14 13:06:22 +00:00
10 changed files with 155 additions and 52 deletions

View File

@@ -11,6 +11,7 @@
"start": "NODE_ENV=production node dist/index.js", "start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault false './src/**/!(*.test).{ts,tsx}'", "i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault false './src/**/!(*.test).{ts,tsx}'",
"migration:generate": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:generate", "migration:generate": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:generate",
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:create",
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:run", "migration:run": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:run",
"format": "prettier --write ." "format": "prettier --write ."
}, },

View File

@@ -1,4 +1,5 @@
import Axios, { AxiosInstance } from 'axios'; import Axios, { AxiosInstance } from 'axios';
import logger from '../logger';
interface RadarrMovieOptions { interface RadarrMovieOptions {
title: string; title: string;
@@ -96,6 +97,11 @@ class RadarrAPI {
return response.data; return response.data;
} catch (e) { } catch (e) {
logger.error('Something went wrong adding a movie to Radarr', {
label: 'Radarr',
message: e.message,
options,
});
throw new Error(`[Radarr] Failed to add movie: ${e.message}`); throw new Error(`[Radarr] Failed to add movie: ${e.message}`);
} }
}; };

View File

@@ -234,8 +234,19 @@ export class MediaRequest {
return; return;
} }
const radarrSettings = settings.radarr.find(
(radarr) => radarr.isDefault && !radarr.is4k
);
if (!radarrSettings) {
logger.info(
'There is no default radarr configured. Did you set any of your Radarr servers as default?',
{ label: 'Media Request' }
);
return;
}
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const radarrSettings = settings.radarr[0];
const radarr = new RadarrAPI({ const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey, apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${ url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
@@ -283,6 +294,18 @@ export class MediaRequest {
return; return;
} }
const sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && !sonarr.is4k
);
if (!sonarrSettings) {
logger.info(
'There is no default sonarr configured. Did you set any of your Sonarr servers as default?',
{ label: 'Media Request' }
);
return;
}
const media = await mediaRepository.findOne({ const media = await mediaRepository.findOne({
where: { id: this.media.id }, where: { id: this.media.id },
relations: ['requests'], relations: ['requests'],
@@ -293,7 +316,6 @@ export class MediaRequest {
} }
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const sonarrSettings = settings.sonarr[0];
const sonarr = new SonarrAPI({ const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey, apiKey: sonarrSettings.apiKey,
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${ url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${

View File

@@ -51,10 +51,19 @@ class JobPlexSync {
private async processMovie(plexitem: PlexLibraryItem) { private async processMovie(plexitem: PlexLibraryItem) {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
try {
if (plexitem.guid.match(plexRegex)) { if (plexitem.guid.match(plexRegex)) {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey); const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
const newMedia = new Media(); const newMedia = new Media();
if (!metadata.Guid) {
logger.debug('No Guid metadata for this title. Skipping', {
label: 'Plex Sync',
ratingKey: plexitem.ratingKey,
});
return;
}
metadata.Guid.forEach((ref) => { metadata.Guid.forEach((ref) => {
if (ref.id.match(imdbRegex)) { if (ref.id.match(imdbRegex)) {
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined; newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
@@ -110,6 +119,14 @@ class JobPlexSync {
} }
} }
} }
} catch (e) {
this.log(
`Failed to process plex item. ratingKey: ${
plexitem.parentRatingKey ?? plexitem.ratingKey
}`,
'error'
);
}
} }
private async processShow(plexitem: PlexLibraryItem) { private async processShow(plexitem: PlexLibraryItem) {

View File

@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm';
export class DropImdbIdConstraint1607928251245 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropUniqueConstraint(
'media',
'UQ_7ff2d11f6a83cb52386eaebe74b'
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createUniqueConstraint(
'media',
new TableUnique({
name: 'UQ_7ff2d11f6a83cb52386eaebe74b',
columnNames: ['imdbId'],
})
);
}
}

View File

@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Axios from 'axios'; import Axios from 'axios';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({ const messages = defineMessages({
save: 'Save Changes', save: 'Save Changes',
@@ -14,10 +15,13 @@ const messages = defineMessages({
webhookUrl: 'Webhook URL', webhookUrl: 'Webhook URL',
validationWebhookUrlRequired: 'You must provide a webhook URL', validationWebhookUrlRequired: 'You must provide a webhook URL',
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks', webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
discordsettingssaved: 'Discord notification settings saved!',
discordsettingsfailed: 'Discord notification settings failed to save.',
}); });
const NotificationsDiscord: React.FC = () => { const NotificationsDiscord: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR( const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/discord' '/api/v1/settings/notifications/discord'
); );
@@ -49,8 +53,15 @@ const NotificationsDiscord: React.FC = () => {
webhookUrl: values.webhookUrl, webhookUrl: values.webhookUrl,
}, },
}); });
addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) { } catch (e) {
// TODO show error addToast(intl.formatMessage(messages.discordsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally { } finally {
revalidate(); revalidate();
} }

View File

@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Axios from 'axios'; import Axios from 'axios';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({ const messages = defineMessages({
save: 'Save Changes', save: 'Save Changes',
@@ -20,10 +21,13 @@ const messages = defineMessages({
enableSsl: 'Enable SSL', enableSsl: 'Enable SSL',
authUser: 'Auth User', authUser: 'Auth User',
authPass: 'Auth Pass', authPass: 'Auth Pass',
emailsettingssaved: 'Email notification settings saved!',
emailsettingsfailed: 'Email notification settings failed to save.',
}); });
const NotificationsEmail: React.FC = () => { const NotificationsEmail: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR( const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/email' '/api/v1/settings/notifications/email'
); );
@@ -65,14 +69,21 @@ const NotificationsEmail: React.FC = () => {
options: { options: {
emailFrom: values.emailFrom, emailFrom: values.emailFrom,
smtpHost: values.smtpHost, smtpHost: values.smtpHost,
smtpPort: values.smtpPort, smtpPort: Number(values.smtpPort),
secure: values.secure, secure: values.secure,
authUser: values.authUser, authUser: values.authUser,
authPass: values.authPass, authPass: values.authPass,
}, },
}); });
addToast(intl.formatMessage(messages.emailsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) { } catch (e) {
// TODO show error addToast(intl.formatMessage(messages.emailsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally { } finally {
revalidate(); revalidate();
} }

View File

@@ -78,7 +78,7 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
try { try {
await axios.post('/api/v1/settings/plex', { await axios.post('/api/v1/settings/plex', {
ip: values.hostname, ip: values.hostname,
port: values.port, port: Number(values.port),
} as PlexSettings); } as PlexSettings);
revalidate(); revalidate();

View File

@@ -7,6 +7,10 @@ import type { Nullable } from '../utils/typeHelpers';
type Url = string | UrlObject; type Url = string | UrlObject;
const encodeURIExtraParams = (string: string): string => {
return encodeURIComponent(string).replace(/!/g, '%21');
};
interface SearchObject { interface SearchObject {
searchValue: string; searchValue: string;
searchOpen: boolean; searchOpen: boolean;
@@ -35,14 +39,17 @@ const useSearchInput = (): SearchObject => {
if (router.pathname.startsWith('/search')) { if (router.pathname.startsWith('/search')) {
router.replace({ router.replace({
pathname: router.pathname, pathname: router.pathname,
query: { ...router.query, query: debouncedValue }, query: {
...router.query,
query: encodeURIExtraParams(debouncedValue),
},
}); });
} else { } else {
setLastRoute(router.asPath); setLastRoute(router.asPath);
router router
.push({ .push({
pathname: '/search', pathname: '/search',
query: { query: debouncedValue }, query: { query: encodeURIExtraParams(debouncedValue) },
}) })
.then(() => window.scrollTo(0, 0)); .then(() => window.scrollTo(0, 0));
} }
@@ -85,8 +92,12 @@ const useSearchInput = (): SearchObject => {
* is on /search * is on /search
*/ */
useEffect(() => { useEffect(() => {
if (router.query.query !== debouncedValue) { if (router.query.query !== encodeURIExtraParams(debouncedValue)) {
setSearchValue((router.query.query as string) ?? ''); setSearchValue(
router.query.query
? decodeURIComponent(router.query.query as string)
: ''
);
if (!router.pathname.startsWith('/search') && !router.query.query) { if (!router.pathname.startsWith('/search') && !router.query.query) {
setIsOpen(false); setIsOpen(false);

View File

@@ -91,7 +91,11 @@
"components.Settings.Notifications.agentenabled": "Agent Enabled", "components.Settings.Notifications.agentenabled": "Agent Enabled",
"components.Settings.Notifications.authPass": "Auth Pass", "components.Settings.Notifications.authPass": "Auth Pass",
"components.Settings.Notifications.authUser": "Auth User", "components.Settings.Notifications.authUser": "Auth User",
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved!",
"components.Settings.Notifications.emailsender": "Email Sender Address", "components.Settings.Notifications.emailsender": "Email Sender Address",
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved!",
"components.Settings.Notifications.enableSsl": "Enable SSL", "components.Settings.Notifications.enableSsl": "Enable SSL",
"components.Settings.Notifications.save": "Save Changes", "components.Settings.Notifications.save": "Save Changes",
"components.Settings.Notifications.saving": "Saving...", "components.Settings.Notifications.saving": "Saving...",