Merge branch 'develop'
This commit is contained in:
@@ -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 ."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'}://${
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
20
server/migration/1607928251245-DropImdbIdConstraint.ts
Normal file
20
server/migration/1607928251245-DropImdbIdConstraint.ts
Normal 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'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
Reference in New Issue
Block a user