Fixed: Updating movie files via Manage Files

This commit is contained in:
Bogdan 2025-03-22 15:33:01 +02:00
parent 1d855aed00
commit 35f1a61bf8
9 changed files with 85 additions and 513 deletions

View file

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

View file

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectLanguageModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectLanguageModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectLanguageModal;

View file

@ -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 (
<SelectLanguageModalContent
{...this.props}
onLanguageSelect={this.onLanguageSelect}
/>
);
}
}
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);

View file

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectQualityModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectQualityModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectQualityModal;

View file

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ManualImportSelectQuality')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('QualitiesLoadError')}
</Alert>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('Quality')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={this.onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Proper')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={this.onProperChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Real')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={this.onRealChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onQualitySelect}
>
{translate('SelectQuality')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
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;

View file

@ -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 (
<SelectQualityModalContent
{...this.props}
onQualitySelect={this.onQualitySelect}
/>
);
}
}
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);

View file

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

View file

@ -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({

View file

@ -183,6 +183,55 @@ namespace Radarr.Api.V3.MovieFiles
return new { };
}
[HttpPut("bulk")]
[Consumes("application/json")]
public object SetPropertiesBulk([FromBody] List<MovieFileResource> 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)
{