mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-19 21:46:43 -04:00
@@ -63,6 +63,16 @@ export interface AppSectionItemState<T> {
|
|||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionProviderState<T>
|
||||||
|
extends AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||||
|
import Metadata from 'typings/Metadata';
|
||||||
|
|
||||||
|
interface MetadataAppState extends AppSectionProviderState<Metadata> {}
|
||||||
|
|
||||||
|
export default MetadataAppState;
|
||||||
@@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig';
|
|||||||
import NamingExample from 'typings/Settings/NamingExample';
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
import MetadataAppState from './MetadataAppState';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
@@ -97,6 +98,7 @@ interface SettingsAppState {
|
|||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
metadata: MetadataAppState;
|
||||||
naming: NamingAppState;
|
naming: NamingAppState;
|
||||||
namingExamples: NamingExamplesAppState;
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ ProviderFieldFormGroup.propTypes = {
|
|||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
advanced: PropTypes.bool.isRequired,
|
advanced: PropTypes.bool.isRequired,
|
||||||
hidden: PropTypes.string,
|
hidden: PropTypes.string,
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
provider: PropTypes.string,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Modal
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditMetadataModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditMetadataModal;
|
|
||||||
@@ -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 (
|
||||||
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<EditMetadataModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMetadataModal;
|
||||||
@@ -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 (
|
|
||||||
<EditMetadataModal
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector);
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.message {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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 (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('EditMetadata', { metadataType: name.value })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form {...otherProps}>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Enable')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="enable"
|
|
||||||
helpText={translate('EnableMetadataHelpText')}
|
|
||||||
{...enable}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
fields.map((field) => {
|
|
||||||
return (
|
|
||||||
<ProviderFieldFormGroup
|
|
||||||
key={field.name}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
provider="metadata"
|
|
||||||
{...field}
|
|
||||||
isDisabled={!enable.value}
|
|
||||||
onChange={onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -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 (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('EditMetadata', { metadataType: name.value })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form {...otherSettings}>
|
||||||
|
{message ? (
|
||||||
|
<Alert className={styles.message} kind={message.value.type}>
|
||||||
|
{message.value.message}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Enable')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="enable"
|
||||||
|
helpText={translate('EnableMetadataHelpText')}
|
||||||
|
{...enable}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{fields.map((field) => {
|
||||||
|
return (
|
||||||
|
<ProviderFieldFormGroup
|
||||||
|
key={field.name}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
provider="metadata"
|
||||||
|
{...field}
|
||||||
|
isDisabled={!enable.value}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMetadataModalContent;
|
||||||
@@ -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 (
|
|
||||||
<EditMetadataModalContent
|
|
||||||
{...this.props}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onFieldChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
@@ -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 (
|
|
||||||
<Card
|
|
||||||
className={styles.metadata}
|
|
||||||
overlayContent={true}
|
|
||||||
onPress={this.onEditMetadataPress}
|
|
||||||
>
|
|
||||||
<div className={styles.name}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
enable ?
|
|
||||||
<Label kind={kinds.SUCCESS}>
|
|
||||||
{translate('Enabled')}
|
|
||||||
</Label> :
|
|
||||||
<Label
|
|
||||||
kind={kinds.DISABLED}
|
|
||||||
outline={true}
|
|
||||||
>
|
|
||||||
{translate('Disabled')}
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
enable && !!metadataFields.length &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.section}>
|
|
||||||
{translate('Metadata')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
metadataFields.map((field) => {
|
|
||||||
if (!field.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={field.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
enable && !!imageFields.length &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.section}>
|
|
||||||
{translate('Images')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
imageFields.map((field) => {
|
|
||||||
if (!field.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={field.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<EditMetadataModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={this.state.isEditMetadataModalOpen}
|
|
||||||
onModalClose={this.onEditMetadataModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
enable: PropTypes.bool.isRequired,
|
|
||||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadata;
|
|
||||||
@@ -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 (
|
||||||
|
<Card
|
||||||
|
className={styles.metadata}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={handleOpenPress}
|
||||||
|
>
|
||||||
|
<div className={styles.name}>{name}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{enable ? (
|
||||||
|
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
|
||||||
|
) : (
|
||||||
|
<Label kind={kinds.DISABLED} outline={true}>
|
||||||
|
{translate('Disabled')}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enable && metadataFields.length ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.section}>{translate('Metadata')}</div>
|
||||||
|
|
||||||
|
{metadataFields.map((field) => {
|
||||||
|
if (!field.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label key={field.label} kind={kinds.SUCCESS}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{enable && imageFields.length ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.section}>{translate('Images')}</div>
|
||||||
|
|
||||||
|
{imageFields.map((field) => {
|
||||||
|
if (!field.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label key={field.label} kind={kinds.SUCCESS}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<EditMetadataModal
|
||||||
|
advancedSettings={false}
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditMetadataModalOpen}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadata;
|
||||||
@@ -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 (
|
|
||||||
<FieldSet legend={translate('Metadata')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('MetadataLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.metadatas}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<Metadata
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadatas.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadatas;
|
|
||||||
@@ -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<MetadataType>(
|
||||||
|
'settings.metadata',
|
||||||
|
sortByProp('name')
|
||||||
|
),
|
||||||
|
(metadata: MetadataAppState) => metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metadatas() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, error, items, ...otherProps } = useSelector(
|
||||||
|
createMetadatasSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchMetadata());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Metadata')}>
|
||||||
|
<PageSectionContent
|
||||||
|
isFetching={isFetching}
|
||||||
|
errorMessage={translate('MetadataLoadError')}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.metadatas}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <Metadata key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadatas;
|
||||||
@@ -3,7 +3,7 @@ import PageContent from 'Components/Page/PageContent';
|
|||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MetadatasConnector from './Metadata/MetadatasConnector';
|
import Metadatas from './Metadata/Metadatas';
|
||||||
|
|
||||||
function MetadataSettings() {
|
function MetadataSettings() {
|
||||||
return (
|
return (
|
||||||
@@ -13,7 +13,7 @@ function MetadataSettings() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<MetadatasConnector />
|
<Metadatas />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function selectSettings<T extends ModelBaseSetting>(
|
|||||||
|
|
||||||
const setting: Pending<T> = {
|
const setting: Pending<T> = {
|
||||||
value: item[key],
|
value: item[key],
|
||||||
|
pending: false,
|
||||||
errors: getFailures(errors, key),
|
errors: getFailures(errors, key),
|
||||||
warnings: getFailures(warnings, key),
|
warnings: getFailures(warnings, key),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
import Field from './Field';
|
|
||||||
|
|
||||||
export type Protocol = 'torrent' | 'usenet' | 'unknown';
|
export type Protocol = 'torrent' | 'usenet' | 'unknown';
|
||||||
|
|
||||||
interface DownloadClient extends ModelBase {
|
interface DownloadClient extends Provider {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
protocol: Protocol;
|
protocol: Protocol;
|
||||||
priority: number;
|
priority: number;
|
||||||
removeCompletedDownloads: boolean;
|
removeCompletedDownloads: boolean;
|
||||||
removeFailedDownloads: boolean;
|
removeFailedDownloads: boolean;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface Field {
|
|||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
value: boolean | number | string | number[];
|
value: boolean | number | string | number[];
|
||||||
|
section: string;
|
||||||
|
hidden: 'hidden' | 'hiddenIfNotSet' | 'visible';
|
||||||
type: string;
|
type: string;
|
||||||
advanced: boolean;
|
advanced: boolean;
|
||||||
privacy: string;
|
privacy: string;
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
import Field from './Field';
|
|
||||||
|
|
||||||
interface ImportList extends ModelBase {
|
interface ImportList extends Provider {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
enableAutomaticAdd: boolean;
|
enableAutomaticAdd: boolean;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
rootFolderPath: string;
|
rootFolderPath: string;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
import Field from './Field';
|
|
||||||
|
|
||||||
interface Indexer extends ModelBase {
|
interface Indexer extends Provider {
|
||||||
enableRss: boolean;
|
enableRss: boolean;
|
||||||
enableAutomaticSearch: boolean;
|
enableAutomaticSearch: boolean;
|
||||||
enableInteractiveSearch: boolean;
|
enableInteractiveSearch: boolean;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Provider from './Provider';
|
||||||
|
|
||||||
|
interface Metadata extends Provider {
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadata;
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import Provider from './Provider';
|
||||||
import Field from './Field';
|
|
||||||
|
|
||||||
interface Notification extends ModelBase {
|
interface Notification extends Provider {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
name: string;
|
|
||||||
fields: Field[];
|
|
||||||
implementationName: string;
|
|
||||||
implementation: string;
|
|
||||||
configContract: string;
|
|
||||||
infoLink: string;
|
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<Kind, 'info' | 'error' | 'warning'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Provider extends ModelBase {
|
||||||
|
name: string;
|
||||||
|
fields: Field[];
|
||||||
|
implementationName: string;
|
||||||
|
implementation: string;
|
||||||
|
configContract: string;
|
||||||
|
infoLink: string;
|
||||||
|
message: ProviderMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Provider;
|
||||||
@@ -33,7 +33,7 @@ export interface Pending<T> {
|
|||||||
value: T;
|
value: T;
|
||||||
errors: Failure[];
|
errors: Failure[];
|
||||||
warnings: Failure[];
|
warnings: Failure[];
|
||||||
pending?: boolean;
|
pending: boolean;
|
||||||
previousValue?: T;
|
previousValue?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -6,21 +5,21 @@ using System.Text.RegularExpressions;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Extras.Metadata.Files;
|
using NzbDrone.Core.Extras.Metadata.Files;
|
||||||
using NzbDrone.Core.MediaCover;
|
using NzbDrone.Core.Localization;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
||||||
{
|
{
|
||||||
public class KometaMetadata : MetadataBase<KometaMetadataSettings>
|
public class KometaMetadata : MetadataBase<KometaMetadataSettings>
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly IMapCoversToLocal _mediaCoverService;
|
|
||||||
|
|
||||||
public KometaMetadata(IMapCoversToLocal mediaCoverService,
|
public KometaMetadata(ILocalizationService localizationService, Logger logger)
|
||||||
Logger logger)
|
|
||||||
{
|
{
|
||||||
_mediaCoverService = mediaCoverService;
|
_localizationService = localizationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +29,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
|||||||
|
|
||||||
public override string Name => "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)
|
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
|
||||||
{
|
{
|
||||||
if (metadataFile.Type == MetadataType.EpisodeImage)
|
if (metadataFile.Type == MetadataType.EpisodeImage)
|
||||||
@@ -104,92 +105,17 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
|||||||
|
|
||||||
public override List<ImageFileResult> SeriesImages(Series series)
|
public override List<ImageFileResult> SeriesImages(Series series)
|
||||||
{
|
{
|
||||||
if (!Settings.SeriesImages)
|
return new List<ImageFileResult>();
|
||||||
{
|
|
||||||
return new List<ImageFileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProcessSeriesImages(series).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<ImageFileResult> SeasonImages(Series series, Season season)
|
public override List<ImageFileResult> SeasonImages(Series series, Season season)
|
||||||
{
|
{
|
||||||
if (!Settings.SeasonImages)
|
return new List<ImageFileResult>();
|
||||||
{
|
|
||||||
return new List<ImageFileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProcessSeasonImages(series, season).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
|
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
|
||||||
{
|
{
|
||||||
if (!Settings.EpisodeImages)
|
return new List<ImageFileResult>();
|
||||||
{
|
|
||||||
return new List<ImageFileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ImageFileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var screenshot = firstEpisode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
|
||||||
|
|
||||||
if (screenshot == null)
|
|
||||||
{
|
|
||||||
_logger.Debug("Episode screenshot not available");
|
|
||||||
return new List<ImageFileResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new List<ImageFileResult>
|
|
||||||
{
|
|
||||||
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<ImageFileResult>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<ImageFileResult> 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<ImageFileResult> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile)
|
private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile)
|
||||||
|
|||||||
@@ -15,19 +15,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa
|
|||||||
|
|
||||||
public KometaMetadataSettings()
|
public KometaMetadataSettings()
|
||||||
{
|
{
|
||||||
SeriesImages = true;
|
Deprecated = true;
|
||||||
SeasonImages = true;
|
|
||||||
EpisodeImages = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[FieldDefinition(0, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Poster.jpg")]
|
[FieldDefinition(0, Label = "MetadataKometaDeprecatedSetting", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, Hidden = HiddenType.Hidden)]
|
||||||
public bool SeriesImages { get; set; }
|
public bool Deprecated { 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; }
|
|
||||||
|
|
||||||
public bool IsValid => true;
|
public bool IsValid => true;
|
||||||
|
|
||||||
|
|||||||
@@ -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<IMetadata>))]
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1163,6 +1163,8 @@
|
|||||||
"Message": "Message",
|
"Message": "Message",
|
||||||
"Metadata": "Metadata",
|
"Metadata": "Metadata",
|
||||||
"MetadataLoadError": "Unable to load 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",
|
"MetadataPlexSettingsEpisodeMappings": "Episode Mappings",
|
||||||
"MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file",
|
"MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file",
|
||||||
"MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File",
|
"MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File",
|
||||||
|
|||||||
Reference in New Issue
Block a user