From 16ff1176f7a0200b7c1addc4ace4f19cfaba2ce5 Mon Sep 17 00:00:00 2001 From: Qstick Date: Mon, 10 Jun 2019 22:59:39 -0400 Subject: [PATCH] Fixed: Quality Groups and Profiles --- frontend/src/App/AppRoutes.js | 4 +- .../CustomFormatSettingsConnector.js | 30 +++ .../CustomFormats/CustomFormat.css | 38 ++++ .../CustomFormats/CustomFormat.js | 141 +++++++++++++ .../CustomFormats/CustomFormats.css | 21 ++ .../CustomFormats/CustomFormats.js | 109 ++++++++++ .../CustomFormats/CustomFormatsConnector.js | 65 ++++++ .../CustomFormats/CustomFormatsConnector.js | 17 -- ...EditQualityProfileModalContentConnector.js | 2 +- .../Profiles/Quality/QualityProfile.js | 8 +- .../Store/Actions/Settings/customFormats.js | 97 +++++++++ frontend/src/Store/Actions/settingsActions.js | 5 + src/NzbDrone.Api/Profiles/ProfileModule.cs | 1 - src/NzbDrone.Api/Profiles/ProfileResource.cs | 37 +++- .../Profiles/ProfileSchemaModule.cs | 4 +- .../Datastore/MarrDataLazyLoadingFixture.cs | 2 +- ...matAllowedByProfileSpecificationFixture.cs | 2 +- .../CutoffSpecificationFixture.cs | 14 +- .../HistorySpecificationFixture.cs | 8 +- ...ityAllowedByProfileSpecificationFixture.cs | 2 +- .../QueueSpecificationFixture.cs | 6 +- .../RssSync/DelaySpecificationFixture.cs | 2 +- .../RssSync/ProperSpecificationFixture.cs | 2 +- .../UpgradeDiskSpecificationFixture.cs | 2 +- .../PendingReleaseServiceTests/AddFixture.cs | 2 +- .../RemoveGrabbedFixture.cs | 2 +- .../RemoveRejectedFixture.cs | 2 +- .../HistoryTests/HistoryServiceFixture.cs | 4 +- .../MovieRepositoryFixture.cs | 2 +- .../NzbDrone.Core.Test.csproj | 1 + .../Profiles/ProfileRepositoryFixture.cs | 2 +- .../Qualities/QualityIndexCompareToFixture.cs | 36 ++++ .../Qualities/QualityModelComparerFixture.cs | 72 ++++++- src/NzbDrone.Core/Datastore/TableMapping.cs | 3 +- .../DownloadDecisionComparer.cs | 2 +- .../QualityUpgradableSpecification.cs | 2 +- .../Specifications/CutoffSpecification.cs | 11 +- .../QualityAllowedByProfileSpecification.cs | 6 +- .../RssSync/DelaySpecification.cs | 4 +- .../Specifications/UpgradeSpecification.cs | 4 +- .../Movies/MovieCutoffService.cs | 10 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Profiles/Profile.cs | 53 ++++- .../Profiles/ProfileQualityItem.cs | 37 +++- src/NzbDrone.Core/Profiles/ProfileService.cs | 85 ++++++-- src/NzbDrone.Core/Profiles/QualityIndex.cs | 55 +++++ .../Qualities/QualityDefinition.cs | 4 +- .../Qualities/QualityModelComparer.cs | 62 ++++-- .../Quality/QualityCutoffValidator.cs | 39 ++++ .../Profiles/Quality/QualityItemsValidator.cs | 197 ++++++++++++++++++ .../Profiles/Quality/QualityProfileModule.cs | 6 +- .../Quality/QualityProfileResource.cs | 31 ++- .../Quality/QualityProfileSchemaModule.cs | 45 +--- .../Quality/QualityProfileValidation.cs | 43 ---- src/Radarr.Api.V2/Radarr.Api.V2.csproj | 3 +- 55 files changed, 1229 insertions(+), 216 deletions(-) create mode 100644 frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js delete mode 100644 frontend/src/Settings/CustomFormats/CustomFormatsConnector.js create mode 100644 frontend/src/Store/Actions/Settings/customFormats.js create mode 100644 src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs create mode 100644 src/NzbDrone.Core/Profiles/QualityIndex.cs create mode 100644 src/Radarr.Api.V2/Profiles/Quality/QualityCutoffValidator.cs create mode 100644 src/Radarr.Api.V2/Profiles/Quality/QualityItemsValidator.cs delete mode 100644 src/Radarr.Api.V2/Profiles/Quality/QualityProfileValidation.cs diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 6e94fe4a1..5558d0e0f 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -17,7 +17,7 @@ import Settings from 'Settings/Settings'; import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; import Profiles from 'Settings/Profiles/Profiles'; import Quality from 'Settings/Quality/Quality'; -import CustomFormatsConnector from 'Settings/CustomFormats/CustomFormatsConnector'; +import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector'; import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import NetImportSettingsConnector from 'Settings/NetImport/NetImportSettingsConnector'; @@ -142,7 +142,7 @@ function AppRoutes(props) { + + + + + + + ); + } +} + +export default DragDropContext(HTML5Backend)(CustomFormatSettingsConnector); + diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.css b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.css new file mode 100644 index 000000000..ed030aa80 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.css @@ -0,0 +1,38 @@ +.customFormat { + composes: card from '~Components/Card.css'; + + width: 300px; +} + +.nameContainer { + display: flex; + justify-content: space-between; +} + +.name { + @add-mixin truncate; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.cloneButton { + composes: button from '~Components/Link/IconButton.css'; + + height: 36px; +} + +.formats { + display: flex; + flex-wrap: wrap; + margin-top: 5px; + pointer-events: all; +} + +.tooltipLabel { + composes: label from '~Components/Label.css'; + + margin: 0; + border: none; +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js new file mode 100644 index 000000000..d389ff536 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js @@ -0,0 +1,141 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector'; +import styles from './CustomFormat.css'; + +class CustomFormat extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditCustomFormatModalOpen: false, + isDeleteCustomFormatModalOpen: false + }; + } + + // + // Listeners + + onEditCustomFormatPress = () => { + this.setState({ isEditCustomFormatModalOpen: true }); + } + + onEditCustomFormatModalClose = () => { + this.setState({ isEditCustomFormatModalOpen: false }); + } + + onDeleteCustomFormatPress = () => { + this.setState({ + isEditCustomFormatModalOpen: false, + isDeleteCustomFormatModalOpen: true + }); + } + + onDeleteCustomFormatModalClose = () => { + this.setState({ isDeleteCustomFormatModalOpen: false }); + } + + onConfirmDeleteCustomFormat = () => { + this.props.onConfirmDeleteCustomFormat(this.props.id); + } + + onCloneCustomFormatPress = () => { + const { + id, + onCloneCustomFormatPress + } = this.props; + + onCloneCustomFormatPress(id); + } + + // + // Render + + render() { + const { + // id, + name, + items, + isDeleting + } = this.props; + + return ( + +
+
+ {name} +
+ + +
+ +
+ { + items.map((item) => { + if (!item.allowed) { + return null; + } + + return ( + + ); + }) + } +
+ + {/* */} + + +
+ ); + } +} + +CustomFormat.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteCustomFormat: PropTypes.func.isRequired, + onCloneCustomFormatPress: PropTypes.func.isRequired +}; + +export default CustomFormat; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.css b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.css new file mode 100644 index 000000000..d4b3d6375 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.css @@ -0,0 +1,21 @@ +.customFormats { + display: flex; + flex-wrap: wrap; +} + +.addCustomFormat { + composes: customFormat from '~./CustomFormat.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; + font-size: 45px; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js new file mode 100644 index 000000000..f17ad18d4 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js @@ -0,0 +1,109 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import CustomFormat from './CustomFormat'; +// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector'; +import styles from './CustomFormats.css'; + +class CustomFormats extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isCustomFormatModalOpen: false + }; + } + + // + // Listeners + + onCloneCustomFormatPress = (id) => { + this.props.onCloneCustomFormatPress(id); + this.setState({ isCustomFormatModalOpen: true }); + } + + onEditCustomFormatPress = () => { + this.setState({ isCustomFormatModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isCustomFormatModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + isDeleting, + onConfirmDeleteCustomFormat, + onCloneCustomFormatPress, + ...otherProps + } = this.props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + {/* + */} + +
+
+ ); + } +} + +CustomFormats.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteCustomFormat: PropTypes.func.isRequired, + onCloneCustomFormatPress: PropTypes.func.isRequired +}; + +export default CustomFormats; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js new file mode 100644 index 000000000..ee5f820c4 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchCustomFormats, deleteCustomFormat, cloneCustomFormat } from 'Store/Actions/settingsActions'; +import CustomFormats from './CustomFormats'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.customFormats, + (customFormats) => { + return { + ...customFormats + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchCustomFormats: fetchCustomFormats, + dispatchDeleteCustomFormat: deleteCustomFormat, + dispatchCloneCustomFormat: cloneCustomFormat +}; + +class CustomFormatsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchCustomFormats(); + } + + // + // Listeners + + onConfirmDeleteCustomFormat = (id) => { + this.props.dispatchDeleteCustomFormat({ id }); + } + + onCloneCustomFormatPress = (id) => { + this.props.dispatchCloneCustomFormat({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CustomFormatsConnector.propTypes = { + dispatchFetchCustomFormats: PropTypes.func.isRequired, + dispatchDeleteCustomFormat: PropTypes.func.isRequired, + dispatchCloneCustomFormat: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CustomFormatsConnector); diff --git a/frontend/src/Settings/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormatsConnector.js deleted file mode 100644 index ae818f64a..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormatsConnector.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import PageContent from 'Components/Page/PageContent'; -import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; - -function CustomFormatsConnector() { - return ( - - - - - ); -} - -export default CustomFormatsConnector; - diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js index ea93ed2dd..2decf2198 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js @@ -121,7 +121,7 @@ class EditQualityProfileModalContentConnector extends Component { return false; } - return i.id === cutoff.id || (i.quality && i.quality.id === cutoff.id); + return i.id === cutoff || (i.quality && i.quality.id === cutoff); }); // If the cutoff isn't allowed anymore or there isn't a cutoff set one diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.js b/frontend/src/Settings/Profiles/Quality/QualityProfile.js index 65da9fa3b..703f13ac1 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfile.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.js @@ -97,20 +97,20 @@ class QualityProfile extends Component { } if (item.quality) { - const isCutoff = item.quality.id === cutoff.id; + const isCutoff = item.quality.id === cutoff; return ( ); } - const isCutoff = item.id === cutoff.id; + const isCutoff = item.id === cutoff; return ( { + return { + section, + ...payload + }; +}); + +export const cloneCustomFormat = createAction(CLONE_CUSTOM_FORMAT); + +// +// Details + +export default { + + // + // State + + defaultState: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isSchemaFetching: false, + isSchemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + // + // Action Handlers + + actionHandlers: { + [FETCH_CUSTOM_FORMATS]: createFetchHandler(section, '/customformat'), + [FETCH_CUSTOM_FORMAT_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'), + [SAVE_CUSTOM_FORMAT]: createSaveProviderHandler(section, '/customformat'), + [DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat') + }, + + // + // Reducers + + reducers: { + [SET_CUSTOM_FORMAT_VALUE]: createSetSettingValueReducer(section), + + [CLONE_CUSTOM_FORMAT]: function(state, { payload }) { + const id = payload.id; + const newState = getSectionState(state, section); + const item = newState.items.find((i) => i.id === id); + const pendingChanges = { ...item, id: 0 }; + delete pendingChanges.id; + + pendingChanges.name = `${pendingChanges.name} - Copy`; + newState.pendingChanges = pendingChanges; + + return updateSectionState(state, section, newState); + } + } + +}; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 0dab4fb54..07dc280ee 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -1,6 +1,7 @@ import { createAction } from 'redux-actions'; import { handleThunks } from 'Store/thunks'; import createHandleActions from './Creators/createHandleActions'; +import customFormats from './Settings/customFormats'; import delayProfiles from './Settings/delayProfiles'; import downloadClients from './Settings/downloadClients'; import downloadClientOptions from './Settings/downloadClientOptions'; @@ -20,6 +21,7 @@ import remotePathMappings from './Settings/remotePathMappings'; import restrictions from './Settings/restrictions'; import ui from './Settings/ui'; +export * from './Settings/customFormats'; export * from './Settings/delayProfiles'; export * from './Settings/downloadClients'; export * from './Settings/downloadClientOptions'; @@ -50,6 +52,7 @@ export const section = 'settings'; export const defaultState = { advancedSettings: false, + customFormats: customFormats.defaultState, delayProfiles: delayProfiles.defaultState, downloadClients: downloadClients.defaultState, downloadClientOptions: downloadClientOptions.defaultState, @@ -88,6 +91,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS); // Action Handlers export const actionHandlers = handleThunks({ + ...customFormats.actionHandlers, ...delayProfiles.actionHandlers, ...downloadClients.actionHandlers, ...downloadClientOptions.actionHandlers, @@ -117,6 +121,7 @@ export const reducers = createHandleActions({ return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); }, + ...customFormats.reducers, ...delayProfiles.reducers, ...downloadClients.reducers, ...downloadClientOptions.reducers, diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs index 554bcf991..53a69bd12 100644 --- a/src/NzbDrone.Api/Profiles/ProfileModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileModule.cs @@ -6,7 +6,6 @@ using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Profiles; using NzbDrone.Core.Validation; using Radarr.Http; -using Radarr.Http.Mapping; namespace NzbDrone.Api.Profiles { diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs index 8bd0b1902..2c1ad1fb6 100644 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using NzbDrone.Common.Extensions; using NzbDrone.Api.Qualities; using Radarr.Http.REST; using NzbDrone.Core.Parser; @@ -37,14 +38,42 @@ namespace NzbDrone.Api.Profiles { if (model == null) return null; + var cutoffItem = model.Items.First(q => + { + if (q.Id == model.Cutoff) return true; + + if (q.Quality == null) return false; + + return q.Quality.Id == model.Cutoff; + }); + + var cutoff = cutoffItem.Items == null || cutoffItem.Items.Empty() + ? cutoffItem.Quality + : cutoffItem.Items.First().Quality; + return new ProfileResource { Id = model.Id, Name = model.Name, - Cutoff = model.Cutoff, PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "", - Items = model.Items.ConvertAll(ToResource), + Cutoff = cutoff, + + // Flatten groups so things don't explode + Items = model.Items.SelectMany(i => + { + if (i == null) + { + return null; + } + + if (i.Items.Any()) + { + return i.Items.ConvertAll(ToResource); + } + + return new List { ToResource(i) }; + }).ToList(), FormatCutoff = model.FormatCutoff.ToResource(), FormatItems = model.FormatItems.ConvertAll(ToResource), Language = model.Language @@ -80,7 +109,7 @@ namespace NzbDrone.Api.Profiles Id = resource.Id, Name = resource.Name, - Cutoff = (Quality)resource.Cutoff.Id, + Cutoff = resource.Cutoff.Id, PreferredTags = resource.PreferredTags.Split(',').ToList(), Items = resource.Items.ConvertAll(ToModel), FormatCutoff = resource.FormatCutoff.ToModel(), diff --git a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs index cfa9dbdb1..4fe278247 100644 --- a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Parser; @@ -42,7 +42,7 @@ namespace NzbDrone.Api.Profiles }); var profile = new Profile(); - profile.Cutoff = Quality.Unknown; + profile.Cutoff = Quality.Unknown.Id; profile.Items = items; profile.FormatCutoff = CustomFormat.None; profile.FormatItems = formatItems; diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs index 26aef7971..2200b0d44 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.Datastore var profile = new Profile { Name = "Test", - Cutoff = Quality.WEBDL720p, + Cutoff = Quality.WEBDL720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs index 72a8aa582..970cd52da 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p.Id }) .Build(); remoteMovie = new RemoteMovie diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index df119123d..2a0631ebc 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Profiles; @@ -32,28 +32,28 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_current_episode_is_less_than_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.DVD, new Revision(version: 2))).Should().BeTrue(); } [Test] public void should_return_false_if_current_episode_is_equal_to_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, new Revision(version: 2))).Should().BeFalse(); } [Test] public void should_return_false_if_current_episode_is_greater_than_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse(); } [Test] public void should_return_true_when_new_episode_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, new Revision(version: 1)), new QualityModel(Quality.HDTV720p, new Revision(version: 2))).Should().BeTrue(); } @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, new Revision(version: 2)), new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse(); } @@ -77,7 +77,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), FormatCutoff = CustomFormats.CustomFormat.None, FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format") diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 439589664..284ee4ec2 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _upgradeHistory = Mocker.Resolve(); _fakeMovie = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultSingle = new RemoteMovie @@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing() { - _fakeMovie.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeMovie.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); @@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_be_upgradable_if_cutoff_already_met() { - _fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); _upgradableQuality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)); @@ -184,7 +184,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_return_false_if_cutoff_already_met_and_cdh_is_disabled() { GivenCdhDisabled(); - _fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index c92a27c8b..980e7363f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p.Id }) .Build(); remoteMovie = new RemoteMovie diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index 467eaff9d..e88730ce9 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_when_quality_in_queue_is_lower() { - _movie.Profile.Value.Cutoff = Quality.Bluray1080p; + _movie.Profile.Value.Cutoff = Quality.Bluray1080p.Id; var remoteEpisode = Builder.CreateNew() .With(r => r.Movie = _movie) @@ -113,7 +113,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_false_when_quality_in_queue_is_better() { - _movie.Profile.Value.Cutoff = Quality.Bluray1080p; + _movie.Profile.Value.Cutoff = Quality.Bluray1080p.Id; var remoteEpisode = Builder.CreateNew() .With(r => r.Movie = _movie) @@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_false_if_quality_in_queue_meets_cutoff() { - _movie.Profile.Value.Cutoff = _remoteMovie.ParsedMovieInfo.Quality.Quality; + _movie.Profile.Value.Cutoff = _remoteMovie.ParsedMovieInfo.Quality.Quality.Id; var remoteEpisode = Builder.CreateNew() .With(r => r.Movie = _movie) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index 1f01c4ae1..db488ff03 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.WEBDL720p }); _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p }); - _profile.Cutoff = Quality.WEBDL720p; + _profile.Cutoff = Quality.WEBDL720p.Id; _remoteEpisode.ParsedMovieInfo = new ParsedMovieInfo(); _remoteEpisode.Release = new ReleaseInfo(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index a184ac80b..4c0a56a70 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _secondFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)), DateAdded = DateTime.Now }; var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id }) .With(c => c.MovieFile = _firstFile) .Build(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 81fc48bb2..78fa2d60a 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _firstFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now }; var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }) .With(e => e.MovieFile = _firstFile) .Build(); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index d3733c082..0d6f8ba51 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _profile = new Profile { Name = "Test", - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index 30fbe5cb8..c0a9f9a56 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _profile = new Profile { Name = "Test", - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 2f21b8b56..0a91cdc7d 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _profile = new Profile { Name = "Test", - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index a5ab5f8a0..708d7fde6 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -25,8 +25,8 @@ namespace NzbDrone.Core.Test.HistoryTests [SetUp] public void Setup() { - _profile = new Profile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities() }; - _profileCustom = new Profile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities(Quality.DVD) }; + _profile = new Profile { Cutoff = Quality.WEBDL720p.Id, Items = QualityFixture.GetDefaultQualities() }; + _profileCustom = new Profile { Cutoff = Quality.WEBDL720p.Id, Items = QualityFixture.GetDefaultQualities(Quality.DVD) }; } [Test] diff --git a/src/NzbDrone.Core.Test/MovieTests/MovieRepositoryTests/MovieRepositoryFixture.cs b/src/NzbDrone.Core.Test/MovieTests/MovieRepositoryTests/MovieRepositoryFixture.cs index 083c351ca..c3e838bca 100644 --- a/src/NzbDrone.Core.Test/MovieTests/MovieRepositoryTests/MovieRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MovieTests/MovieRepositoryTests/MovieRepositoryFixture.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.MovieTests.MovieRepositoryTests Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(), FormatCutoff = CustomFormats.CustomFormat.None, - Cutoff = Quality.Bluray1080p, + Cutoff = Quality.Bluray1080p.Id, Name = "TestProfile" }; diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index b4eaaf4d5..bb5c85efe 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -305,6 +305,7 @@ + diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs index f048b1a93..d75b29f88 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.Profiles Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), FormatCutoff = CustomFormats.CustomFormat.None, FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(), - Cutoff = Quality.Bluray1080p, + Cutoff = Quality.Bluray1080p.Id, Name = "TestProfile" }; diff --git a/src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs b/src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs new file mode 100644 index 000000000..b2a68e9b6 --- /dev/null +++ b/src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Qualities +{ + [TestFixture] + public class QualityIndexCompareToFixture : CoreTest + { + [TestCase(1, 0, 1, 0, 0)] + [TestCase(1, 1, 1, 0, 1)] + [TestCase(2, 0, 1, 0, 1)] + [TestCase(1, 0, 1, 1, -1)] + [TestCase(1, 0, 2, 0, -1)] + public void should_match_expected_when_respect_group_order_is_true(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected) + { + var left = new QualityIndex(leftIndex, leftGroupIndex); + var right = new QualityIndex(rightIndex, rightGroupIndex); + left.CompareTo(right, true).Should().Be(expected); + } + + [TestCase(1, 0, 1, 0, 0)] + [TestCase(1, 1, 1, 0, 0)] + [TestCase(2, 0, 1, 0, 1)] + [TestCase(1, 0, 1, 1, 0)] + [TestCase(1, 0, 2, 0, -1)] + public void should_match_expected_when_respect_group_order_is_false(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected) + { + var left = new QualityIndex(leftIndex, leftGroupIndex); + var right = new QualityIndex(rightIndex, rightGroupIndex); + left.CompareTo(right, false).Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index ecc07fb34..4c81d7f40 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Profiles; @@ -31,6 +31,50 @@ namespace NzbDrone.Core.Test.Qualities Subject = new QualityModelComparer(new Profile { Items = QualityFixture.GetDefaultQualities(Quality.Bluray720p, Quality.DVD) }); } + private void GivenGroupedProfile() + { + var profile = new Profile + { + Items = new List + { + new ProfileQualityItem + { + Allowed = false, + Quality = Quality.SDTV + }, + new ProfileQualityItem + { + Allowed = false, + Quality = Quality.DVD + }, + new ProfileQualityItem + { + Allowed = true, + Items = new List + { + new ProfileQualityItem + { + Allowed = true, + Quality = Quality.HDTV720p + }, + new ProfileQualityItem + { + Allowed = true, + Quality = Quality.WEBDL720p + } + } + }, + new ProfileQualityItem + { + Allowed = true, + Quality = Quality.Bluray720p + } + } + }; + + Subject = new QualityModelComparer(profile); + } + private void GivenDefaultProfileWithFormats() { _customFormat1 = new CustomFormats.CustomFormat("My Format 1", "L_ENGLISH"){Id=1}; @@ -118,5 +162,31 @@ namespace NzbDrone.Core.Test.Qualities compare.Should().BeGreaterThan(0); } + + [Test] + public void should_ignore_group_order_by_default() + { + GivenGroupedProfile(); + + var first = new QualityModel(Quality.HDTV720p); + var second = new QualityModel(Quality.WEBDL720p); + + var compare = Subject.Compare(first, second); + + compare.Should().Be(0); + } + + [Test] + public void should_respect_group_order() + { + GivenGroupedProfile(); + + var first = new QualityModel(Quality.HDTV720p); + var second = new QualityModel(Quality.WEBDL720p); + + var compare = Subject.Compare(first, second, true); + + compare.Should().BeLessThan(0); + } } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 49174bbd5..9752c471c 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -112,7 +112,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("ImportExclusions"); Mapper.Entity().RegisterModel("QualityDefinitions") - .Ignore(d => d.Weight) + .Ignore(d => d.GroupName) + .Ignore(d => d.Weight) .Relationship(); Mapper.Entity().RegisterModel("CustomFormats") diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index ae97292db..dd3cd908d 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.DecisionEngine private int CompareQuality(DownloadDecision x, DownloadDecision y) { - return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.Value.Items.FindIndex(v => v.Quality == remoteMovie.ParsedMovieInfo.Quality.Quality)), + return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.Value.GetIndex(remoteMovie.ParsedMovieInfo.Quality.Quality)), CompareCustomFormats(x, y), CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Real), CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Version)); diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs index 1f636ba8c..3313f0698 100644 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.DecisionEngine public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) { var comparer = new QualityModelComparer(profile); - var compare = comparer.Compare(currentQuality.Quality, profile.Cutoff); + var compare = comparer.Compare(currentQuality.Quality.Id, profile.Cutoff); if (compare < 0) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index 48dbbaa80..4b1bd0717 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -21,11 +21,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) { + var profile = subject.Movie.Profile.Value; + if (subject.Movie.MovieFile != null) { - if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, subject.Movie.MovieFile.Quality, subject.ParsedMovieInfo.Quality)) + if (!_qualityUpgradableSpecification.CutoffNotMet(profile, + subject.Movie.MovieFile.Quality, + subject.ParsedMovieInfo.Quality)) { - return Decision.Reject("Existing file meets cutoff: {0}", subject.Movie.Profile.Value.Cutoff); + var qualityCutoffIndex = profile.GetIndex(profile.Cutoff); + var qualityCutoff = profile.Items[qualityCutoffIndex.Index]; + + return Decision.Reject("Existing file meets cutoff: {0} - {1}", qualityCutoff, subject.Movie.Profile.Value.Cutoff); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 6e33b00da..91b853fbb 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -18,7 +18,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedMovieInfo.Quality); - if (!subject.Movie.Profile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedMovieInfo.Quality.Quality)) + var profile = subject.Movie.Profile.Value; + var qualityIndex = profile.GetIndex(subject.ParsedMovieInfo.Quality.Quality); + var qualityOrGroup = profile.Items[qualityIndex.Index]; + + if (!qualityOrGroup.Allowed) { _logger.Debug("Quality {0} rejected by Movie's quality profile", subject.ParsedMovieInfo.Quality); return Decision.Reject("{0} is not wanted in profile", subject.ParsedMovieInfo.Quality.Quality); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index e1169e760..c6f80937a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -82,8 +82,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } // If quality meets or exceeds the best allowed quality in the profile accept it immediately - var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality()); - var isBestInProfile = comparer.Compare(subject.ParsedMovieInfo.Quality, bestQualityInProfile) >= 0; + var bestQualityInProfile = profile.LastAllowedQuality(); + var isBestInProfile = comparer.Compare(subject.ParsedMovieInfo.Quality.Quality, bestQualityInProfile) >= 0; if (isBestInProfile && isPreferredProtocol && (preferredCount > 0 || preferredWords == null)) { diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/UpgradeSpecification.cs index f78faf63b..5ce483c5c 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/Specifications/UpgradeSpecification.cs @@ -21,8 +21,8 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications var qualityComparer = new QualityModelComparer(localMovie.Movie.Profile); if (localMovie.Movie.MovieFile != null && qualityComparer.Compare(localMovie.Movie.MovieFile.Quality, localMovie.Quality) > 0) { - _logger.Debug("This file isn't an upgrade for all episodes. Skipping {0}", localMovie.Path); - return Decision.Reject("Not an upgrade for existing episode file(s)"); + _logger.Debug("This file isn't an upgrade for all movies. Skipping {0}", localMovie.Path); + return Decision.Reject("Not an upgrade for existing movie file(s)"); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/Movies/MovieCutoffService.cs b/src/NzbDrone.Core/Movies/MovieCutoffService.cs index 1c8e78062..65fd9d362 100644 --- a/src/NzbDrone.Core/Movies/MovieCutoffService.cs +++ b/src/NzbDrone.Core/Movies/MovieCutoffService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Datastore; @@ -16,13 +16,11 @@ namespace NzbDrone.Core.Movies { private readonly IMovieRepository _movieRepository; private readonly IProfileService _profileService; - private readonly Logger _logger; public MovieCutoffService(IMovieRepository movieRepository, IProfileService profileService, Logger logger) { _movieRepository = movieRepository; _profileService = profileService; - _logger = logger; } public PagingSpec MoviesWhereCutoffUnmet(PagingSpec pagingSpec) @@ -33,12 +31,12 @@ namespace NzbDrone.Core.Movies //Get all items less than the cutoff foreach (var profile in profiles) { - var cutoffIndex = profile.Items.FindIndex(v => v.Quality.Id == profile.Cutoff.Id); - var belowCutoff = profile.Items.Take(cutoffIndex).ToList(); + var cutoffIndex = profile.GetIndex(profile.Cutoff); + var belowCutoff = profile.Items.Take(cutoffIndex.Index).ToList(); if (belowCutoff.Any()) { - qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id))); + qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.SelectMany(i => i.GetQualities().Select(q => q.Id)))); } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e76bf23c9..02ca873ab 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1030,6 +1030,7 @@ + diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs index 154f37024..e05d7907f 100644 --- a/src/NzbDrone.Core/Profiles/Profile.cs +++ b/src/NzbDrone.Core/Profiles/Profile.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Profiles } public string Name { get; set; } - public Quality Cutoff { get; set; } + public int Cutoff { get; set; } public List Items { get; set; } public CustomFormat FormatCutoff { get; set; } public List FormatItems { get; set; } @@ -24,7 +24,54 @@ namespace NzbDrone.Core.Profiles public Quality LastAllowedQuality() { - return Items.Last(q => q.Allowed).Quality; + var lastAllowed = Items.Last(q => q.Allowed); + + if (lastAllowed.Quality != null) + { + return lastAllowed.Quality; + } + + // Returning any item from the group will work, + // returning the last because it's the true last quality. + return lastAllowed.Items.Last().Quality; + } + + public QualityIndex GetIndex(Quality quality) + { + return GetIndex(quality.Id); + } + + public QualityIndex GetIndex(int id) + { + for (var i = 0; i < Items.Count; i++) + { + var item = Items[i]; + var quality = item.Quality; + + // Quality matches by ID + if (quality != null && quality.Id == id) + { + return new QualityIndex(i); + } + + // Group matches by ID + if (item.Id > 0 && item.Id == id) + { + return new QualityIndex(i); + } + + for (var g = 0; g < item.Items.Count; g++) + { + var groupItem = item.Items[g]; + + if (groupItem.Quality.Id == id) + { + return new QualityIndex(i, g); + } + } + } + + return new QualityIndex(); } } } diff --git a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs index 7e7f4be84..c161a8516 100644 --- a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs +++ b/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs @@ -1,12 +1,47 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Profiles { public class ProfileQualityItem : IEmbeddedDocument { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int Id { get; set; } + public string Name { get; set; } public Quality Quality { get; set; } + public List Items { get; set; } public bool Allowed { get; set; } + + public ProfileQualityItem() + { + Items = new List(); + } + + public List GetQualities() + { + if (Quality == null) + { + return Items.Select(s => s.Quality).ToList(); + } + + return new List { Quality }; + } + + public override string ToString() + { + var qualitiesString = string.Join(", ", GetQualities()); + + if (Name.IsNotNullOrWhiteSpace()) + { + return $"{Name} ({qualitiesString})"; + } + + return qualitiesString; + } } } diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/ProfileService.cs index abc92b749..ac582cfdf 100644 --- a/src/NzbDrone.Core/Profiles/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/ProfileService.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.Profiles List All(); Profile Get(int id); bool Exists(int id); + Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed); } public class ProfileService : IProfileService, IHandle @@ -106,25 +107,6 @@ namespace NzbDrone.Core.Profiles return _profileRepository.Exists(id); } - private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed) - { - var items = Quality.DefaultQualityDefinitions - .OrderBy(v => v.Weight) - .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) - .ToList(); - - var profile = new Profile { Name = name, Cutoff = cutoff, Items = items, Language = Language.English, FormatCutoff = CustomFormat.None, FormatItems = new List - { - new ProfileFormatItem - { - Allowed = true, - Format = CustomFormat.None - } - }}; - - return Add(profile); - } - public void Handle(ApplicationStartedEvent message) { // Hack to force custom formats to be loaded into memory, if you have a better solution please let me know. @@ -200,5 +182,70 @@ namespace NzbDrone.Core.Profiles Quality.Remux2160p ); } + + public Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed) + { + var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight); + var items = new List(); + var groupId = 1000; + var profileCutoff = cutoff == null ? Quality.Unknown.Id : cutoff.Id; + + foreach (var group in groupedQualites) + { + if (group.Count() == 1) + { + var quality = group.First().Quality; + + items.Add(new ProfileQualityItem { Quality = group.First().Quality, Allowed = allowed.Contains(quality) }); + continue; + } + + var groupAllowed = group.Any(g => allowed.Contains(g.Quality)); + + items.Add(new ProfileQualityItem + { + Id = groupId, + Name = group.First().GroupName, + Items = group.Select(g => new ProfileQualityItem + { + Quality = g.Quality, + Allowed = groupAllowed + }).ToList(), + Allowed = groupAllowed + }); + + if (group.Any(g => g.Quality.Id == profileCutoff)) + { + profileCutoff = groupId; + } + + groupId++; + } + + var qualityProfile = new Profile + { + Name = name, + Cutoff = profileCutoff, + Items = items, + FormatCutoff = CustomFormat.None, + FormatItems = new List + { + new ProfileFormatItem + { + Allowed = true, + Format = CustomFormat.None + } + } + }; + + return qualityProfile; + } + + private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed) + { + var profile = GetDefaultProfile(name, cutoff, allowed); + + return Add(profile); + } } } diff --git a/src/NzbDrone.Core/Profiles/QualityIndex.cs b/src/NzbDrone.Core/Profiles/QualityIndex.cs new file mode 100644 index 000000000..3435a78a3 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/QualityIndex.cs @@ -0,0 +1,55 @@ +using System; + +namespace NzbDrone.Core.Profiles +{ + public class QualityIndex : IComparable, IComparable + { + public int Index { get; set; } + public int GroupIndex { get; set; } + + public QualityIndex() + { + Index = 0; + GroupIndex = 0; + } + + public QualityIndex(int index) + { + Index = index; + GroupIndex = 0; + } + + public QualityIndex(int index, int groupIndex) + { + Index = index; + GroupIndex = groupIndex; + } + + public int CompareTo(object obj) + { + return CompareTo((QualityIndex)obj, true); + } + + public int CompareTo(QualityIndex other) + { + return CompareTo(other, true); + } + + public int CompareTo(QualityIndex right, bool respectGroupOrder) + { + if (right == null) + { + return 1; + } + + var indexCompare = Index.CompareTo(right.Index); + + if (respectGroupOrder && indexCompare == 0) + { + return GroupIndex.CompareTo(right.GroupIndex); + } + + return indexCompare; ; + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinition.cs b/src/NzbDrone.Core/Qualities/QualityDefinition.cs index d8291fb08..abdb7831f 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinition.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinition.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities { @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Qualities public Quality Quality { get; set; } public string Title { get; set; } - + public string GroupName { get; set; } public int Weight { get; set; } public double? MinSize { get; set; } diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index ca29f6f89..324e334c0 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnsureThat; using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.Datastore.Migration; -using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Profiles; namespace NzbDrone.Core.Qualities @@ -20,12 +18,47 @@ namespace NzbDrone.Core.Qualities _profile = profile; } + public int Compare(int left, int right, bool respectGroupOrder = false) + { + var leftIndex = _profile.GetIndex(left); + var rightIndex = _profile.GetIndex(right); + + return leftIndex.CompareTo(rightIndex, respectGroupOrder); + } + public int Compare(Quality left, Quality right) { - int leftIndex = _profile.Items.FindIndex(v => v.Quality == left); - int rightIndex = _profile.Items.FindIndex(v => v.Quality == right); + return Compare(left, right, false); + } - return leftIndex.CompareTo(rightIndex); + public int Compare(Quality left, Quality right, bool respectGroupOrder) + { + var leftIndex = _profile.GetIndex(left); + var rightIndex = _profile.GetIndex(right); + + return leftIndex.CompareTo(rightIndex, respectGroupOrder); + } + + public int Compare(QualityModel left, QualityModel right) + { + return Compare(left, right, false); + } + + public int Compare(QualityModel left, QualityModel right, bool respectGroupOrder) + { + int result = Compare(left.Quality, right.Quality, respectGroupOrder); + + if (result == 0) + { + result = Compare(left.CustomFormats, right.CustomFormats); + + if (result == 0) + { + result = left.Revision.CompareTo(right.Revision); + } + } + + return result; } public int Compare(List left, List right) @@ -61,22 +94,5 @@ namespace NzbDrone.Core.Qualities return leftIndicies.Select(i => i.CompareTo(rightIndex)).Sum(); } - - public int Compare(QualityModel left, QualityModel right) - { - int result = Compare(left.Quality, right.Quality); - - if (result == 0) - { - result = Compare(left.CustomFormats, right.CustomFormats); - - if (result == 0) - { - result = left.Revision.CompareTo(right.Revision); - } - } - - return result; - } } } diff --git a/src/Radarr.Api.V2/Profiles/Quality/QualityCutoffValidator.cs b/src/Radarr.Api.V2/Profiles/Quality/QualityCutoffValidator.cs new file mode 100644 index 000000000..91773ed25 --- /dev/null +++ b/src/Radarr.Api.V2/Profiles/Quality/QualityCutoffValidator.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Radarr.Api.V2.Profiles.Quality +{ + public static class QualityCutoffValidator + { + public static IRuleBuilderOptions ValidCutoff(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new ValidCutoffValidator()); + } + } + + public class ValidCutoffValidator : PropertyValidator + { + public ValidCutoffValidator() + : base("Cutoff must be an allowed quality or group") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var cutoff = (int)context.PropertyValue; + dynamic instance = context.ParentContext.InstanceToValidate; + var items = instance.Items as IList; + + var cutoffItem = items.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff); + + if (cutoffItem == null) return false; + + if (!cutoffItem.Allowed) return false; + + return true; + } + } +} diff --git a/src/Radarr.Api.V2/Profiles/Quality/QualityItemsValidator.cs b/src/Radarr.Api.V2/Profiles/Quality/QualityItemsValidator.cs new file mode 100644 index 000000000..58b16c952 --- /dev/null +++ b/src/Radarr.Api.V2/Profiles/Quality/QualityItemsValidator.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; + +namespace Radarr.Api.V2.Profiles.Quality +{ + public static class QualityItemsValidator + { + public static IRuleBuilderOptions> ValidItems(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + ruleBuilder.SetValidator(new AllowedValidator()); + ruleBuilder.SetValidator(new QualityNameValidator()); + ruleBuilder.SetValidator(new EmptyItemGroupNameValidator()); + ruleBuilder.SetValidator(new ItemGroupIdValidator()); + ruleBuilder.SetValidator(new UniqueIdValidator()); + ruleBuilder.SetValidator(new UniqueQualityIdValidator()); + return ruleBuilder.SetValidator(new ItemGroupNameValidator()); + } + } + + public class AllowedValidator : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } + + public class EmptyItemGroupNameValidator : PropertyValidator + { + public EmptyItemGroupNameValidator() + : base("Groups must not be empty") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty())) + { + return false; + } + + return true; + } + } + + public class QualityNameValidator : PropertyValidator + { + public QualityNameValidator() + : base("Individual qualities should not be named") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null)) + { + return false; + } + + return true; + } + } + + public class ItemGroupNameValidator : PropertyValidator + { + public ItemGroupNameValidator() + : base("Groups must have a name") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace())) + { + return false; + } + + return true; + } + } + + public class ItemGroupIdValidator : PropertyValidator + { + public ItemGroupIdValidator() + : base("Groups must have an ID") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Quality == null && i.Id == 0)) + { + return false; + } + + return true; + } + } + + public class UniqueIdValidator : PropertyValidator + { + public UniqueIdValidator() + : base("Groups must have a unique ID") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1)) + { + return false; + } + + return true; + } + } + + public class UniqueQualityIdValidator : PropertyValidator + { + public UniqueQualityIdValidator() + : base("Qualities can only be used once") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + var qualityIds = new HashSet(); + + foreach (var item in items) + { + if (item.Id > 0) + { + foreach (var quality in item.Items) + { + if (qualityIds.Contains(quality.Quality.Id)) + { + return false; + } + + qualityIds.Add(quality.Quality.Id); + } + } + + else + { + if (qualityIds.Contains(item.Quality.Id)) + { + return false; + } + + qualityIds.Add(item.Quality.Id); + } + } + + return true; + } + } +} diff --git a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileModule.cs b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileModule.cs index 8eb5de017..ca64051ed 100644 --- a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileModule.cs +++ b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileModule.cs @@ -20,8 +20,10 @@ namespace Radarr.Api.V2.Profiles.Quality _profileService = profileService; _formatService = formatService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); + // TODO: Need to validate the cutoff is allowed and the ID/quality ID exists + // TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality + SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); + SharedValidator.RuleFor(c => c.Items).ValidItems(); SharedValidator.RuleFor(c => c.Language).ValidLanguage(); SharedValidator.RuleFor(c => c.FormatItems).Must(items => { diff --git a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileResource.cs b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileResource.cs index 667a0c0e8..0c7ddf6c6 100644 --- a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileResource.cs +++ b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileResource.cs @@ -11,18 +11,25 @@ namespace Radarr.Api.V2.Profiles.Quality public class QualityProfileResource : RestResource { public string Name { get; set; } - public NzbDrone.Core.Qualities.Quality Cutoff { get; set; } + public int Cutoff { get; set; } public string PreferredTags { get; set; } - public List Items { get; set; } + public List Items { get; set; } public CustomFormatResource FormatCutoff { get; set; } public List FormatItems { get; set; } public Language Language { get; set; } } - public class ProfileQualityItemResource : RestResource + public class QualityProfileQualityItemResource : RestResource { + public string Name { get; set; } public NzbDrone.Core.Qualities.Quality Quality { get; set; } + public List Items { get; set; } public bool Allowed { get; set; } + + public QualityProfileQualityItemResource() + { + Items = new List(); + } } public class ProfileFormatItemResource : RestResource @@ -40,7 +47,6 @@ namespace Radarr.Api.V2.Profiles.Quality return new QualityProfileResource { Id = model.Id, - Name = model.Name, Cutoff = model.Cutoff, PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "", @@ -51,13 +57,16 @@ namespace Radarr.Api.V2.Profiles.Quality }; } - public static ProfileQualityItemResource ToResource(this ProfileQualityItem model) + public static QualityProfileQualityItemResource ToResource(this ProfileQualityItem model) { if (model == null) return null; - return new ProfileQualityItemResource + return new QualityProfileQualityItemResource { + Id = model.Id, + Name = model.Name, Quality = model.Quality, + Items = model.Items.ConvertAll(ToResource), Allowed = model.Allowed }; } @@ -78,9 +87,8 @@ namespace Radarr.Api.V2.Profiles.Quality return new Profile { Id = resource.Id, - Name = resource.Name, - Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id, + Cutoff = resource.Cutoff, PreferredTags = resource.PreferredTags.Split(',').ToList(), Items = resource.Items.ConvertAll(ToModel), FormatCutoff = resource.FormatCutoff.ToModel(), @@ -89,13 +97,16 @@ namespace Radarr.Api.V2.Profiles.Quality }; } - public static ProfileQualityItem ToModel(this ProfileQualityItemResource resource) + public static ProfileQualityItem ToModel(this QualityProfileQualityItemResource resource) { if (resource == null) return null; return new ProfileQualityItem { - Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id, + Id = resource.Id, + Name = resource.Name, + Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null, + Items = resource.Items.ConvertAll(ToModel), Allowed = resource.Allowed }; } diff --git a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileSchemaModule.cs index d3db82508..8ae6eab9d 100644 --- a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileSchemaModule.cs +++ b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileSchemaModule.cs @@ -1,54 +1,25 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.Parser; using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; using Radarr.Http; -using Radarr.Http.Mapping; namespace Radarr.Api.V2.Profiles.Quality { public class QualityProfileSchemaModule : RadarrRestModule { - private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICustomFormatService _formatService; + private readonly IProfileService _profileService; - public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService, ICustomFormatService formatService) - : base("/profile/schema") + public QualityProfileSchemaModule(IProfileService profileService) + : base("/qualityprofile/schema") { - _qualityDefinitionService = qualityDefinitionService; - _formatService = formatService; + _profileService = profileService; - GetResourceAll = GetAll; + GetResourceSingle = GetSchema; } - private List GetAll() + private QualityProfileResource GetSchema() { - var items = _qualityDefinitionService.All() - .OrderBy(v => v.Weight) - .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) - .ToList(); + var qualityProfile = _profileService.GetDefaultProfile(string.Empty); - var formatItems = _formatService.All().Select(v => new ProfileFormatItem - { - Format = v, Allowed = true - }).ToList(); - - formatItems.Insert(0, new ProfileFormatItem - { - Format = CustomFormat.None, - Allowed = true - }); - - var profile = new Profile(); - profile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown; - profile.Items = items; - profile.FormatCutoff = CustomFormat.None; - profile.FormatItems = formatItems; - profile.Language = Language.English; - - return new List { profile.ToResource() }; + return qualityProfile.ToResource(); } } } diff --git a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileValidation.cs b/src/Radarr.Api.V2/Profiles/Quality/QualityProfileValidation.cs deleted file mode 100644 index 35f090837..000000000 --- a/src/Radarr.Api.V2/Profiles/Quality/QualityProfileValidation.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using FluentValidation.Validators; - -namespace Radarr.Api.V2.Profiles.Quality -{ - public static class QualityProfileValidation - { - public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - - return ruleBuilder.SetValidator(new AllowedValidator()); - } - } - - public class AllowedValidator : PropertyValidator - { - public AllowedValidator() - : base("Must contain at least one allowed quality") - { - - } - - protected override bool IsValid(PropertyValidatorContext context) - { - var list = context.PropertyValue as IList; - - if (list == null) - { - return false; - } - - if (!list.Any(c => c.Allowed)) - { - return false; - } - - return true; - } - } -} diff --git a/src/Radarr.Api.V2/Radarr.Api.V2.csproj b/src/Radarr.Api.V2/Radarr.Api.V2.csproj index f5f40a4d6..7f22a80fe 100644 --- a/src/Radarr.Api.V2/Radarr.Api.V2.csproj +++ b/src/Radarr.Api.V2/Radarr.Api.V2.csproj @@ -153,10 +153,11 @@ + + -