@@ -307,52 +353,58 @@ const SettingsServices: React.FC = () => {
{!sonarrData && !sonarrError &&
}
{sonarrData && !sonarrError && (
-
- {sonarrData.map((sonarr) => (
- setEditSonarrModal({ open: true, sonarr })}
- onDelete={() =>
- setDeleteServerModal({
- open: true,
- serverId: sonarr.id,
- type: 'sonarr',
- })
- }
- />
- ))}
-
-
-
- setEditSonarrModal({ open: true, sonarr: null })
+ <>
+ {sonarrData.length > 0 &&
+ !sonarrData.some(
+ (sonarr) => sonarr.isDefault && !sonarr.is4k
+ ) && }
+
+ {sonarrData.map((sonarr) => (
+ setEditSonarrModal({ open: true, sonarr })}
+ onDelete={() =>
+ setDeleteServerModal({
+ open: true,
+ serverId: sonarr.id,
+ type: 'sonarr',
+ })
}
- >
-
+ ))}
+
+
+
+ setEditSonarrModal({ open: true, sonarr: null })
+ }
>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ >
)}
>
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index b1d7b2bf0..8072e1464 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -207,6 +207,9 @@
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.nextexecution": "Next Execution",
+ "components.Settings.no4kimplemented": "(Default 4K servers are not currently implemented)",
+ "components.Settings.nodefault": "No default server selected!",
+ "components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
"components.Settings.notificationsettings": "Notification Settings",
"components.Settings.notificationsettingsDescription": "Here you can pick and choose what types of notifications to send and through what types of services.",
"components.Settings.notrunning": "Not Running",
From fc12ab84d9482eb3a11f117f8cab6fd48a9401cd Mon Sep 17 00:00:00 2001
From: sct
Date: Thu, 17 Dec 2020 03:28:49 +0000
Subject: [PATCH 10/13] fix(frontend): clarify that radarr/sonnarr servers must
be tested before profiles/folders appear
Also blocks "Add Server" or "Save" button until all required fields are entered
fixes #326 and #328
---
src/components/Settings/RadarrModal/index.tsx | 38 +++++++++++++------
src/components/Settings/SonarrModal/index.tsx | 27 ++++++++++---
src/i18n/locale/en.json | 8 ++++
3 files changed, 56 insertions(+), 17 deletions(-)
diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx
index 0734a8b96..f89439544 100644
--- a/src/components/Settings/RadarrModal/index.tsx
+++ b/src/components/Settings/RadarrModal/index.tsx
@@ -42,6 +42,10 @@ const messages = defineMessages({
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
selectMinimumAvailability: 'Select minimum availability',
+ loadingprofiles: 'Loading quality profiles…',
+ testFirstQualityProfiles: 'Test your connection to load quality profiles',
+ loadingrootfolders: 'Loading root folders…',
+ testFirstRootFolders: 'Test your connection to load root folders',
});
interface TestResponse {
@@ -86,7 +90,9 @@ const RadarrModal: React.FC = ({
intl.formatMessage(messages.validationPortRequired)
),
apiKey: Yup.string().required(intl.formatMessage(messages.apiKey)),
- rootFolder: Yup.string().required(intl.formatMessage(messages.rootfolder)),
+ rootFolder: Yup.string().required(
+ intl.formatMessage(messages.validationRootFolderRequired)
+ ),
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
@@ -179,7 +185,7 @@ const RadarrModal: React.FC = ({
baseUrl: radarr?.baseUrl,
activeProfileId: radarr?.activeProfileId,
rootFolder: radarr?.activeDirectory,
- minimumAvailability: radarr?.minimumAvailability,
+ minimumAvailability: radarr?.minimumAvailability ?? 'released',
isDefault: radarr?.isDefault ?? false,
is4k: radarr?.is4k ?? false,
}}
@@ -226,6 +232,7 @@ const RadarrModal: React.FC = ({
handleSubmit,
setFieldValue,
isSubmitting,
+ isValid,
}) => {
return (
= ({
secondaryDisabled={
!values.apiKey || !values.hostname || !values.port || isTesting
}
- okDisabled={!isValidated || isSubmitting || isTesting}
+ okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
onOk={() => handleSubmit()}
title={
!radarr
@@ -453,10 +460,17 @@ const RadarrModal: React.FC = ({
as="select"
id="activeProfileId"
name="activeProfileId"
- className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
+ disabled={!isValidated || isTesting}
+ className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
- {intl.formatMessage(messages.selectQualityProfile)}
+ {isTesting
+ ? intl.formatMessage(messages.loadingprofiles)
+ : !isValidated
+ ? intl.formatMessage(
+ messages.testFirstQualityProfiles
+ )
+ : intl.formatMessage(messages.selectQualityProfile)}
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
@@ -489,10 +503,15 @@ const RadarrModal: React.FC = ({
as="select"
id="rootFolder"
name="rootFolder"
- className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
+ disabled={!isValidated || isTesting}
+ className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
- {intl.formatMessage(messages.selectRootFolder)}
+ {isTesting
+ ? intl.formatMessage(messages.loadingrootfolders)
+ : !isValidated
+ ? intl.formatMessage(messages.testFirstRootFolders)
+ : intl.formatMessage(messages.selectRootFolder)}
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
@@ -527,11 +546,6 @@ const RadarrModal: React.FC = ({
name="minimumAvailability"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
-
- {intl.formatMessage(
- messages.selectMinimumAvailability
- )}
-
Announced
In Cinemas
Released
diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx
index 9303131d7..ad6fee594 100644
--- a/src/components/Settings/SonarrModal/index.tsx
+++ b/src/components/Settings/SonarrModal/index.tsx
@@ -40,6 +40,10 @@ const messages = defineMessages({
server4k: '4K Server',
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
+ loadingprofiles: 'Loading quality profiles…',
+ testFirstQualityProfiles: 'Test your connection to load quality profiles',
+ loadingrootfolders: 'Loading root folders…',
+ testFirstRootFolders: 'Test your connection to load root folders',
});
interface TestResponse {
@@ -225,6 +229,7 @@ const SonarrModal: React.FC = ({
handleSubmit,
setFieldValue,
isSubmitting,
+ isValid,
}) => {
return (
= ({
secondaryDisabled={
!values.apiKey || !values.hostname || !values.port || isTesting
}
- okDisabled={!isValidated || isSubmitting || isTesting}
+ okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
onOk={() => handleSubmit()}
title={
!sonarr
@@ -452,10 +457,17 @@ const SonarrModal: React.FC = ({
as="select"
id="activeProfileId"
name="activeProfileId"
- className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
+ disabled={!isValidated || isTesting}
+ className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
- {intl.formatMessage(messages.selectQualityProfile)}
+ {isTesting
+ ? intl.formatMessage(messages.loadingprofiles)
+ : !isValidated
+ ? intl.formatMessage(
+ messages.testFirstQualityProfiles
+ )
+ : intl.formatMessage(messages.selectQualityProfile)}
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
@@ -488,10 +500,15 @@ const SonarrModal: React.FC = ({
as="select"
id="rootFolder"
name="rootFolder"
- className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
+ disabled={!isValidated || isTesting}
+ className="mt-1 form-select block rounded-md w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
- {intl.formatMessage(messages.selectRootFolder)}
+ {isTesting
+ ? intl.formatMessage(messages.loadingrootfolders)
+ : !isValidated
+ ? intl.formatMessage(messages.testFirstRootFolders)
+ : intl.formatMessage(messages.selectRootFolder)}
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index 8072e1464..f08ebcd5e 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -116,6 +116,8 @@
"components.Settings.RadarrModal.defaultserver": "Default Server",
"components.Settings.RadarrModal.editradarr": "Edit Radarr Server",
"components.Settings.RadarrModal.hostname": "Hostname",
+ "components.Settings.RadarrModal.loadingprofiles": "Loading quality profiles…",
+ "components.Settings.RadarrModal.loadingrootfolders": "Loading root folders…",
"components.Settings.RadarrModal.minimumAvailability": "Minimum Availability",
"components.Settings.RadarrModal.port": "Port",
"components.Settings.RadarrModal.qualityprofile": "Quality Profile",
@@ -130,6 +132,8 @@
"components.Settings.RadarrModal.servernamePlaceholder": "A Radarr Server",
"components.Settings.RadarrModal.ssl": "SSL",
"components.Settings.RadarrModal.test": "Test",
+ "components.Settings.RadarrModal.testFirstQualityProfiles": "Test your connection to load quality profiles",
+ "components.Settings.RadarrModal.testFirstRootFolders": "Test your connection to load root folders",
"components.Settings.RadarrModal.testing": "Testing...",
"components.Settings.RadarrModal.toastRadarrTestFailure": "Failed to connect to Radarr Server",
"components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!",
@@ -156,6 +160,8 @@
"components.Settings.SonarrModal.defaultserver": "Default Server",
"components.Settings.SonarrModal.editsonarr": "Edit Sonarr Server",
"components.Settings.SonarrModal.hostname": "Hostname",
+ "components.Settings.SonarrModal.loadingprofiles": "Loading quality profiles…",
+ "components.Settings.SonarrModal.loadingrootfolders": "Loading root folders…",
"components.Settings.SonarrModal.port": "Port",
"components.Settings.SonarrModal.qualityprofile": "Quality Profile",
"components.Settings.SonarrModal.rootfolder": "Root Folder",
@@ -169,6 +175,8 @@
"components.Settings.SonarrModal.servernamePlaceholder": "A Sonarr Server",
"components.Settings.SonarrModal.ssl": "SSL",
"components.Settings.SonarrModal.test": "Test",
+ "components.Settings.SonarrModal.testFirstQualityProfiles": "Test your connection to load quality profiles",
+ "components.Settings.SonarrModal.testFirstRootFolders": "Test your connection to load root folders",
"components.Settings.SonarrModal.testing": "Testing...",
"components.Settings.SonarrModal.toastRadarrTestFailure": "Could not connect to Sonarr Server",
"components.Settings.SonarrModal.toastRadarrTestSuccess": "Sonarr connection established!",
From d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef Mon Sep 17 00:00:00 2001
From: sct
Date: Thu, 17 Dec 2020 04:06:45 +0000
Subject: [PATCH 11/13] fix(email): do not pass auth object to transport if no
auth data present
re #312
---
server/lib/notifications/agents/email.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts
index 6ba96f608..185525251 100644
--- a/server/lib/notifications/agents/email.ts
+++ b/server/lib/notifications/agents/email.ts
@@ -29,10 +29,13 @@ class EmailAgent implements NotificationAgent {
host: emailSettings.smtpHost,
port: emailSettings.smtpPort,
secure: emailSettings.secure,
- auth: {
- user: emailSettings.authUser,
- pass: emailSettings.authPass,
- },
+ auth:
+ emailSettings.authUser && emailSettings.authPass
+ ? {
+ user: emailSettings.authUser,
+ pass: emailSettings.authPass,
+ }
+ : undefined,
});
}
From 67146c33ef7f28d520ba2c50b32673d43f4525c8 Mon Sep 17 00:00:00 2001
From: sct
Date: Thu, 17 Dec 2020 06:28:03 +0000
Subject: [PATCH 12/13] fix(plex-sync): bundle duplicate ratingKeys to speed up
recently added sync
This includes a rewrite to move movie/series availability notifications into a subscriber to prevent
duplicate notifications for series
fix #360
---
ormconfig.js | 2 +
server/entity/Media.ts | 29 -------
server/entity/Season.ts | 60 --------------
server/job/plexsync/index.ts | 21 ++++-
server/subscriber/MediaSubscriber.ts | 112 +++++++++++++++++++++++++++
5 files changed, 134 insertions(+), 90 deletions(-)
create mode 100644 server/subscriber/MediaSubscriber.ts
diff --git a/ormconfig.js b/ormconfig.js
index 93da376bc..2c0afb735 100644
--- a/ormconfig.js
+++ b/ormconfig.js
@@ -5,6 +5,7 @@ const devConfig = {
logging: false,
entities: ['server/entity/**/*.ts'],
migrations: ['server/migration/**/*.ts'],
+ subscribers: ['server/subscriber/**/*.ts'],
cli: {
entitiesDir: 'server/entity',
migrationsDir: 'server/migration',
@@ -19,6 +20,7 @@ const prodConfig = {
entities: ['dist/entity/**/*.js'],
migrations: ['dist/migration/**/*.js'],
migrationsRun: true,
+ subscribers: ['dist/subscriber/**/*.js'],
cli: {
entitiesDir: 'dist/entity',
migrationsDir: 'dist/migration',
diff --git a/server/entity/Media.ts b/server/entity/Media.ts
index 8f2f8ff6d..0222e1043 100644
--- a/server/entity/Media.ts
+++ b/server/entity/Media.ts
@@ -8,14 +8,11 @@ import {
UpdateDateColumn,
getRepository,
In,
- AfterUpdate,
} from 'typeorm';
import { MediaRequest } from './MediaRequest';
import { MediaStatus, MediaType } from '../constants/media';
import logger from '../logger';
import Season from './Season';
-import notificationManager, { Notification } from '../lib/notifications';
-import TheMovieDb from '../api/themoviedb';
@Entity()
class Media {
@@ -98,32 +95,6 @@ class Media {
constructor(init?: Partial) {
Object.assign(this, init);
}
-
- @AfterUpdate()
- private async _notifyAvailable() {
- if (this.status === MediaStatus.AVAILABLE) {
- if (this.mediaType === MediaType.MOVIE) {
- const requestRepository = getRepository(MediaRequest);
- const relatedRequests = await requestRepository.find({
- where: { media: this },
- });
-
- if (relatedRequests.length > 0) {
- const tmdb = new TheMovieDb();
- const movie = await tmdb.getMovie({ movieId: this.tmdbId });
-
- relatedRequests.forEach((request) => {
- notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
- notifyUser: request.requestedBy,
- subject: movie.title,
- message: movie.overview,
- image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
- });
- });
- }
- }
- }
- }
}
export default Media;
diff --git a/server/entity/Season.ts b/server/entity/Season.ts
index a591c3ca7..d66805cbd 100644
--- a/server/entity/Season.ts
+++ b/server/entity/Season.ts
@@ -5,15 +5,9 @@ import {
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
- AfterInsert,
- AfterUpdate,
- getRepository,
} from 'typeorm';
import { MediaStatus } from '../constants/media';
import Media from './Media';
-import logger from '../logger';
-import TheMovieDb from '../api/themoviedb';
-import notificationManager, { Notification } from '../lib/notifications';
@Entity()
class Season {
@@ -38,60 +32,6 @@ class Season {
constructor(init?: Partial) {
Object.assign(this, init);
}
-
- @AfterInsert()
- @AfterUpdate()
- private async _sendSeasonAvailableNotification() {
- if (this.status === MediaStatus.AVAILABLE) {
- try {
- const lazyMedia = await this.media;
- const tmdb = new TheMovieDb();
- const mediaRepository = getRepository(Media);
- const media = await mediaRepository.findOneOrFail({
- where: { id: lazyMedia.id },
- relations: ['requests'],
- });
-
- const availableSeasons = media.seasons.map(
- (season) => season.seasonNumber
- );
-
- const request = media.requests.find(
- (request) =>
- // Check if the season is complete AND it contains the current season that was just marked available
- request.seasons.every((season) =>
- availableSeasons.includes(season.seasonNumber)
- ) &&
- request.seasons.some(
- (season) => season.seasonNumber === this.seasonNumber
- )
- );
-
- if (request) {
- const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
- notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
- subject: tv.name,
- message: tv.overview,
- notifyUser: request.requestedBy,
- image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
- extra: [
- {
- name: 'Seasons',
- value: request.seasons
- .map((season) => season.seasonNumber)
- .join(', '),
- },
- ],
- });
- }
- } catch (e) {
- logger.error('Something went wrong sending season available notice', {
- label: 'Notifications',
- message: e.message,
- });
- }
- }
- }
}
export default Season;
diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts
index 87e078d40..c38197ffa 100644
--- a/server/job/plexsync/index.ts
+++ b/server/job/plexsync/index.ts
@@ -7,6 +7,7 @@ import { MediaStatus, MediaType } from '../../constants/media';
import logger from '../../logger';
import { getSettings, Library } from '../../lib/settings';
import Season from '../../entity/Season';
+import { uniqWith } from 'lodash';
const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000;
@@ -326,7 +327,25 @@ class JobPlexSync {
`Beginning to process recently added for library: ${library.name}`,
'info'
);
- this.items = await this.plexClient.getRecentlyAdded(library.id);
+ const libraryItems = await this.plexClient.getRecentlyAdded(
+ library.id
+ );
+
+ // Bundle items up by rating keys
+ this.items = uniqWith(libraryItems, (mediaA, mediaB) => {
+ if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) {
+ return (
+ mediaA.grandparentRatingKey === mediaB.grandparentRatingKey
+ );
+ }
+
+ if (mediaA.parentRatingKey && mediaB.parentRatingKey) {
+ return mediaA.parentRatingKey === mediaB.parentRatingKey;
+ }
+
+ return mediaA.ratingKey === mediaB.ratingKey;
+ });
+
await this.loop();
}
} else {
diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts
new file mode 100644
index 000000000..f63b14f64
--- /dev/null
+++ b/server/subscriber/MediaSubscriber.ts
@@ -0,0 +1,112 @@
+import {
+ EntitySubscriberInterface,
+ EventSubscriber,
+ getRepository,
+ UpdateEvent,
+} from 'typeorm';
+import TheMovieDb from '../api/themoviedb';
+import { MediaStatus, MediaType } from '../constants/media';
+import Media from '../entity/Media';
+import { MediaRequest } from '../entity/MediaRequest';
+import notificationManager, { Notification } from '../lib/notifications';
+
+@EventSubscriber()
+export class MediaSubscriber implements EntitySubscriberInterface {
+ private async notifyAvailableMovie(entity: Media) {
+ if (entity.status === MediaStatus.AVAILABLE) {
+ if (entity.mediaType === MediaType.MOVIE) {
+ const requestRepository = getRepository(MediaRequest);
+ const relatedRequests = await requestRepository.find({
+ where: { media: entity },
+ });
+
+ if (relatedRequests.length > 0) {
+ const tmdb = new TheMovieDb();
+ const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
+
+ relatedRequests.forEach((request) => {
+ notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
+ notifyUser: request.requestedBy,
+ subject: movie.title,
+ message: movie.overview,
+ image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
+ });
+ });
+ }
+ }
+ }
+ }
+
+ private async notifyAvailableSeries(entity: Media, dbEntity: Media) {
+ const newAvailableSeasons = entity.seasons
+ .filter((season) => season.status === MediaStatus.AVAILABLE)
+ .map((season) => season.seasonNumber);
+ const oldAvailableSeasons = dbEntity.seasons
+ .filter((season) => season.status === MediaStatus.AVAILABLE)
+ .map((season) => season.seasonNumber);
+
+ const changedSeasons = newAvailableSeasons.filter(
+ (seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
+ );
+
+ if (changedSeasons.length > 0) {
+ const tmdb = new TheMovieDb();
+ const requestRepository = getRepository(MediaRequest);
+ const processedSeasons: number[] = [];
+
+ for (const changedSeasonNumber of changedSeasons) {
+ const requests = await requestRepository.find({
+ where: { media: entity },
+ });
+ const request = requests.find(
+ (request) =>
+ // Check if the season is complete AND it contains the current season that was just marked available
+ request.seasons.every((season) =>
+ newAvailableSeasons.includes(season.seasonNumber)
+ ) &&
+ request.seasons.some(
+ (season) => season.seasonNumber === changedSeasonNumber
+ )
+ );
+
+ if (request && !processedSeasons.includes(changedSeasonNumber)) {
+ processedSeasons.push(
+ ...request.seasons.map((season) => season.seasonNumber)
+ );
+ const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
+ notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
+ subject: tv.name,
+ message: tv.overview,
+ notifyUser: request.requestedBy,
+ image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
+ extra: [
+ {
+ name: 'Seasons',
+ value: request.seasons
+ .map((season) => season.seasonNumber)
+ .join(', '),
+ },
+ ],
+ });
+ }
+ }
+ }
+ }
+
+ public beforeUpdate(event: UpdateEvent): void {
+ if (
+ event.entity.mediaType === MediaType.MOVIE &&
+ event.entity.status === MediaStatus.AVAILABLE
+ ) {
+ this.notifyAvailableMovie(event.entity);
+ }
+
+ if (
+ event.entity.mediaType === MediaType.TV &&
+ (event.entity.status === MediaStatus.AVAILABLE ||
+ event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
+ ) {
+ this.notifyAvailableSeries(event.entity, event.databaseEntity);
+ }
+ }
+}
From 18925decafdac518f52a354c594cc378d2529022 Mon Sep 17 00:00:00 2001
From: sct
Date: Thu, 17 Dec 2020 12:05:45 +0000
Subject: [PATCH 13/13] fix(frontend): correctly show an unauthorized error
when a user fails to login
fixes #322
---
server/routes/auth.ts | 25 ++++++++++--
src/components/Login/index.tsx | 49 ++++++++++++++++++++++--
src/components/PlexLoginButton/index.tsx | 4 +-
src/styles/globals.css | 2 +-
4 files changed, 70 insertions(+), 10 deletions(-)
diff --git a/server/routes/auth.ts b/server/routes/auth.ts
index 29314d337..50a5f2201 100644
--- a/server/routes/auth.ts
+++ b/server/routes/auth.ts
@@ -24,7 +24,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
return res.status(200).json(user.filter());
});
-authRoutes.post('/login', async (req, res) => {
+authRoutes.post('/login', async (req, res, next) => {
const userRepository = getRepository(User);
const body = req.body as { authToken?: string };
@@ -86,6 +86,22 @@ authRoutes.post('/login', async (req, res) => {
avatar: account.thumb,
});
await userRepository.save(user);
+ } else {
+ logger.info(
+ 'Failed login attempt from user without access to plex server',
+ {
+ label: 'Auth',
+ account: {
+ ...account,
+ authentication_token: '__REDACTED__',
+ authToken: '__REDACTED__',
+ },
+ }
+ );
+ return next({
+ status: 403,
+ message: 'You do not have access to this Plex server',
+ });
}
}
@@ -97,9 +113,10 @@ authRoutes.post('/login', async (req, res) => {
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
logger.error(e.message, { label: 'Auth' });
- res
- .status(500)
- .json({ error: 'Something went wrong. Is your auth token valid?' });
+ return next({
+ status: 500,
+ message: 'Something went wrong. Is your auth token valid?',
+ });
}
});
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx
index 484010a2b..87095b7bf 100644
--- a/src/components/Login/index.tsx
+++ b/src/components/Login/index.tsx
@@ -5,12 +5,15 @@ import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import ImageFader from '../Common/ImageFader';
import { defineMessages, FormattedMessage } from 'react-intl';
+import Transition from '../Transition';
const messages = defineMessages({
signinplex: 'Sign in to continue',
});
const Login: React.FC = () => {
+ const [error, setError] = useState('');
+ const [isProcessing, setProcessing] = useState(false);
const [authToken, setAuthToken] = useState(undefined);
const { user, revalidate } = useUser();
const router = useRouter();
@@ -20,10 +23,17 @@ const Login: React.FC = () => {
// ask swr to revalidate the user which _shouid_ come back with a valid user.
useEffect(() => {
const login = async () => {
- const response = await axios.post('/api/v1/auth/login', { authToken });
+ setProcessing(true);
+ try {
+ const response = await axios.post('/api/v1/auth/login', { authToken });
- if (response.data?.email) {
- revalidate();
+ if (response.data?.email) {
+ revalidate();
+ }
+ } catch (e) {
+ setError(e.response.data.message);
+ setAuthToken(undefined);
+ setProcessing(false);
}
};
if (authToken) {
@@ -64,7 +74,40 @@ const Login: React.FC = () => {
className="bg-gray-800 bg-opacity-50 py-8 px-4 shadow sm:rounded-lg sm:px-10"
style={{ backdropFilter: 'blur(5px)' }}
>
+
+
+
setAuthToken(authToken)}
/>
diff --git a/src/components/PlexLoginButton/index.tsx b/src/components/PlexLoginButton/index.tsx
index 56883fb39..3c58e2336 100644
--- a/src/components/PlexLoginButton/index.tsx
+++ b/src/components/PlexLoginButton/index.tsx
@@ -12,23 +12,23 @@ const plexOAuth = new PlexOAuth();
interface PlexLoginButtonProps {
onAuthToken: (authToken: string) => void;
+ isProcessing?: boolean;
onError?: (message: string) => void;
}
const PlexLoginButton: React.FC