New: Reset Quality Definitions to default

This commit is contained in:
Qstick 2022-06-05 22:50:46 -05:00 committed by Mark McDowall
parent 7c98c2397a
commit d5fff15f32
13 changed files with 333 additions and 9 deletions

View File

@ -19,7 +19,7 @@ import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings'; import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles'; import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality'; import QualityConnector from 'Settings/Quality/QualityConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
@ -158,7 +158,7 @@ function AppRoutes(props) {
<Route <Route
path="/settings/quality" path="/settings/quality"
component={Quality} component={QualityConnector}
/> />
<Route <Route

View File

@ -15,6 +15,7 @@ export const REFRESH_SERIES = 'RefreshSeries';
export const RENAME_FILES = 'RenameFiles'; export const RENAME_FILES = 'RenameFiles';
export const RENAME_SERIES = 'RenameSeries'; export const RENAME_SERIES = 'RenameSeries';
export const RESET_API_KEY = 'ResetApiKey'; export const RESET_API_KEY = 'ResetApiKey';
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
export const RSS_SYNC = 'RssSync'; export const RSS_SYNC = 'RssSync';
export const SEASON_SEARCH = 'SeasonSearch'; export const SEASON_SEARCH = 'SeasonSearch';
export const SERIES_SEARCH = 'SeriesSearch'; export const SERIES_SEARCH = 'SeriesSearch';

View File

@ -14,6 +14,7 @@ import { fetchHealth } from 'Store/Actions/systemActions';
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions'; import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions'; import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
function getState(status) { function getState(status) {
switch (status) { switch (status) {
@ -70,6 +71,7 @@ const mapDispatchToProps = {
dispatchUpdateItem: updateItem, dispatchUpdateItem: updateItem,
dispatchRemoveItem: removeItem, dispatchRemoveItem: removeItem,
dispatchFetchHealth: fetchHealth, dispatchFetchHealth: fetchHealth,
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
dispatchFetchQueue: fetchQueue, dispatchFetchQueue: fetchQueue,
dispatchFetchQueueDetails: fetchQueueDetails, dispatchFetchQueueDetails: fetchQueueDetails,
dispatchFetchRootFolders: fetchRootFolders, dispatchFetchRootFolders: fetchRootFolders,
@ -221,6 +223,10 @@ class SignalRConnector extends Component {
} }
} }
handleQualitydefinition = () => {
this.props.dispatchFetchQualityDefinitions();
}
handleQueue = () => { handleQueue = () => {
if (this.props.isQueuePopulated) { if (this.props.isQueuePopulated) {
this.props.dispatchFetchQueue(); this.props.dispatchFetchQueue();
@ -377,6 +383,7 @@ SignalRConnector.propTypes = {
dispatchUpdateItem: PropTypes.func.isRequired, dispatchUpdateItem: PropTypes.func.isRequired,
dispatchRemoveItem: PropTypes.func.isRequired, dispatchRemoveItem: PropTypes.func.isRequired,
dispatchFetchHealth: PropTypes.func.isRequired, dispatchFetchHealth: PropTypes.func.isRequired,
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
dispatchFetchQueue: PropTypes.func.isRequired, dispatchFetchQueue: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired, dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired,

View File

@ -1,8 +1,13 @@
import React, { Component } from 'react'; import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent'; 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 QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector'; import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
class Quality extends Component { class Quality extends Component {
@ -16,7 +21,8 @@ class Quality extends Component {
this.state = { this.state = {
isSaving: false, isSaving: false,
hasPendingChanges: false hasPendingChanges: false,
isConfirmQualityDefinitionResetModalOpen: false
}; };
} }
@ -31,6 +37,14 @@ class Quality extends Component {
this.setState(payload); this.setState(payload);
} }
onResetQualityDefinitionsPress = () => {
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
}
onCloseResetQualityDefinitionsModal = () => {
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
}
onSavePress = () => { onSavePress = () => {
if (this._saveCallback) { if (this._saveCallback) {
this._saveCallback(); this._saveCallback();
@ -43,6 +57,7 @@ class Quality extends Component {
render() { render() {
const { const {
isSaving, isSaving,
isResettingQualityDefinitions,
hasPendingChanges hasPendingChanges
} = this.state; } = this.state;
@ -51,6 +66,18 @@ class Quality extends Component {
<SettingsToolbarConnector <SettingsToolbarConnector
isSaving={isSaving} isSaving={isSaving}
hasPendingChanges={hasPendingChanges} hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label="Reset Definitions"
iconName={icons.REFRESH}
isSpinning={isResettingQualityDefinitions}
onPress={this.onResetQualityDefinitionsPress}
/>
</Fragment>
}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
/> />
@ -60,9 +87,18 @@ class Quality extends Component {
onChildStateChange={this.onChildStateChange} onChildStateChange={this.onChildStateChange}
/> />
</PageContentBody> </PageContentBody>
<ResetQualityDefinitionsModal
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
onModalClose={this.onCloseResetQualityDefinitionsModal}
/>
</PageContent> </PageContent>
); );
} }
} }
Quality.propTypes = {
isResettingQualityDefinitions: PropTypes.bool.isRequired
};
export default Quality; export default Quality;

View File

@ -0,0 +1,38 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as commandNames from 'Commands/commandNames';
import Quality from './Quality';
function createMapStateToProps() {
return createSelector(
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS),
(isResettingQualityDefinitions) => {
return {
isResettingQualityDefinitions
};
}
);
}
class QualityConnector extends Component {
//
// Render
render() {
return (
<Quality
{...this.props}
/>
);
}
}
QualityConnector.propTypes = {
isResettingQualityDefinitions: PropTypes.bool.isRequired
};
export default connect(createMapStateToProps)(QualityConnector);

View File

@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal';
import ResetQualityDefinitionsModalContentConnector from './ResetQualityDefinitionsModalContentConnector';
function ResetQualityDefinitionsModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<ResetQualityDefinitionsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
ResetQualityDefinitionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ResetQualityDefinitionsModal;

View File

@ -0,0 +1,3 @@
.messageContainer {
margin-bottom: 20px;
}

View File

@ -0,0 +1,103 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import styles from './ResetQualityDefinitionsModalContent.css';
class ResetQualityDefinitionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
resetDefinitionTitles: false
};
}
//
// Listeners
onResetDefinitionTitlesChange = ({ value }) => {
this.setState({ resetDefinitionTitles: value });
}
onResetQualityDefinitionsConfirmed = () => {
const resetDefinitionTitles = this.state.resetDefinitionTitles;
this.setState({ resetDefinitionTitles: false });
this.props.onResetQualityDefinitions(resetDefinitionTitles);
}
//
// Render
render() {
const {
onModalClose,
isResettingQualityDefinitions
} = this.props;
const resetDefinitionTitles = this.state.resetDefinitionTitles;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Reset Quality Definitions
</ModalHeader>
<ModalBody>
<div className={styles.messageContainer}>
Are you sure you want to reset quality definitions?
</div>
<FormGroup>
<FormLabel>Reset Titles</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="resetDefinitionTitles"
value={resetDefinitionTitles}
helpText="Reset definition titles as well as values"
onChange={this.onResetDefinitionTitlesChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onResetQualityDefinitionsConfirmed}
isDisabled={isResettingQualityDefinitions}
>
Reset
</Button>
</ModalFooter>
</ModalContent>
);
}
}
ResetQualityDefinitionsModalContent.propTypes = {
onResetQualityDefinitions: PropTypes.func.isRequired,
isResettingQualityDefinitions: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ResetQualityDefinitionsModalContent;

View File

@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import ResetQualityDefinitionsModalContent from './ResetQualityDefinitionsModalContent';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
function createMapStateToProps() {
return createSelector(
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS),
(isResettingQualityDefinitions) => {
return {
isResettingQualityDefinitions
};
}
);
}
const mapDispatchToProps = {
executeCommand
};
class ResetQualityDefinitionsModalContentConnector extends Component {
//
// Listeners
onResetQualityDefinitions = (resetTitles) => {
this.props.executeCommand({ name: commandNames.RESET_QUALITY_DEFINITIONS, resetTitles });
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<ResetQualityDefinitionsModalContent
{...this.props}
onResetQualityDefinitions={this.onResetQualityDefinitions}
/>
);
}
}
ResetQualityDefinitionsModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
isResettingQualityDefinitions: PropTypes.bool.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ResetQualityDefinitionsModalContentConnector);

View File

@ -0,0 +1,14 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Qualities.Commands
{
public class ResetQualityDefinitionsCommand : Command
{
public bool ResetTitles { get; set; }
public ResetQualityDefinitionsCommand(bool resetTitles = false)
{
ResetTitles = resetTitles;
}
}
}

View File

@ -14,7 +14,5 @@ namespace NzbDrone.Core.Qualities
: base(database, eventAggregator) : base(database, eventAggregator)
{ {
} }
} }
} }

View File

@ -5,6 +5,8 @@ using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using System; using System;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Qualities.Commands;
namespace NzbDrone.Core.Qualities namespace NzbDrone.Core.Qualities
{ {
@ -17,7 +19,7 @@ namespace NzbDrone.Core.Qualities
QualityDefinition Get(Quality quality); QualityDefinition Get(Quality quality);
} }
public class QualityDefinitionService : IQualityDefinitionService, IHandle<ApplicationStartedEvent> public class QualityDefinitionService : IQualityDefinitionService, IExecute<ResetQualityDefinitionsCommand>, IHandle<ApplicationStartedEvent>
{ {
private readonly IQualityDefinitionRepository _repo; private readonly IQualityDefinitionRepository _repo;
private readonly ICached<Dictionary<Quality, QualityDefinition>> _cache; private readonly ICached<Dictionary<Quality, QualityDefinition>> _cache;
@ -106,5 +108,28 @@ namespace NzbDrone.Core.Qualities
InsertMissingDefinitions(); InsertMissingDefinitions();
} }
public void Execute(ResetQualityDefinitionsCommand message)
{
List<QualityDefinition> updateList = new List<QualityDefinition>();
var allDefinitions = Quality.DefaultQualityDefinitions.OrderBy(d => d.Weight).ToList();
var existingDefinitions = _repo.All().ToList();
foreach (var definition in allDefinitions)
{
var existing = existingDefinitions.SingleOrDefault(d => d.Quality == definition.Quality);
existing.MinSize = definition.MinSize;
existing.MaxSize = definition.MaxSize;
existing.Title = message.ResetTitles ? definition.Title : existing.Title;
updateList.Add(existing);
}
_repo.UpdateMany(updateList);
_cache.Clear();
}
} }
} }

View File

@ -1,17 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy; using Nancy;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.SignalR;
using Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.Extensions; using Sonarr.Http.Extensions;
namespace Sonarr.Api.V3.Qualities namespace Sonarr.Api.V3.Qualities
{ {
public class QualityDefinitionModule : SonarrRestModule<QualityDefinitionResource> public class QualityDefinitionModule : SonarrRestModuleWithSignalR<QualityDefinitionResource, QualityDefinition>, IHandle<CommandExecutedEvent>
{ {
private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IQualityDefinitionService _qualityDefinitionService;
public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster)
: base(signalRBroadcaster)
{ {
_qualityDefinitionService = qualityDefinitionService; _qualityDefinitionService = qualityDefinitionService;
@ -50,5 +54,13 @@ namespace Sonarr.Api.V3.Qualities
.ToResource() .ToResource()
, HttpStatusCode.Accepted); , HttpStatusCode.Accepted);
} }
public void Handle(CommandExecutedEvent message)
{
if (message.Command.Name == "ResetQualityDefinitions")
{
BroadcastResourceChange(ModelAction.Sync);
}
}
} }
} }