From ad18a4096942c7f0e4599074a90c480d9f129677 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 16 Dec 2020 14:06:25 +0900 Subject: [PATCH 01/13] docs: add samwiseg0 as a contributor (#341) [skip ci] * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 31a4cc63b..2508062ea 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -124,6 +124,16 @@ "contributions": [ "code" ] + }, + { + "login": "samwiseg0", + "name": "samwiseg0", + "avatar_url": "https://avatars1.githubusercontent.com/u/2241731?v=4", + "profile": "https://github.com/samwiseg0", + "contributions": [ + "question", + "infra" + ] } ], "badgeTemplate": "\"All-orange.svg\"/>", diff --git a/README.md b/README.md index 6c52d0153..70e90d687 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Language grade: JavaScript GitHub -All Contributors +All Contributors

@@ -114,10 +114,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Shutruk

🌍
Krystian Charubin

🎨
Kieron Boswell

💻 +
samwiseg0

💬 🚇 - From e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf Mon Sep 17 00:00:00 2001 From: ecelebi29 Date: Thu, 17 Dec 2020 03:18:18 +0300 Subject: [PATCH 02/13] fix(sonarr.ts, mediarequest.ts): add missing seasonFolder option (#358) --- server/api/sonarr.ts | 2 ++ server/entity/MediaRequest.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/server/api/sonarr.ts b/server/api/sonarr.ts index 72a2de6b4..4a86b68bb 100644 --- a/server/api/sonarr.ts +++ b/server/api/sonarr.ts @@ -76,6 +76,7 @@ interface AddSeriesOptions { title: string; profileId: number; seasons: number[]; + seasonFolder: boolean; rootFolderPath: string; monitored?: boolean; searchNow?: boolean; @@ -149,6 +150,7 @@ class SonarrAPI { monitored: false, })) ), + seasonFolder: options.seasonFolder, monitored: options.monitored, rootFolderPath: options.rootFolderPath, addOptions: { diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 1bfe23741..e584dc55c 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -335,6 +335,7 @@ export class MediaRequest { title: series.name, tvdbid: series.external_ids.tvdb_id, seasons: this.seasons.map((season) => season.seasonNumber), + seasonFolder: sonarrSettings.enableSeasonFolders, monitored: true, searchNow: true, }); From 3210ac29d053421fa4e01cb99050ab04bf263707 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 17 Dec 2020 09:20:28 +0900 Subject: [PATCH 03/13] docs: add ecelebi29 as a contributor (#359) [skip ci] * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2508062ea..1e66d0395 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -134,6 +134,15 @@ "question", "infra" ] + }, + { + "login": "ecelebi29", + "name": "ecelebi29", + "avatar_url": "https://avatars2.githubusercontent.com/u/8337120?v=4", + "profile": "https://github.com/ecelebi29", + "contributions": [ + "code" + ] } ], "badgeTemplate": "\"All-orange.svg\"/>", diff --git a/README.md b/README.md index 70e90d687..d2a391f01 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Language grade: JavaScript GitHub -All Contributors +All Contributors

@@ -116,6 +116,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Kieron Boswell

💻
samwiseg0

💬 🚇 + +
ecelebi29

💻 + From bb89f679b6de6bcc4cd021cc8a472d21ba403d0e Mon Sep 17 00:00:00 2001 From: ecelebi29 Date: Thu, 17 Dec 2020 04:07:07 +0300 Subject: [PATCH 04/13] docs(contributing.md, readme.md): fixed 2 small typos (#361) [skip ci] --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6267abc3..66d98b66e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ You can also run the development environment in [Docker](https://www.docker.com/ - PRs with commits not following this standard will not be merged. - Please make meaningful commits, or squash them - Always rebase your commit to the latest `develop` branch. Do not merge develop into your branch. -- It is your responsbility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch. +- It is your responsibility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch. - You can create a Draft pull request early to get feedback on your work. - Your code must be formatted correctly or the tests will fail. - We use Prettier to format our codebase. It should auto run with a git hook, but its recommended to have a Prettier extension installed in your editor and have it format on save. diff --git a/README.md b/README.md index d2a391f01..b7a5abfd7 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ - More notification types (Slack/Telegram/etc.). - Issues system. This will allow users to report issues with content on your media server. - Local user system (for those who don't use Plex). -- Compatiblity APIs (to work with existing tools in your system). +- Compatibility APIs (to work with existing tools in your system). ## Running Overseerr From 647d5efb095d18e19f702452d20e5b697d721206 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:07:40 +0900 Subject: [PATCH 05/13] docs: add ecelebi29 as a contributor (#362) [skip ci] * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1e66d0395..378c8ee28 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -141,7 +141,8 @@ "avatar_url": "https://avatars2.githubusercontent.com/u/8337120?v=4", "profile": "https://github.com/ecelebi29", "contributions": [ - "code" + "code", + "doc" ] } ], diff --git a/README.md b/README.md index b7a5abfd7..03d6e5276 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
samwiseg0

💬 🚇 -
ecelebi29

💻 +
ecelebi29

💻 📖 From c21fa5b5350abdd8e03c077fde7246fa398e176e Mon Sep 17 00:00:00 2001 From: sct Date: Thu, 17 Dec 2020 01:14:12 +0000 Subject: [PATCH 06/13] fix(frontend): fix tv shows failing to open when firstAirDate is undefined fix #347 --- server/models/Tv.ts | 2 +- src/components/TitleCard/index.tsx | 4 ++-- src/components/TvDetails/index.tsx | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/models/Tv.ts b/server/models/Tv.ts index f303c95c2..f1e8f7797 100644 --- a/server/models/Tv.ts +++ b/server/models/Tv.ts @@ -56,7 +56,7 @@ export interface TvDetails { profilePath?: string; }[]; episodeRunTime: number[]; - firstAirDate: string; + firstAirDate?: string; genres: Genre[]; homepage: string; inProduction: boolean; diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx index 443f06a1b..c460a9f90 100644 --- a/src/components/TitleCard/index.tsx +++ b/src/components/TitleCard/index.tsx @@ -21,7 +21,7 @@ interface TitleCardProps { id: number; image?: string; summary?: string; - year: string; + year?: string; title: string; userScore: number; mediaType: MediaType; @@ -169,7 +169,7 @@ const TitleCard: React.FC = ({ >
-
{year}
+ {year &&
{year}
}

{title} diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 451a119f8..84e5fc156 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -227,8 +227,12 @@ const TvDetails: React.FC = ({ tv }) => { )}

- {data.name}{' '} - ({data.firstAirDate.slice(0, 4)}) + {data.name} + {data.firstAirDate && ( + + ({data.firstAirDate.slice(0, 4)}) + + )}

{data.genres.map((g) => g.name).join(', ')} From ce0266f74ea3979b291ff962271a928682892788 Mon Sep 17 00:00:00 2001 From: sct Date: Thu, 17 Dec 2020 01:36:23 +0000 Subject: [PATCH 07/13] fix(frontend): add http/https prefix to hostname fields for plex/radarr/sonarr fixes #357 --- src/components/Settings/RadarrModal/index.tsx | 5 ++++- src/components/Settings/SettingsPlex.tsx | 5 ++++- src/components/Settings/SonarrModal/index.tsx | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index f7d80081f..dbe47c9fd 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -316,6 +316,9 @@ const RadarrModal: React.FC = ({
+ + {values.ssl ? 'https://' : 'http://'} + = ({ setIsValidated(false); setFieldValue('hostname', e.target.value); }} - className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500" + className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500" />
{errors.hostname && touched.hostname && ( diff --git a/src/components/Settings/SettingsPlex.tsx b/src/components/Settings/SettingsPlex.tsx index d4fa052ed..0fa23ad3b 100644 --- a/src/components/Settings/SettingsPlex.tsx +++ b/src/components/Settings/SettingsPlex.tsx @@ -224,12 +224,15 @@ const SettingsPlex: React.FC = ({ onComplete }) => {
+ + {values.useSsl ? 'https://' : 'http://'} +
{errors.hostname && touched.hostname && ( diff --git a/src/components/Settings/SonarrModal/index.tsx b/src/components/Settings/SonarrModal/index.tsx index a98c64ed0..9303131d7 100644 --- a/src/components/Settings/SonarrModal/index.tsx +++ b/src/components/Settings/SonarrModal/index.tsx @@ -319,6 +319,9 @@ const SonarrModal: React.FC = ({
+ + {values.ssl ? 'https://' : 'http://'} + = ({ setIsValidated(false); setFieldValue('hostname', e.target.value); }} - className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500" + className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500" />
{errors.hostname && touched.hostname && ( From 2fe53ec5a8534e75c7d0cef31a8b46065111e0a7 Mon Sep 17 00:00:00 2001 From: sct Date: Thu, 17 Dec 2020 01:42:21 +0000 Subject: [PATCH 08/13] fix(frontend): make minimum availability required for Radarr servers fixes #345 --- src/components/Settings/RadarrModal/index.tsx | 10 ++++++++++ src/i18n/locale/en.json | 1 + 2 files changed, 11 insertions(+) diff --git a/src/components/Settings/RadarrModal/index.tsx b/src/components/Settings/RadarrModal/index.tsx index dbe47c9fd..0734a8b96 100644 --- a/src/components/Settings/RadarrModal/index.tsx +++ b/src/components/Settings/RadarrModal/index.tsx @@ -17,6 +17,7 @@ const messages = defineMessages({ validationApiKeyRequired: 'You must provide an API key', validationRootFolderRequired: 'You must select a root folder', validationProfileRequired: 'You must select a profile', + validationMinimumAvailabilityRequired: 'You must select minimum availability', toastRadarrTestSuccess: 'Radarr connection established!', toastRadarrTestFailure: 'Failed to connect to Radarr Server', saving: 'Saving...', @@ -89,6 +90,9 @@ const RadarrModal: React.FC = ({ activeProfileId: Yup.string().required( intl.formatMessage(messages.validationProfileRequired) ), + minimumAvailability: Yup.string().required( + intl.formatMessage(messages.validationMinimumAvailabilityRequired) + ), }); const testConnection = useCallback( @@ -534,6 +538,12 @@ const RadarrModal: React.FC = ({
+ {errors.minimumAvailability && + touched.minimumAvailability && ( +
+ {errors.minimumAvailability} +
+ )}
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 8b1b27e39..b1d7b2bf0 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -135,6 +135,7 @@ "components.Settings.RadarrModal.toastRadarrTestSuccess": "Radarr connection established!", "components.Settings.RadarrModal.validationApiKeyRequired": "You must provide an API key", "components.Settings.RadarrModal.validationHostnameRequired": "You must provide a hostname/IP", + "components.Settings.RadarrModal.validationMinimumAvailabilityRequired": "You must select minimum availability", "components.Settings.RadarrModal.validationNameRequired": "You must provide a server name", "components.Settings.RadarrModal.validationPortRequired": "You must provide a port", "components.Settings.RadarrModal.validationProfileRequired": "You must select a profile", From 0d088e085e68d39455fda21d1fd08ebcaef2c06b Mon Sep 17 00:00:00 2001 From: sct Date: Thu, 17 Dec 2020 02:08:09 +0000 Subject: [PATCH 09/13] feat(frontend): show alert when there are no default radarr/sonarr servers fixes #344 --- src/components/Settings/SettingsServices.tsx | 228 ++++++++++++------- src/i18n/locale/en.json | 3 + 2 files changed, 143 insertions(+), 88 deletions(-) diff --git a/src/components/Settings/SettingsServices.tsx b/src/components/Settings/SettingsServices.tsx index 07ccafd3a..5a3b1e8c5 100644 --- a/src/components/Settings/SettingsServices.tsx +++ b/src/components/Settings/SettingsServices.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { defineMessages, FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Badge from '../Common/Badge'; import Button from '../Common/Button'; import useSWR from 'swr'; @@ -31,8 +31,48 @@ const messages = defineMessages({ activeProfile: 'Active Profile', addradarr: 'Add Radarr Server', addsonarr: 'Add Sonarr Server', + nodefault: 'No default server selected!', + nodefaultdescription: + 'At least one server must be marked as default before any requests will make it to your services.', + no4kimplemented: '(Default 4K servers are not currently implemented)', }); +const NoDefaultAlert: React.FC = () => { + const intl = useIntl(); + return ( +
+
+
+ +
+
+

+ {intl.formatMessage(messages.nodefault)} +

+
+

{intl.formatMessage(messages.nodefaultdescription)}

+

+ {intl.formatMessage(messages.no4kimplemented)} +

+
+
+
+
+ ); +}; + interface ServerInstanceProps { name: string; isDefault?: boolean; @@ -249,51 +289,57 @@ const SettingsServices: React.FC = () => {
{!radarrData && !radarrError && } {radarrData && !radarrError && ( -
    - {radarrData.map((radarr) => ( - setEditRadarrModal({ open: true, radarr })} - onDelete={() => - setDeleteServerModal({ - open: true, - serverId: radarr.id, - type: 'radarr', - }) - } - /> - ))} -
  • -
    - -
    -
  • -
+ + + + + +
+ + + )}
@@ -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', - }) - } - /> - ))} -
  • -
    - -
    -
  • -
+ + + + + +
+ + + )}
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" > {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" > {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" > - 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" > {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" > {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)' }} > + +
+
+
+ +
+
+

{error}

+
+
+
+
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 = ({ onAuthToken, onError, + isProcessing, }) => { const intl = useIntl(); const [loading, setLoading] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); const getPlexLogin = async () => { setLoading(true); try { const authToken = await plexOAuth.login(); setLoading(false); - setIsProcessing(true); onAuthToken(authToken); } catch (e) { if (onError) { diff --git a/src/styles/globals.css b/src/styles/globals.css index 18c2b0ff0..9df59f095 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -7,7 +7,7 @@ body { } .plex-button { - @apply w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 transition ease-in-out duration-150 text-center; + @apply w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 transition ease-in-out duration-150 text-center disabled:opacity-50; background-color: #cc7b19; }