1
0
Fork 0
mirror of https://github.com/Sonarr/Sonarr synced 2024-12-21 23:33:00 +00:00

New: Kometa metadata file creation disabled

Closes #7400
This commit is contained in:
Mark McDowall 2024-12-01 16:20:55 -08:00 committed by GitHub
parent fb9a5efe05
commit c62fc9d05b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 445 additions and 599 deletions

View file

@ -63,6 +63,16 @@ export interface AppSectionItemState<T> {
item: T;
}
export interface AppSectionProviderState<T>
extends AppSectionDeleteState,
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;

View file

@ -0,0 +1,6 @@
import { AppSectionProviderState } from 'App/State/AppSectionState';
import Metadata from 'typings/Metadata';
interface MetadataAppState extends AppSectionProviderState<Metadata> {}
export default MetadataAppState;

View file

@ -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<DownloadClient>,
@ -97,6 +98,7 @@ interface SettingsAppState {
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
notifications: NotificationAppState;

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -0,0 +1,5 @@
.message {
composes: alert from '~Components/Alert.css';
margin-bottom: 30px;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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() {
/>
<PageContentBody>
<MetadatasConnector />
<Metadatas />
</PageContentBody>
</PageContent>
);

View file

@ -100,6 +100,7 @@ function selectSettings<T extends ModelBaseSetting>(
const setting: Pending<T> = {
value: item[key],
pending: false,
errors: getFailures(errors, key),
warnings: getFailures(warnings, key),
};

View file

@ -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[];
}

View file

@ -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;

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -0,0 +1,7 @@
import Provider from './Provider';
interface Metadata extends Provider {
enable: boolean;
}
export default Metadata;

View file

@ -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[];
}

View file

@ -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;

View file

@ -33,7 +33,7 @@ export interface Pending<T> {
value: T;
errors: Failure[];
warnings: Failure[];
pending?: boolean;
pending: boolean;
previousValue?: T;
}

View file

@ -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<KometaMetadataSettings>
{
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<ImageFileResult> SeriesImages(Series series)
{
if (!Settings.SeriesImages)
{
return new List<ImageFileResult>();
}
return ProcessSeriesImages(series).ToList();
return new List<ImageFileResult>();
}
public override List<ImageFileResult> SeasonImages(Series series, Season season)
{
if (!Settings.SeasonImages)
{
return new List<ImageFileResult>();
}
return ProcessSeasonImages(series, season).ToList();
return new List<ImageFileResult>();
}
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
{
if (!Settings.EpisodeImages)
{
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);
}
}
return new List<ImageFileResult>();
}
private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile)

View file

@ -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;

View file

@ -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());
}
}
}

View file

@ -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",