diff --git a/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js b/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js index 902f5801d..e3d2a493f 100644 --- a/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js +++ b/frontend/src/MovieFile/Edit/FileEditModalContentConnector.js @@ -86,18 +86,18 @@ class FileEditModalContentConnector extends Component { real: real ? 1 : 0 }; - const movieFileIds = [this.props.movieFileId]; - this.props.dispatchUpdateMovieFiles({ - movieFileIds, - languages, - indexerFlags, - edition, - releaseGroup, - quality: { - quality, - revision - } + files: [{ + id: this.props.movieFileId, + languages, + indexerFlags, + edition, + releaseGroup, + quality: { + quality, + revision + } + }] }); this.props.onModalClose(true); diff --git a/frontend/src/MovieFile/Language/SelectLanguageModal.js b/frontend/src/MovieFile/Language/SelectLanguageModal.js deleted file mode 100644 index 938d26a6d..000000000 --- a/frontend/src/MovieFile/Language/SelectLanguageModal.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import SelectLanguageModalContentConnector from './SelectLanguageModalContentConnector'; - -class SelectLanguageModal extends Component { - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - - - - ); - } -} - -SelectLanguageModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectLanguageModal; diff --git a/frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js b/frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js deleted file mode 100644 index a9d3094eb..000000000 --- a/frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js +++ /dev/null @@ -1,97 +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 SelectLanguageModalContent from 'InteractiveImport/Language/SelectLanguageModalContent'; -import { updateMovieFiles } from 'Store/Actions/movieFileActions'; -import { fetchLanguages } from 'Store/Actions/settingsActions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.languages, - (languages) => { - const { - isFetching, - isPopulated, - error, - items - } = languages; - - const filterItems = ['Any', 'Original']; - const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name)); - - return { - isFetching, - isPopulated, - error, - items: filteredLanguages - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchLanguages: fetchLanguages, - dispatchupdateMovieFiles: updateMovieFiles -}; - -class SelectLanguageModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount = () => { - if (!this.props.isPopulated) { - this.props.dispatchFetchLanguages(); - } - }; - - // - // Listeners - - onLanguageSelect = ({ languageIds }) => { - const languages = []; - - languageIds.forEach((languageId) => { - const language = _.find(this.props.items, - (item) => item.id === parseInt(languageId)); - - if (language !== undefined) { - languages.push(language); - } - }); - - this.props.dispatchupdateMovieFiles({ - movieFileIds: this.props.ids, - languages - }); - - this.props.onModalClose(true); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -SelectLanguageModalContentConnector.propTypes = { - ids: PropTypes.arrayOf(PropTypes.number).isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - dispatchFetchLanguages: PropTypes.func.isRequired, - dispatchupdateMovieFiles: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector); diff --git a/frontend/src/MovieFile/Quality/SelectQualityModal.js b/frontend/src/MovieFile/Quality/SelectQualityModal.js deleted file mode 100644 index d3e31d2dd..000000000 --- a/frontend/src/MovieFile/Quality/SelectQualityModal.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Modal from 'Components/Modal/Modal'; -import SelectQualityModalContentConnector from './SelectQualityModalContentConnector'; - -class SelectQualityModal extends Component { - - // - // Render - - render() { - const { - isOpen, - onModalClose, - ...otherProps - } = this.props; - - return ( - - - - ); - } -} - -SelectQualityModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectQualityModal; diff --git a/frontend/src/MovieFile/Quality/SelectQualityModalContent.js b/frontend/src/MovieFile/Quality/SelectQualityModalContent.js deleted file mode 100644 index 268fa6e1a..000000000 --- a/frontend/src/MovieFile/Quality/SelectQualityModalContent.js +++ /dev/null @@ -1,170 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -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 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 SelectQualityModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - const { - qualityId, - proper, - real - } = props; - - this.state = { - qualityId, - proper, - real - }; - } - - // - // Listeners - - onQualityChange = ({ value }) => { - this.setState({ qualityId: parseInt(value) }); - }; - - onProperChange = ({ value }) => { - this.setState({ proper: value }); - }; - - onRealChange = ({ value }) => { - this.setState({ real: value }); - }; - - onQualitySelect = () => { - this.props.onQualitySelect(this.state); - }; - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - items, - onModalClose - } = this.props; - - const { - qualityId, - proper, - real - } = this.state; - - const qualityOptions = items.map(({ id, name }) => { - return { - key: id, - value: name - }; - }); - - return ( - - - {translate('ManualImportSelectQuality')} - - - - { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('QualitiesLoadError')} - - } - - { - isPopulated && !error && -
- - {translate('Quality')} - - - - - - {translate('Proper')} - - - - - - {translate('Real')} - - - -
- } -
- - - - - - -
- ); - } -} - -SelectQualityModalContent.propTypes = { - qualityId: PropTypes.number.isRequired, - proper: PropTypes.bool.isRequired, - real: PropTypes.bool.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onQualitySelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SelectQualityModalContent; diff --git a/frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js b/frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js deleted file mode 100644 index 70fb1733f..000000000 --- a/frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js +++ /dev/null @@ -1,97 +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 { updateMovieFiles } from 'Store/Actions/movieFileActions'; -import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; -import getQualities from 'Utilities/Quality/getQualities'; -import SelectQualityModalContent from './SelectQualityModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.qualityProfiles, - (qualityProfiles) => { - const { - isSchemaFetching: isFetching, - isSchemaPopulated: isPopulated, - schemaError: error, - schema - } = qualityProfiles; - - return { - isFetching, - isPopulated, - error, - items: getQualities(schema.items) - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchQualityProfileSchema: fetchQualityProfileSchema, - dispatchupdateMovieFiles: updateMovieFiles -}; - -class SelectQualityModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount = () => { - if (!this.props.isPopulated) { - this.props.dispatchFetchQualityProfileSchema(); - } - }; - - // - // Listeners - - onQualitySelect = ({ qualityId, proper, real }) => { - const quality = _.find(this.props.items, - (item) => item.id === qualityId); - - const revision = { - version: proper ? 2 : 1, - real: real ? 1 : 0 - }; - - const movieFileIds = this.props.ids; - - this.props.dispatchupdateMovieFiles({ - movieFileIds, - quality: { - quality, - revision - } - }); - - this.props.onModalClose(true); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -SelectQualityModalContentConnector.propTypes = { - ids: PropTypes.arrayOf(PropTypes.number).isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, - dispatchupdateMovieFiles: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector); diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 264f7f089..9539e659b 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -29,6 +29,7 @@ export const defaultState = { isReprocessing: false, error: null, items: [], + originalItems: [], sortKey: 'relativePath', sortDirection: sortDirections.ASCENDING, favoriteFolders: [], @@ -119,7 +120,8 @@ export const actionHandlers = handleThunks({ section, isFetching: false, isPopulated: true, - error: null + error: null, + originalItems: data }) ])); }); @@ -228,13 +230,13 @@ export const reducers = createHandleActions({ }, [UPDATE_INTERACTIVE_IMPORT_ITEMS]: (state, { payload }) => { - const ids = payload.ids; + const { ids, ...otherPayload } = payload; const newState = Object.assign({}, state); const items = [...newState.items]; ids.forEach((id) => { const index = items.findIndex((item) => item.id === id); - const item = Object.assign({}, items[index], payload); + const item = Object.assign({}, items[index], otherPayload); items.splice(index, 1, item); }); diff --git a/frontend/src/Store/Actions/movieFileActions.js b/frontend/src/Store/Actions/movieFileActions.js index 86047a47f..3567da0d9 100644 --- a/frontend/src/Store/Actions/movieFileActions.js +++ b/frontend/src/Store/Actions/movieFileActions.js @@ -248,44 +248,14 @@ export const actionHandlers = handleThunks({ }, [UPDATE_MOVIE_FILES]: function(getState, payload, dispatch) { - - const { - movieFileIds, - languages, - indexerFlags, - quality, - edition, - releaseGroup - } = payload; + const { files } = payload; dispatch(set({ section, isSaving: true })); - const requestData = { - movieFileIds - }; - - if (languages) { - requestData.languages = languages; - } - - if (indexerFlags !== undefined) { - requestData.indexerFlags = indexerFlags; - } - - if (quality) { - requestData.quality = quality; - } - - if (releaseGroup !== undefined) { - requestData.releaseGroup = releaseGroup; - } - - if (edition !== undefined) { - requestData.edition = edition; - } + const requestData = files; const promise = createAjaxRequest({ - url: '/movieFile/editor', + url: '/movieFile/bulk', method: 'PUT', dataType: 'json', data: JSON.stringify(requestData) @@ -293,36 +263,25 @@ export const actionHandlers = handleThunks({ promise.done((data) => { dispatch(batchActions([ - ...movieFileIds.map((id) => { - const movieFile = data.find((file) => file.id === id); + ...files.map((file) => { + const id = file.id; + const props = {}; + const movieFile = data.find((f) => f.id === id); - const props = { - customFormats: movieFile.customFormats, - customFormatScore: movieFile.customFormatScore, - qualityCutoffNotMet: movieFile.qualityCutoffNotMet - }; + props.qualityCutoffNotMet = movieFile.qualityCutoffNotMet; + props.customFormats = movieFile.customFormats; + props.customFormatScore = movieFile.customFormatScore; + props.edition = movieFile.edition; + props.languages = file.languages; + props.quality = file.quality; + props.releaseGroup = file.releaseGroup; + props.indexerFlags = file.indexerFlags; - if (languages) { - props.languages = languages; - } - - if (indexerFlags !== undefined) { - props.indexerFlags = indexerFlags; - } - - if (quality) { - props.quality = quality; - } - - if (edition !== undefined) { - props.edition = edition; - } - - if (releaseGroup !== undefined) { - props.releaseGroup = releaseGroup; - } - - return updateItem({ section, id, ...props }); + return updateItem({ + section, + id, + ...props + }); }), set({ diff --git a/src/Radarr.Api.V3/MovieFiles/MovieFileController.cs b/src/Radarr.Api.V3/MovieFiles/MovieFileController.cs index 77e8e5d73..bae311e52 100644 --- a/src/Radarr.Api.V3/MovieFiles/MovieFileController.cs +++ b/src/Radarr.Api.V3/MovieFiles/MovieFileController.cs @@ -183,6 +183,55 @@ namespace Radarr.Api.V3.MovieFiles return new { }; } + [HttpPut("bulk")] + [Consumes("application/json")] + public object SetPropertiesBulk([FromBody] List resources) + { + var movieFiles = _mediaFileService.GetMovies(resources.Select(r => r.Id)); + + foreach (var movieFile in movieFiles) + { + var resourceMovieFile = resources.Single(r => r.Id == movieFile.Id); + + if (resourceMovieFile.Languages != null) + { + // Don't allow user to set files with 'Any' or 'Original' language + movieFile.Languages = resourceMovieFile.Languages.Where(l => l != null && l != Language.Any && l != Language.Original).ToList(); + } + + if (resourceMovieFile.Quality != null) + { + movieFile.Quality = resourceMovieFile.Quality; + } + + if (resourceMovieFile.SceneName != null && SceneChecker.IsSceneTitle(resourceMovieFile.SceneName)) + { + movieFile.SceneName = resourceMovieFile.SceneName; + } + + if (resourceMovieFile.Edition != null) + { + movieFile.Edition = resourceMovieFile.Edition; + } + + if (resourceMovieFile.ReleaseGroup != null) + { + movieFile.ReleaseGroup = resourceMovieFile.ReleaseGroup; + } + + if (resourceMovieFile.IndexerFlags.HasValue) + { + movieFile.IndexerFlags = (IndexerFlags)resourceMovieFile.IndexerFlags; + } + } + + _mediaFileService.Update(movieFiles); + + var movie = _movieService.GetMovie(movieFiles.First().MovieId); + + return Accepted(movieFiles.ConvertAll(f => f.ToResource(movie, _upgradableSpecification, _formatCalculator))); + } + [NonAction] public void Handle(MovieFileAddedEvent message) {