From c62fc9d05bb9e1fe51b454d78e80bd9250e31f89 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 1 Dec 2024 16:20:55 -0800 Subject: [PATCH] New: Kometa metadata file creation disabled Closes #7400 --- frontend/src/App/State/AppSectionState.ts | 10 ++ frontend/src/App/State/MetadataAppState.ts | 6 + frontend/src/App/State/SettingsAppState.ts | 2 + .../Components/Form/ProviderFieldFormGroup.js | 2 + .../Metadata/Metadata/EditMetadataModal.js | 27 ---- .../Metadata/Metadata/EditMetadataModal.tsx | 36 +++++ .../Metadata/EditMetadataModalConnector.js | 44 ----- .../Metadata/EditMetadataModalContent.css | 5 + .../EditMetadataModalContent.css.d.ts | 7 + .../Metadata/EditMetadataModalContent.js | 105 ------------ .../Metadata/EditMetadataModalContent.tsx | 128 +++++++++++++++ .../EditMetadataModalContentConnector.js | 95 ----------- .../Settings/Metadata/Metadata/Metadata.js | 150 ------------------ .../Settings/Metadata/Metadata/Metadata.tsx | 107 +++++++++++++ .../Settings/Metadata/Metadata/Metadatas.js | 44 ----- .../Settings/Metadata/Metadata/Metadatas.tsx | 52 ++++++ .../src/Settings/Metadata/MetadataSettings.js | 4 +- .../src/Store/Selectors/selectSettings.ts | 1 + frontend/src/typings/DownloadClient.ts | 11 +- frontend/src/typings/Field.ts | 2 + frontend/src/typings/ImportList.ts | 11 +- frontend/src/typings/Indexer.ts | 11 +- frontend/src/typings/Metadata.ts | 7 + frontend/src/typings/Notification.ts | 11 +- frontend/src/typings/Provider.ts | 20 +++ frontend/src/typings/pending.ts | 2 +- .../Consumers/Kometa/KometaMetadata.cs | 94 ++--------- .../Kometa/KometaMetadataSettings.cs | 14 +- .../HealthCheck/Checks/MetadataCheck.cs | 34 ++++ src/NzbDrone.Core/Localization/Core/en.json | 2 + 30 files changed, 445 insertions(+), 599 deletions(-) create mode 100644 frontend/src/App/State/MetadataAppState.ts delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx delete mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js delete mode 100644 frontend/src/Settings/Metadata/Metadata/Metadata.js create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadata.tsx delete mode 100644 frontend/src/Settings/Metadata/Metadata/Metadatas.js create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadatas.tsx create mode 100644 frontend/src/typings/Metadata.ts create mode 100644 frontend/src/typings/Provider.ts create mode 100644 src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index fa55c8e38..4e9dbe7a0 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -63,6 +63,16 @@ export interface AppSectionItemState { item: T; } +export interface AppSectionProviderState + extends AppSectionDeleteState, + AppSectionSaveState { + isFetching: boolean; + isPopulated: boolean; + error: Error; + items: T[]; + pendingChanges: Partial; +} + interface AppSectionState { isFetching: boolean; isPopulated: boolean; diff --git a/frontend/src/App/State/MetadataAppState.ts b/frontend/src/App/State/MetadataAppState.ts new file mode 100644 index 000000000..60d5c434c --- /dev/null +++ b/frontend/src/App/State/MetadataAppState.ts @@ -0,0 +1,6 @@ +import { AppSectionProviderState } from 'App/State/AppSectionState'; +import Metadata from 'typings/Metadata'; + +interface MetadataAppState extends AppSectionProviderState {} + +export default MetadataAppState; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 6299b498d..28d3fc098 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig'; import NamingExample from 'typings/Settings/NamingExample'; import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import UiSettings from 'typings/Settings/UiSettings'; +import MetadataAppState from './MetadataAppState'; export interface DownloadClientAppState extends AppSectionState, @@ -97,6 +98,7 @@ interface SettingsAppState { indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; languages: LanguageSettingsAppState; + metadata: MetadataAppState; naming: NamingAppState; namingExamples: NamingExamplesAppState; notifications: NotificationAppState; diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index a4f13dbd1..f081f5906 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -138,6 +138,8 @@ ProviderFieldFormGroup.propTypes = { type: PropTypes.string.isRequired, advanced: PropTypes.bool.isRequired, hidden: PropTypes.string, + isDisabled: PropTypes.bool, + provider: PropTypes.string, pending: PropTypes.bool.isRequired, errors: PropTypes.arrayOf(PropTypes.object).isRequired, warnings: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js deleted file mode 100644 index 4b33df528..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EditMetadataModalContentConnector from './EditMetadataModalContentConnector'; - -function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditMetadataModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditMetadataModal; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx new file mode 100644 index 000000000..6dd30ca78 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditMetadataModalContent, { + EditMetadataModalContentProps, +} from './EditMetadataModalContent'; + +interface EditMetadataModalProps extends EditMetadataModalContentProps { + isOpen: boolean; +} + +function EditMetadataModal({ + isOpen, + onModalClose, + ...otherProps +}: EditMetadataModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(clearPendingChanges({ section: 'metadata' })); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditMetadataModal; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js deleted file mode 100644 index 7513bb82c..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditMetadataModal from './EditMetadataModal'; - -function createMapDispatchToProps(dispatch, props) { - const section = 'settings.metadata'; - - return { - dispatchClearPendingChanges() { - dispatch(clearPendingChanges({ section })); - } - }; -} - -class EditMetadataModalConnector extends Component { - // - // Listeners - - onModalClose = () => { - this.props.dispatchClearPendingChanges({ section: 'metadata' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditMetadataModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css new file mode 100644 index 000000000..7393b9c35 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css @@ -0,0 +1,5 @@ +.message { + composes: alert from '~Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts new file mode 100644 index 000000000..65c237dff --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'message': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js deleted file mode 100644 index 221c6bcaf..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js +++ /dev/null @@ -1,105 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { inputTypes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function EditMetadataModalContent(props) { - const { - advancedSettings, - isSaving, - saveError, - item, - onInputChange, - onFieldChange, - onModalClose, - onSavePress, - ...otherProps - } = props; - - const { - name, - enable, - fields - } = item; - - return ( - - - {translate('EditMetadata', { metadataType: name.value })} - - - -
- - {translate('Enable')} - - - - - { - fields.map((field) => { - return ( - - ); - }) - } - - -
- - - - - - {translate('Save')} - - -
- ); -} - -EditMetadataModalContent.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onFieldChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onDeleteMetadataPress: PropTypes.func -}; - -export default EditMetadataModalContent; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx new file mode 100644 index 000000000..997a4c39c --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.tsx @@ -0,0 +1,128 @@ +import React, { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { inputTypes } from 'Helpers/Props'; +import { + saveMetadata, + setMetadataFieldValue, + setMetadataValue, +} from 'Store/Actions/settingsActions'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './EditMetadataModalContent.css'; + +export interface EditMetadataModalContentProps { + id: number; + advancedSettings: boolean; + onModalClose: () => void; +} + +function EditMetadataModalContent({ + id, + advancedSettings, + onModalClose, +}: EditMetadataModalContentProps) { + const dispatch = useDispatch(); + + const { isSaving, saveError, pendingChanges, items } = useSelector( + (state: AppState) => state.settings.metadata + ); + + const { settings, ...otherSettings } = useMemo(() => { + const item = items.find((item) => item.id === id)!; + + return selectSettings(item, pendingChanges, saveError); + }, [id, items, pendingChanges, saveError]); + + const { name, enable, fields, message } = settings; + + const handleInputChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error not typed + dispatch(setMetadataValue({ name, value })); + }, + [dispatch] + ); + + const handleFieldChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error not typed + dispatch(setMetadataFieldValue({ name, value })); + }, + [dispatch] + ); + + const handleSavePress = useCallback(() => { + dispatch(saveMetadata({ id })); + }, [id, dispatch]); + + return ( + + + {translate('EditMetadata', { metadataType: name.value })} + + + +
+ {message ? ( + + {message.value.message} + + ) : null} + + + {translate('Enable')} + + + + + {fields.map((field) => { + return ( + + ); + })} + +
+ + + + + + {translate('Save')} + + +
+ ); +} + +export default EditMetadataModalContent; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js deleted file mode 100644 index 62dae94f6..000000000 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js +++ /dev/null @@ -1,95 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveMetadata, setMetadataFieldValue, setMetadataValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditMetadataModalContent from './EditMetadataModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - (state, { id }) => id, - (state) => state.settings.metadata, - (advancedSettings, id, metadata) => { - const { - isSaving, - saveError, - pendingChanges, - items - } = metadata; - - const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError); - - return { - advancedSettings, - id, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - setMetadataValue, - setMetadataFieldValue, - saveMetadata -}; - -class EditMetadataModalContentConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setMetadataValue({ name, value }); - }; - - onFieldChange = ({ name, value }) => { - this.props.setMetadataFieldValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveMetadata({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditMetadataModalContentConnector.propTypes = { - id: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setMetadataValue: PropTypes.func.isRequired, - setMetadataFieldValue: PropTypes.func.isRequired, - saveMetadata: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.js b/frontend/src/Settings/Metadata/Metadata/Metadata.js deleted file mode 100644 index ffb0ab967..000000000 --- a/frontend/src/Settings/Metadata/Metadata/Metadata.js +++ /dev/null @@ -1,150 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditMetadataModalConnector from './EditMetadataModalConnector'; -import styles from './Metadata.css'; - -class Metadata extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditMetadataModalOpen: false - }; - } - - // - // Listeners - - onEditMetadataPress = () => { - this.setState({ isEditMetadataModalOpen: true }); - }; - - onEditMetadataModalClose = () => { - this.setState({ isEditMetadataModalOpen: false }); - }; - - // - // Render - - render() { - const { - id, - name, - enable, - fields - } = this.props; - - const metadataFields = []; - const imageFields = []; - - fields.forEach((field) => { - if (field.section === 'metadata') { - metadataFields.push(field); - } else { - imageFields.push(field); - } - }); - - return ( - -
- {name} -
- -
- { - enable ? - : - - } -
- - { - enable && !!metadataFields.length && -
-
- {translate('Metadata')} -
- - { - metadataFields.map((field) => { - if (!field.value) { - return null; - } - - return ( - - ); - }) - } -
- } - - { - enable && !!imageFields.length && -
-
- {translate('Images')} -
- - { - imageFields.map((field) => { - if (!field.value) { - return null; - } - - return ( - - ); - }) - } -
- } - - -
- ); - } -} - -Metadata.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - enable: PropTypes.bool.isRequired, - fields: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Metadata; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.tsx b/frontend/src/Settings/Metadata/Metadata/Metadata.tsx new file mode 100644 index 000000000..52797218d --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.tsx @@ -0,0 +1,107 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import { kinds } from 'Helpers/Props'; +import Field from 'typings/Field'; +import translate from 'Utilities/String/translate'; +import EditMetadataModal from './EditMetadataModal'; +import styles from './Metadata.css'; + +interface MetadataProps { + id: number; + name: string; + enable: boolean; + fields: Field[]; +} + +function Metadata({ id, name, enable, fields }: MetadataProps) { + const [isEditMetadataModalOpen, setIsEditMetadataModalOpen] = useState(false); + + const { metadataFields, imageFields } = useMemo(() => { + return fields.reduce<{ metadataFields: Field[]; imageFields: Field[] }>( + (acc, field) => { + if (field.section === 'metadata') { + acc.metadataFields.push(field); + } else { + acc.imageFields.push(field); + } + + return acc; + }, + { metadataFields: [], imageFields: [] } + ); + }, [fields]); + + const handleOpenPress = useCallback(() => { + setIsEditMetadataModalOpen(true); + }, []); + + const handleModalClose = useCallback(() => { + setIsEditMetadataModalOpen(false); + }, []); + + return ( + +
{name}
+ +
+ {enable ? ( + + ) : ( + + )} +
+ + {enable && metadataFields.length ? ( +
+
{translate('Metadata')}
+ + {metadataFields.map((field) => { + if (!field.value) { + return null; + } + + return ( + + ); + })} +
+ ) : null} + + {enable && imageFields.length ? ( +
+
{translate('Images')}
+ + {imageFields.map((field) => { + if (!field.value) { + return null; + } + + return ( + + ); + })} +
+ ) : null} + + +
+ ); +} + +export default Metadata; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.js b/frontend/src/Settings/Metadata/Metadata/Metadatas.js deleted file mode 100644 index a52275bcc..000000000 --- a/frontend/src/Settings/Metadata/Metadata/Metadatas.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import translate from 'Utilities/String/translate'; -import Metadata from './Metadata'; -import styles from './Metadatas.css'; - -function Metadatas(props) { - const { - items, - ...otherProps - } = props; - - return ( -
- -
- { - items.map((item) => { - return ( - - ); - }) - } -
-
-
- ); -} - -Metadatas.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Metadatas; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.tsx b/frontend/src/Settings/Metadata/Metadata/Metadatas.tsx new file mode 100644 index 000000000..befe207d8 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.tsx @@ -0,0 +1,52 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import MetadataAppState from 'App/State/MetadataAppState'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import { fetchMetadata } from 'Store/Actions/settingsActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import MetadataType from 'typings/Metadata'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import Metadata from './Metadata'; +import styles from './Metadatas.css'; + +function createMetadatasSelector() { + return createSelector( + createSortedSectionSelector( + 'settings.metadata', + sortByProp('name') + ), + (metadata: MetadataAppState) => metadata + ); +} + +function Metadatas() { + const dispatch = useDispatch(); + const { isFetching, error, items, ...otherProps } = useSelector( + createMetadatasSelector() + ); + + useEffect(() => { + dispatch(fetchMetadata()); + }, [dispatch]); + + return ( +
+ +
+ {items.map((item) => { + return ; + })} +
+
+
+ ); +} + +export default Metadatas; diff --git a/frontend/src/Settings/Metadata/MetadataSettings.js b/frontend/src/Settings/Metadata/MetadataSettings.js index 5c9f9ea82..143a05956 100644 --- a/frontend/src/Settings/Metadata/MetadataSettings.js +++ b/frontend/src/Settings/Metadata/MetadataSettings.js @@ -3,7 +3,7 @@ import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import MetadatasConnector from './Metadata/MetadatasConnector'; +import Metadatas from './Metadata/Metadatas'; function MetadataSettings() { return ( @@ -13,7 +13,7 @@ function MetadataSettings() { /> - + ); diff --git a/frontend/src/Store/Selectors/selectSettings.ts b/frontend/src/Store/Selectors/selectSettings.ts index 2fb229e75..75665d73b 100644 --- a/frontend/src/Store/Selectors/selectSettings.ts +++ b/frontend/src/Store/Selectors/selectSettings.ts @@ -100,6 +100,7 @@ function selectSettings( const setting: Pending = { value: item[key], + pending: false, errors: getFailures(errors, key), warnings: getFailures(warnings, key), }; diff --git a/frontend/src/typings/DownloadClient.ts b/frontend/src/typings/DownloadClient.ts index 547b8c620..417d74b0b 100644 --- a/frontend/src/typings/DownloadClient.ts +++ b/frontend/src/typings/DownloadClient.ts @@ -1,20 +1,13 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; export type Protocol = 'torrent' | 'usenet' | 'unknown'; -interface DownloadClient extends ModelBase { +interface DownloadClient extends Provider { enable: boolean; protocol: Protocol; priority: number; removeCompletedDownloads: boolean; removeFailedDownloads: boolean; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Field.ts b/frontend/src/typings/Field.ts index 24a0b35ac..404c436ef 100644 --- a/frontend/src/typings/Field.ts +++ b/frontend/src/typings/Field.ts @@ -13,6 +13,8 @@ interface Field { name: string; label: string; value: boolean | number | string | number[]; + section: string; + hidden: 'hidden' | 'hiddenIfNotSet' | 'visible'; type: string; advanced: boolean; privacy: string; diff --git a/frontend/src/typings/ImportList.ts b/frontend/src/typings/ImportList.ts index a7aa48f26..7e596b25d 100644 --- a/frontend/src/typings/ImportList.ts +++ b/frontend/src/typings/ImportList.ts @@ -1,17 +1,10 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; -interface ImportList extends ModelBase { +interface ImportList extends Provider { enable: boolean; enableAutomaticAdd: boolean; qualityProfileId: number; rootFolderPath: string; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Indexer.ts b/frontend/src/typings/Indexer.ts index dbfed94a8..ea38651f4 100644 --- a/frontend/src/typings/Indexer.ts +++ b/frontend/src/typings/Indexer.ts @@ -1,18 +1,11 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; -interface Indexer extends ModelBase { +interface Indexer extends Provider { enableRss: boolean; enableAutomaticSearch: boolean; enableInteractiveSearch: boolean; protocol: string; priority: number; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Metadata.ts b/frontend/src/typings/Metadata.ts new file mode 100644 index 000000000..b7d0cfb71 --- /dev/null +++ b/frontend/src/typings/Metadata.ts @@ -0,0 +1,7 @@ +import Provider from './Provider'; + +interface Metadata extends Provider { + enable: boolean; +} + +export default Metadata; diff --git a/frontend/src/typings/Notification.ts b/frontend/src/typings/Notification.ts index 3aa3a2d48..12057015b 100644 --- a/frontend/src/typings/Notification.ts +++ b/frontend/src/typings/Notification.ts @@ -1,14 +1,7 @@ -import ModelBase from 'App/ModelBase'; -import Field from './Field'; +import Provider from './Provider'; -interface Notification extends ModelBase { +interface Notification extends Provider { enable: boolean; - name: string; - fields: Field[]; - implementationName: string; - implementation: string; - configContract: string; - infoLink: string; tags: number[]; } diff --git a/frontend/src/typings/Provider.ts b/frontend/src/typings/Provider.ts new file mode 100644 index 000000000..e9eabba0b --- /dev/null +++ b/frontend/src/typings/Provider.ts @@ -0,0 +1,20 @@ +import ModelBase from 'App/ModelBase'; +import { Kind } from 'Helpers/Props/kinds'; +import Field from './Field'; + +export interface ProviderMessage { + message: string; + type: Extract; +} + +interface Provider extends ModelBase { + name: string; + fields: Field[]; + implementationName: string; + implementation: string; + configContract: string; + infoLink: string; + message: ProviderMessage; +} + +export default Provider; diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index af0dd95e1..480c35623 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -33,7 +33,7 @@ export interface Pending { value: T; errors: Failure[]; warnings: Failure[]; - pending?: boolean; + pending: boolean; previousValue?: T; } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs index d994cef35..3386fa021 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,21 +5,21 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; -using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa { public class KometaMetadata : MetadataBase { + private readonly ILocalizationService _localizationService; private readonly Logger _logger; - private readonly IMapCoversToLocal _mediaCoverService; - public KometaMetadata(IMapCoversToLocal mediaCoverService, - Logger logger) + public KometaMetadata(ILocalizationService localizationService, Logger logger) { - _mediaCoverService = mediaCoverService; + _localizationService = localizationService; _logger = logger; } @@ -30,6 +29,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa public override string Name => "Kometa"; + public override ProviderMessage Message => new (_localizationService.GetLocalizedString("MetadataKometaDeprecated"), ProviderMessageType.Warning); + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) { if (metadataFile.Type == MetadataType.EpisodeImage) @@ -104,92 +105,17 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa public override List SeriesImages(Series series) { - if (!Settings.SeriesImages) - { - return new List(); - } - - return ProcessSeriesImages(series).ToList(); + return new List(); } public override List SeasonImages(Series series, Season season) { - if (!Settings.SeasonImages) - { - return new List(); - } - - return ProcessSeasonImages(series, season).ToList(); + return new List(); } public override List EpisodeImages(Series series, EpisodeFile episodeFile) { - if (!Settings.EpisodeImages) - { - return new List(); - } - - try - { - var firstEpisode = episodeFile.Episodes.Value.FirstOrDefault(); - - if (firstEpisode == null) - { - _logger.Debug("Episode file has no associated episodes, potentially a duplicate file"); - return new List(); - } - - var screenshot = firstEpisode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); - - if (screenshot == null) - { - _logger.Debug("Episode screenshot not available"); - return new List(); - } - - return new List - { - new ImageFileResult(GetEpisodeImageFilename(series, episodeFile), screenshot.RemoteUrl) - }; - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to process episode image for file: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); - - return new List(); - } - } - - private IEnumerable ProcessSeriesImages(Series series) - { - foreach (var image in series.Images) - { - if (image.CoverType == MediaCoverTypes.Poster) - { - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); - var destination = image.CoverType + Path.GetExtension(source); - - yield return new ImageFileResult(destination, source); - } - } - } - - private IEnumerable ProcessSeasonImages(Series series, Season season) - { - foreach (var image in season.Images) - { - if (image.CoverType == MediaCoverTypes.Poster) - { - var filename = string.Format("Season{0:00}.jpg", season.SeasonNumber); - - if (season.SeasonNumber == 0) - { - filename = "Season00.jpg"; - } - - yield return new ImageFileResult(filename, image.RemoteUrl); - } - } + return new List(); } private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs index 8b84954f6..1a2011f6b 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs @@ -15,19 +15,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa public KometaMetadataSettings() { - SeriesImages = true; - SeasonImages = true; - EpisodeImages = true; + Deprecated = true; } - [FieldDefinition(0, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Poster.jpg")] - public bool SeriesImages { get; set; } - - [FieldDefinition(1, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##.jpg")] - public bool SeasonImages { get; set; } - - [FieldDefinition(2, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "S##E##.jpg")] - public bool EpisodeImages { get; set; } + [FieldDefinition(0, Label = "MetadataKometaDeprecatedSetting", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, Hidden = HiddenType.Hidden)] + public bool Deprecated { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs new file mode 100644 index 000000000..48fa924d9 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/MetadataCheck.cs @@ -0,0 +1,34 @@ +using System.Linq; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Kometa; +using NzbDrone.Core.Localization; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ProviderUpdatedEvent))] + public class MetadataCheck : HealthCheckBase + { + private readonly IMetadataFactory _metadataFactory; + + public MetadataCheck(IMetadataFactory metadataFactory, ILocalizationService localizationService) + : base(localizationService) + { + _metadataFactory = metadataFactory; + } + + public override HealthCheck Check() + { + var enabled = _metadataFactory.Enabled(); + + if (enabled.Any(m => m.Definition.Implementation == nameof(KometaMetadata))) + { + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + $"{_localizationService.GetLocalizedString("MetadataKometaDeprecated")}"); + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5be713c1f..823e8356f 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1163,6 +1163,8 @@ "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", + "MetadataKometaDeprecated": "Kometa files will no longer be created, support will be removed completely in v5", + "MetadataKometaDeprecatedSetting": "Deprecated", "MetadataPlexSettingsEpisodeMappings": "Episode Mappings", "MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file", "MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File",