From e52288bd67f52855de9c2b537d5715b80910f66a Mon Sep 17 00:00:00 2001 From: Qstick Date: Thu, 15 Oct 2020 10:38:30 -0400 Subject: [PATCH] New: Edit RlsGroup, Flags, and Edition for Movie Files (#5183) * New: Edit RlsGroup and Edition for Movie Files * fixup! remove console log * fixup! translation --- .../src/Components/Form/FormInputGroup.js | 4 + .../Form/IndexerFlagsSelectInputConnector.js | 70 ++++++ frontend/src/Components/Page/PageConnector.js | 15 +- frontend/src/Helpers/Props/inputTypes.js | 2 + frontend/src/MovieFile/Edit/FileEditModal.js | 37 +++ .../MovieFile/Edit/FileEditModalContent.js | 237 ++++++++++++++++++ .../Edit/FileEditModalContentConnector.js | 139 ++++++++++ .../MovieFile/Editor/MovieFileEditorRow.js | 75 ++---- .../Store/Actions/Settings/indexerFlags.js | 48 ++++ .../src/Store/Actions/movieFileActions.js | 29 ++- frontend/src/Store/Actions/settingsActions.js | 5 + src/NzbDrone.Core/Localization/Core/en.json | 1 + .../Indexers/IndexerFlagModule.cs | 25 ++ .../Indexers/IndexerFlagResource.cs | 13 + .../MovieFiles/MovieFileListResource.cs | 4 + .../MovieFiles/MovieFileModule.cs | 20 +- .../MovieFiles/MovieFileResource.cs | 16 +- yarn.lock | 5 - 18 files changed, 685 insertions(+), 60 deletions(-) create mode 100644 frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js create mode 100644 frontend/src/MovieFile/Edit/FileEditModal.js create mode 100644 frontend/src/MovieFile/Edit/FileEditModalContent.js create mode 100644 frontend/src/MovieFile/Edit/FileEditModalContentConnector.js create mode 100644 frontend/src/Store/Actions/Settings/indexerFlags.js create mode 100644 src/Radarr.Api.V3/Indexers/IndexerFlagModule.cs create mode 100644 src/Radarr.Api.V3/Indexers/IndexerFlagResource.cs diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 250a0eb88..269117513 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -10,6 +10,7 @@ import CheckInput from './CheckInput'; import DeviceInputConnector from './DeviceInputConnector'; import EnhancedSelectInput from './EnhancedSelectInput'; import FormInputHelpText from './FormInputHelpText'; +import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import MovieMonitoredSelectInput from './MovieMonitoredSelectInput'; import NumberInput from './NumberInput'; @@ -66,6 +67,9 @@ function getComponent(type) { case inputTypes.ROOT_FOLDER_SELECT: return RootFolderSelectInputConnector; + case inputTypes.INDEXER_FLAGS_SELECT: + return IndexerFlagsSelectInputConnector; + case inputTypes.SELECT: return EnhancedSelectInput; diff --git a/frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js b/frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js new file mode 100644 index 000000000..588488c29 --- /dev/null +++ b/frontend/src/Components/Form/IndexerFlagsSelectInputConnector.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import EnhancedSelectInput from './EnhancedSelectInput'; + +function createMapStateToProps() { + return createSelector( + (state, { indexerFlags }) => indexerFlags, + (state) => state.settings.indexerFlags, + (selectedFlags, indexerFlags) => { + const value = []; + + indexerFlags.items.forEach((item) => { + // eslint-disable-next-line no-bitwise + if ((selectedFlags & item.id) === item.id) { + value.push(item.id); + } + }); + + const values = indexerFlags.items.map(({ id, name }) => { + return { + key: id, + value: name + }; + }); + + return { + value, + values + }; + } + ); +} + +class IndexerFlagsSelectInputConnector extends Component { + + onChange = ({ name, value }) => { + let indexerFlags = 0; + + value.forEach((flagId) => { + indexerFlags += flagId; + }); + + this.props.onChange({ name, value: indexerFlags }); + } + + // + // Render + + render() { + + return ( + + ); + } +} + +IndexerFlagsSelectInputConnector.propTypes = { + name: PropTypes.string.isRequired, + indexerFlags: PropTypes.number.isRequired, + value: PropTypes.arrayOf(PropTypes.number).isRequired, + values: PropTypes.arrayOf(PropTypes.object).isRequired, + onChange: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps)(IndexerFlagsSelectInputConnector); diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 9dd262127..b135820b8 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchMovies } from 'Store/Actions/movieActions'; -import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; +import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchTags } from 'Store/Actions/tagActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -48,6 +48,7 @@ const selectIsPopulated = createSelector( (state) => state.settings.ui.isPopulated, (state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.languages.isPopulated, + (state) => state.settings.indexerFlags.isPopulated, (state) => state.settings.importLists.isPopulated, (state) => state.system.status.isPopulated, ( @@ -56,6 +57,7 @@ const selectIsPopulated = createSelector( uiSettingsIsPopulated, qualityProfilesIsPopulated, languagesIsPopulated, + indexerFlagsIsPopulated, importListsIsPopulated, systemStatusIsPopulated ) => { @@ -65,6 +67,7 @@ const selectIsPopulated = createSelector( uiSettingsIsPopulated && qualityProfilesIsPopulated && languagesIsPopulated && + indexerFlagsIsPopulated && importListsIsPopulated && systemStatusIsPopulated ); @@ -77,6 +80,7 @@ const selectErrors = createSelector( (state) => state.settings.ui.error, (state) => state.settings.qualityProfiles.error, (state) => state.settings.languages.error, + (state) => state.settings.indexerFlags.error, (state) => state.settings.importLists.error, (state) => state.system.status.error, ( @@ -85,6 +89,7 @@ const selectErrors = createSelector( uiSettingsError, qualityProfilesError, languagesError, + indexerFlagsError, importListsError, systemStatusError ) => { @@ -94,6 +99,7 @@ const selectErrors = createSelector( uiSettingsError || qualityProfilesError || languagesError || + indexerFlagsError || importListsError || systemStatusError ); @@ -105,6 +111,7 @@ const selectErrors = createSelector( uiSettingsError, qualityProfilesError, languagesError, + indexerFlagsError, importListsError, systemStatusError }; @@ -153,6 +160,9 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchLanguages() { dispatch(fetchLanguages()); }, + dispatchFetchIndexerFlags() { + dispatch(fetchIndexerFlags()); + }, dispatchFetchImportLists() { dispatch(fetchImportLists()); }, @@ -191,6 +201,7 @@ class PageConnector extends Component { this.props.dispatchFetchTags(); this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchLanguages(); + this.props.dispatchFetchIndexerFlags(); this.props.dispatchFetchImportLists(); this.props.dispatchFetchUISettings(); this.props.dispatchFetchStatus(); @@ -215,6 +226,7 @@ class PageConnector extends Component { dispatchFetchTags, dispatchFetchQualityProfiles, dispatchFetchLanguages, + dispatchFetchIndexerFlags, dispatchFetchImportLists, dispatchFetchUISettings, dispatchFetchStatus, @@ -254,6 +266,7 @@ PageConnector.propTypes = { dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchLanguages: PropTypes.func.isRequired, + dispatchFetchIndexerFlags: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index b8368ce93..46f4c1b84 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -10,6 +10,7 @@ export const PASSWORD = 'password'; export const PATH = 'path'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; +export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const SELECT = 'select'; export const TAG = 'tag'; export const TEXT = 'text'; @@ -30,6 +31,7 @@ export const all = [ PATH, QUALITY_PROFILE_SELECT, ROOT_FOLDER_SELECT, + INDEXER_FLAGS_SELECT, SELECT, TAG, TEXT, diff --git a/frontend/src/MovieFile/Edit/FileEditModal.js b/frontend/src/MovieFile/Edit/FileEditModal.js new file mode 100644 index 000000000..f12665726 --- /dev/null +++ b/frontend/src/MovieFile/Edit/FileEditModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import FileEditModalContentConnector from './FileEditModalContentConnector'; + +class FileEditModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +FileEditModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default FileEditModal; diff --git a/frontend/src/MovieFile/Edit/FileEditModalContent.js b/frontend/src/MovieFile/Edit/FileEditModalContent.js new file mode 100644 index 000000000..02bb6ba84 --- /dev/null +++ b/frontend/src/MovieFile/Edit/FileEditModalContent.js @@ -0,0 +1,237 @@ +import PropTypes from 'prop-types'; +import React, { Component } 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 Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +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, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; + +class FileEditModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + qualityId, + languageIds, + indexerFlags, + proper, + real, + edition, + releaseGroup + } = props; + + this.state = { + qualityId, + languageIds, + indexerFlags, + proper, + real, + edition, + releaseGroup + }; + } + + // + // Listeners + + onQualityChange = ({ value }) => { + this.setState({ qualityId: parseInt(value) }); + } + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + } + + onSaveInputs = () => { + this.props.onSaveInputs(this.state); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + qualities, + languages, + relativePath, + onModalClose + } = this.props; + + const { + qualityId, + languageIds, + indexerFlags, + proper, + real, + edition, + releaseGroup + } = this.state; + + const qualityOptions = qualities.map(({ id, name }) => { + return { + key: id, + value: name + }; + }); + + const languageOptions = languages.map(({ id, name }) => { + return { + key: id, + value: name + }; + }); + + return ( + + + {translate('EditMovieFile')} - {relativePath} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
+ {translate('UnableToLoadQualities')} +
+ } + + { + isPopulated && !error && +
+ + {translate('Quality')} + + + + + + {translate('Proper')} + + + + + + {translate('Real')} + + + + + + {translate('Languages')} + + + + + + {translate('IndexerFlags')} + + + + + + {translate('Edition')} + + + + + + {translate('ReleaseGroup')} + + + +
+ } +
+ + + + + + +
+ ); + } +} + +FileEditModalContent.propTypes = { + qualityId: PropTypes.number.isRequired, + proper: PropTypes.bool.isRequired, + real: PropTypes.bool.isRequired, + relativePath: PropTypes.string.isRequired, + edition: PropTypes.string.isRequired, + releaseGroup: PropTypes.string.isRequired, + languageIds: PropTypes.arrayOf(PropTypes.number).isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + indexerFlags: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + onSaveInputs: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default FileEditModalContent; diff --git a/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js b/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js new file mode 100644 index 000000000..942e4a22b --- /dev/null +++ b/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { updateMovieFiles } from 'Store/Actions/movieFileActions'; +import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector'; +import getQualities from 'Utilities/Quality/getQualities'; +import FileEditModalContent from './FileEditModalContent'; + +function createMapStateToProps() { + return createSelector( + createMovieFileSelector(), + (state) => state.settings.qualityProfiles, + (state) => state.settings.languages, + (movieFile, qualityProfiles, languages) => { + + const filterItems = ['Any', 'Original']; + const filteredLanguages = languages.items.filter((lang) => !filterItems.includes(lang.name)); + + const quality = movieFile.quality; + + return { + isFetching: qualityProfiles.isSchemaFetching || languages.isFetching, + isPopulated: qualityProfiles.isSchemaPopulated && languages.isPopulated, + error: qualityProfiles.error || languages.error, + qualityId: quality ? quality.quality.id : 0, + real: quality ? quality.revision.real > 0 : false, + proper: quality ? quality.revision.version > 1 : false, + qualities: getQualities(qualityProfiles.schema.items), + languageIds: movieFile.languages ? movieFile.languages.map((l) => l.id) : [], + languages: filteredLanguages, + indexerFlags: movieFile.indexerFlags, + edition: movieFile.edition, + releaseGroup: movieFile.releaseGroup, + relativePath: movieFile.relativePath + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchQualityProfileSchema: fetchQualityProfileSchema, + dispatchUpdateMovieFiles: updateMovieFiles +}; + +class FileEditModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.dispatchFetchQualityProfileSchema(); + } + } + + // + // Listeners + + onSaveInputs = ( payload ) => { + const { + qualityId, + real, + proper, + languageIds, + edition, + releaseGroup, + indexerFlags + } = payload; + + const quality = this.props.qualities.find((item) => item.id === qualityId); + + const languages = []; + + languageIds.forEach((languageId) => { + const language = this.props.languages.find((item) => item.id === parseInt(languageId)); + + if (language !== undefined) { + languages.push(language); + } + }); + + const revision = { + version: proper ? 2 : 1, + real: real ? 1 : 0 + }; + + const movieFileIds = [this.props.movieFileId]; + + this.props.dispatchUpdateMovieFiles({ + movieFileIds, + languages, + indexerFlags, + edition, + releaseGroup, + quality: { + quality, + revision + } + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +FileEditModalContentConnector.propTypes = { + movieFileId: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + languageIds: PropTypes.arrayOf(PropTypes.number).isRequired, + indexerFlags: PropTypes.number.isRequired, + qualityId: PropTypes.number.isRequired, + real: PropTypes.bool.isRequired, + edition: PropTypes.string.isRequired, + releaseGroup: PropTypes.string.isRequired, + relativePath: PropTypes.string.isRequired, + proper: PropTypes.bool.isRequired, + dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, + dispatchUpdateMovieFiles: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FileEditModalContentConnector); diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorRow.js b/frontend/src/MovieFile/Editor/MovieFileEditorRow.js index 760e19d5f..80f171789 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorRow.js +++ b/frontend/src/MovieFile/Editor/MovieFileEditorRow.js @@ -3,16 +3,14 @@ import React, { Component } from 'react'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; import TableRow from 'Components/Table/TableRow'; import { icons, kinds } from 'Helpers/Props'; import MovieFormats from 'Movie/MovieFormats'; import MovieLanguage from 'Movie/MovieLanguage'; import MovieQuality from 'Movie/MovieQuality'; -import SelectLanguageModal from 'MovieFile/Language/SelectLanguageModal'; +import FileEditModal from 'MovieFile/Edit/FileEditModal'; import MediaInfoConnector from 'MovieFile/MediaInfoConnector'; import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes'; -import SelectQualityModal from 'MovieFile/Quality/SelectQualityModal'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; import FileDetailsModal from '../FileDetailsModal'; @@ -28,32 +26,15 @@ class MovieFileEditorRow extends Component { super(props, context); this.state = { - isSelectQualityModalOpen: false, - isSelectLanguageModalOpen: false, isConfirmDeleteModalOpen: false, - isFileDetailsModalOpen: false + isFileDetailsModalOpen: false, + isFileEditModalOpen: false }; } // // Listeners - onSelectQualityPress = () => { - this.setState({ isSelectQualityModalOpen: true }); - } - - onSelectLanguagePress = () => { - this.setState({ isSelectLanguageModalOpen: true }); - } - - onSelectQualityModalClose = () => { - this.setState({ isSelectQualityModalOpen: false }); - } - - onSelectLanguageModalClose = () => { - this.setState({ isSelectLanguageModalOpen: false }); - } - onDeletePress = () => { this.setState({ isConfirmDeleteModalOpen: true }); } @@ -76,6 +57,14 @@ class MovieFileEditorRow extends Component { this.setState({ isFileDetailsModalOpen: false }); } + onFileEditPress = () => { + this.setState({ isFileEditModalOpen: true }); + } + + onFileEditModalClose = () => { + this.setState({ isFileEditModalOpen: false }); + } + // // Render @@ -92,9 +81,8 @@ class MovieFileEditorRow extends Component { } = this.props; const { - isSelectQualityModalOpen, - isSelectLanguageModalOpen, isFileDetailsModalOpen, + isFileEditModalOpen, isConfirmDeleteModalOpen } = this.state; @@ -132,10 +120,8 @@ class MovieFileEditorRow extends Component { {formatBytes(size)} - { showLanguagePlaceholder && @@ -149,12 +135,10 @@ class MovieFileEditorRow extends Component { languages={languages} /> } - + - { showQualityPlaceholder && @@ -169,7 +153,7 @@ class MovieFileEditorRow extends Component { isCutoffNotMet={qualityCutoffNotMet} /> } - + + + + + - - 1 : false} - real={quality ? quality.revision.real > 0 : false} - onModalClose={this.onSelectQualityModalClose} - /> - - l.id) : []} - onModalClose={this.onSelectLanguageModalClose} - /> ); } diff --git a/frontend/src/Store/Actions/Settings/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js new file mode 100644 index 000000000..a53fe1c61 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/indexerFlags.js @@ -0,0 +1,48 @@ +import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; +import { createThunk } from 'Store/thunks'; + +// +// Variables + +const section = 'settings.indexerFlags'; + +// +// Actions Types + +export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags'; + +// +// Action Creators + +export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag') + }, + + // + // Reducers + + reducers: { + + } + +}; diff --git a/frontend/src/Store/Actions/movieFileActions.js b/frontend/src/Store/Actions/movieFileActions.js index 2a7203f57..9e7e99ca6 100644 --- a/frontend/src/Store/Actions/movieFileActions.js +++ b/frontend/src/Store/Actions/movieFileActions.js @@ -141,7 +141,10 @@ export const actionHandlers = handleThunks({ const { movieFileIds, languages, - quality + indexerFlags, + quality, + edition, + releaseGroup } = payload; dispatch(set({ section, isSaving: true })); @@ -154,10 +157,22 @@ export const actionHandlers = handleThunks({ data.languages = languages; } + if (indexerFlags !== undefined) { + data.indexerFlags = indexerFlags; + } + if (quality) { data.quality = quality; } + if (releaseGroup) { + data.releaseGroup = releaseGroup; + } + + if (edition) { + data.edition = edition; + } + const promise = createAjaxRequest({ url: '/movieFile/editor', method: 'PUT', @@ -174,10 +189,22 @@ export const actionHandlers = handleThunks({ props.languages = languages; } + if (indexerFlags) { + props.indexerFlags = indexerFlags; + } + if (quality) { props.quality = quality; } + if (edition) { + props.edition = edition; + } + + if (releaseGroup) { + props.releaseGroup = releaseGroup; + } + return updateItem({ section, id, ...props }); }), diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index e15766321..f14548f9a 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -10,6 +10,7 @@ import general from './Settings/general'; import importExclusions from './Settings/importExclusions'; import importListOptions from './Settings/importListOptions'; import importLists from './Settings/importLists'; +import indexerFlags from './Settings/indexerFlags'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languages from './Settings/languages'; @@ -31,6 +32,7 @@ export * from './Settings/delayProfiles'; export * from './Settings/downloadClients'; export * from './Settings/downloadClientOptions'; export * from './Settings/general'; +export * from './Settings/indexerFlags'; export * from './Settings/indexerOptions'; export * from './Settings/indexers'; export * from './Settings/languages'; @@ -66,6 +68,7 @@ export const defaultState = { downloadClients: downloadClients.defaultState, downloadClientOptions: downloadClientOptions.defaultState, general: general.defaultState, + indexerFlags: indexerFlags.defaultState, indexerOptions: indexerOptions.defaultState, indexers: indexers.defaultState, languages: languages.defaultState, @@ -109,6 +112,7 @@ export const actionHandlers = handleThunks({ ...downloadClients.actionHandlers, ...downloadClientOptions.actionHandlers, ...general.actionHandlers, + ...indexerFlags.actionHandlers, ...indexerOptions.actionHandlers, ...indexers.actionHandlers, ...languages.actionHandlers, @@ -143,6 +147,7 @@ export const reducers = createHandleActions({ ...downloadClients.reducers, ...downloadClientOptions.reducers, ...general.reducers, + ...indexerFlags.reducers, ...indexerOptions.reducers, ...indexers.reducers, ...languages.reducers, diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index c85bbca12..9a159f742 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -205,6 +205,7 @@ "EditIndexer": "Edit Indexer", "Edition": "Edition", "EditMovie": "Edit Movie", + "EditMovieFile": "Edit Movie File", "EditPerson": "Edit Person", "EditRemotePathMapping": "Edit Remote Path Mapping", "EditRestriction": "Edit Restriction", diff --git a/src/Radarr.Api.V3/Indexers/IndexerFlagModule.cs b/src/Radarr.Api.V3/Indexers/IndexerFlagModule.cs new file mode 100644 index 000000000..280ff0240 --- /dev/null +++ b/src/Radarr.Api.V3/Indexers/IndexerFlagModule.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Parser.Model; +using Radarr.Http; + +namespace Radarr.Api.V3.Indexers +{ + public class IndexerFlagModule : RadarrRestModule + { + public IndexerFlagModule() + { + GetResourceAll = GetAll; + } + + private List GetAll() + { + return Enum.GetValues(typeof(IndexerFlags)).Cast().Select(f => new IndexerFlagResource + { + Id = (int)f, + Name = f.ToString() + }).ToList(); + } + } +} diff --git a/src/Radarr.Api.V3/Indexers/IndexerFlagResource.cs b/src/Radarr.Api.V3/Indexers/IndexerFlagResource.cs new file mode 100644 index 000000000..b12aea522 --- /dev/null +++ b/src/Radarr.Api.V3/Indexers/IndexerFlagResource.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using Radarr.Http.REST; + +namespace Radarr.Api.V3.Indexers +{ + public class IndexerFlagResource : RestResource + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public new int Id { get; set; } + public string Name { get; set; } + public string NameLower => Name.ToLowerInvariant(); + } +} diff --git a/src/Radarr.Api.V3/MovieFiles/MovieFileListResource.cs b/src/Radarr.Api.V3/MovieFiles/MovieFileListResource.cs index 1016ed453..a8024df32 100644 --- a/src/Radarr.Api.V3/MovieFiles/MovieFileListResource.cs +++ b/src/Radarr.Api.V3/MovieFiles/MovieFileListResource.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; namespace Radarr.Api.V3.MovieFiles @@ -9,5 +10,8 @@ namespace Radarr.Api.V3.MovieFiles public List MovieFileIds { get; set; } public List Languages { get; set; } public QualityModel Quality { get; set; } + public string Edition { get; set; } + public string ReleaseGroup { get; set; } + public int? IndexerFlags { get; set; } } } diff --git a/src/Radarr.Api.V3/MovieFiles/MovieFileModule.cs b/src/Radarr.Api.V3/MovieFiles/MovieFileModule.cs index 4acd740ab..4d57905f7 100644 --- a/src/Radarr.Api.V3/MovieFiles/MovieFileModule.cs +++ b/src/Radarr.Api.V3/MovieFiles/MovieFileModule.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies; +using NzbDrone.Core.Parser.Model; using NzbDrone.SignalR; using Radarr.Api.V3.CustomFormats; using Radarr.Http; @@ -110,9 +111,11 @@ namespace Radarr.Api.V3.MovieFiles private void SetMovieFile(MovieFileResource movieFileResource) { var movieFile = _mediaFileService.GetMovie(movieFileResource.Id); - movieFile.IndexerFlags = movieFileResource.IndexerFlags; + movieFile.IndexerFlags = (IndexerFlags)movieFileResource.IndexerFlags; movieFile.Quality = movieFileResource.Quality; movieFile.Languages = movieFileResource.Languages; + movieFile.Edition = movieFileResource.Edition; + movieFile.ReleaseGroup = movieFileResource.ReleaseGroup; _mediaFileService.Update(movieFile); } @@ -133,6 +136,21 @@ namespace Radarr.Api.V3.MovieFiles // Don't allow user to set movieFile with 'Any' or 'Original' language movieFile.Languages = resource.Languages.Where(l => l != Language.Any || l != Language.Original || l != null).ToList(); } + + if (resource.IndexerFlags != null) + { + movieFile.IndexerFlags = (IndexerFlags)resource.IndexerFlags.Value; + } + + if (resource.Edition != null) + { + movieFile.Edition = resource.Edition; + } + + if (resource.ReleaseGroup != null) + { + movieFile.ReleaseGroup = resource.ReleaseGroup; + } } _mediaFileService.Update(movieFiles); diff --git a/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs b/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs index e65e19feb..54f173a7a 100644 --- a/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs +++ b/src/Radarr.Api.V3/MovieFiles/MovieFileResource.cs @@ -19,13 +19,15 @@ namespace Radarr.Api.V3.MovieFiles public long Size { get; set; } public DateTime DateAdded { get; set; } public string SceneName { get; set; } - public IndexerFlags IndexerFlags { get; set; } + public int IndexerFlags { get; set; } public QualityModel Quality { get; set; } public List CustomFormats { get; set; } public MediaInfoResource MediaInfo { get; set; } public string OriginalFilePath { get; set; } public bool QualityCutoffNotMet { get; set; } public List Languages { get; set; } + public string ReleaseGroup { get; set; } + public string Edition { get; set; } } public static class MovieFileResourceMapper @@ -48,9 +50,11 @@ namespace Radarr.Api.V3.MovieFiles Size = model.Size, DateAdded = model.DateAdded, SceneName = model.SceneName, - IndexerFlags = model.IndexerFlags, + IndexerFlags = (int)model.IndexerFlags, Quality = model.Quality, Languages = model.Languages, + ReleaseGroup = model.ReleaseGroup, + Edition = model.Edition, MediaInfo = model.MediaInfo.ToResource(model.SceneName), OriginalFilePath = model.OriginalFilePath }; @@ -73,9 +77,11 @@ namespace Radarr.Api.V3.MovieFiles Size = model.Size, DateAdded = model.DateAdded, SceneName = model.SceneName, - IndexerFlags = model.IndexerFlags, + IndexerFlags = (int)model.IndexerFlags, Quality = model.Quality, Languages = model.Languages, + Edition = model.Edition, + ReleaseGroup = model.ReleaseGroup, MediaInfo = model.MediaInfo.ToResource(model.SceneName), OriginalFilePath = model.OriginalFilePath }; @@ -98,9 +104,11 @@ namespace Radarr.Api.V3.MovieFiles Size = model.Size, DateAdded = model.DateAdded, SceneName = model.SceneName, - IndexerFlags = model.IndexerFlags, + IndexerFlags = (int)model.IndexerFlags, Quality = model.Quality, Languages = model.Languages, + Edition = model.Edition, + ReleaseGroup = model.ReleaseGroup, MediaInfo = model.MediaInfo.ToResource(model.SceneName), QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(movie.Profile, model.Quality), OriginalFilePath = model.OriginalFilePath diff --git a/yarn.lock b/yarn.lock index e875bba7c..f723282e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1101,11 +1101,6 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@popperjs/core@2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.2.1.tgz#d7d1d7fbdc1f2aa24e62f4ef4b001be7727340c5" - integrity sha512-BChdj3idQiLi+7vPhE6gEDiPzpozvSrUqbSMoSTlRbOQkU0p6u4si0UBydegTyphsYSZC2AUHGYYICP0gqmEVg== - "@react-dnd/asap@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"