From 99728a604d836a1f8381b36544828f915d973148 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 5 Jun 2019 20:54:59 -0500 Subject: [PATCH] New: Added option to filter Release Profile to a specific indexer --- .../Components/Form/EnhancedSelectInput.css | 11 -- .../src/Components/Form/FormInputGroup.js | 4 + .../Form/IndexerSelectInputConnector.js | 102 ++++++++++++++++++ .../src/Components/Form/KeyValueListInput.js | 8 +- frontend/src/Components/Form/TagInput.css | 8 ++ frontend/src/Components/Form/TagInput.js | 6 +- frontend/src/Helpers/Props/inputTypes.js | 2 + .../Release/EditReleaseProfileModalContent.js | 32 +++++- ...EditReleaseProfileModalContentConnector.js | 4 +- .../Profiles/Release/ReleaseProfile.js | 61 ++++++++--- .../Profiles/Release/ReleaseProfiles.js | 3 + .../Release/ReleaseProfilesConnector.js | 17 ++- ...ReleaseRestrictionsSpecificationFixture.cs | 6 +- .../PreferredWordService/CalculateFixture.cs | 14 +-- .../GetMatchingPreferredWordsFixture.cs | 7 +- .../Datastore/BasicRepository.cs | 10 +- ...dd_indexer_and_enabled_release_profiles.cs | 15 +++ .../Specifications/CutoffSpecification.cs | 2 +- .../Specifications/QueueSpecification.cs | 2 +- .../ReleaseRestrictionsSpecification.cs | 6 +- .../RssSync/HistorySpecification.cs | 2 +- .../UpgradeDiskSpecification.cs | 2 +- .../AggregatePreferredWordScore.cs | 2 +- .../Profiles/Releases/PreferredWordService.cs | 8 +- .../Profiles/Releases/ReleaseProfile.cs | 4 + .../Releases/ReleaseProfileService.cs | 8 ++ .../ThingiProvider/IProviderFactory.cs | 1 + .../ThingiProvider/ProviderFactory.cs | 5 + .../Profiles/Release/ReleaseProfileModule.cs | 10 +- .../Release/ReleaseProfileResource.cs | 6 ++ 30 files changed, 305 insertions(+), 63 deletions(-) create mode 100644 frontend/src/Components/Form/IndexerSelectInputConnector.js create mode 100644 src/NzbDrone.Core/Datastore/Migration/136_add_indexer_and_enabled_release_profiles.cs diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index 774a63517..41456c9cf 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -1,19 +1,8 @@ .enhancedSelect { composes: input from '~Components/Form/Input.css'; - composes: link from '~Components/Link/Link.css'; - position: relative; display: flex; align-items: center; - padding: 6px 16px; - width: 100%; - height: 35px; - border: 1px solid $inputBorderColor; - border-radius: 4px; - background-color: $white; - box-shadow: inset 0 1px 1px $inputBoxShadowColor; - color: $black; - cursor: default; } .hasError { diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 87f39bab3..393a6b0ba 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -14,6 +14,7 @@ import PasswordInput from './PasswordInput'; import PathInputConnector from './PathInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector'; +import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; import SeriesTypeSelectInput from './SeriesTypeSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput'; @@ -61,6 +62,9 @@ function getComponent(type) { case inputTypes.LANGUAGE_PROFILE_SELECT: return LanguageProfileSelectInputConnector; + case inputTypes.INDEXER_SELECT: + return IndexerSelectInputConnector; + case inputTypes.ROOT_FOLDER_SELECT: return RootFolderSelectInputConnector; diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js new file mode 100644 index 000000000..72de50f3a --- /dev/null +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -0,0 +1,102 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import sortByName from 'Utilities/Array/sortByName'; +import { fetchIndexers } from 'Store/Actions/settingsActions'; +import EnhancedSelectInput from './EnhancedSelectInput'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.indexers, + (state, { includeAny }) => includeAny, + (indexers, includeAny) => { + const { + isFetching, + isPopulated, + error, + items + } = indexers; + + const values = _.map(items.sort(sortByName), (indexer) => { + return { + key: indexer.id, + value: indexer.name + }; + }); + + if (includeAny) { + values.unshift({ + key: 0, + value: '(Any)' + }); + } + + return { + isFetching, + isPopulated, + error, + values + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchIndexers: fetchIndexers +}; + +class IndexerSelectInputConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.dispatchFetchIndexers(); + } + + const { + name, + value, + values + } = this.props; + } + + // + // Listeners + + onChange = ({ name, value }) => { + this.props.onChange({ name, value: parseInt(value) }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +IndexerSelectInputConnector.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + values: PropTypes.arrayOf(PropTypes.object).isRequired, + includeAny: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + dispatchFetchIndexers: PropTypes.func.isRequired +}; + +IndexerSelectInputConnector.defaultProps = { + includeAny: false +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector); diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js index a52c76f70..a0f64d444 100644 --- a/frontend/src/Components/Form/KeyValueListInput.js +++ b/frontend/src/Components/Form/KeyValueListInput.js @@ -98,7 +98,9 @@ class KeyValueListInput extends Component { className, value, keyPlaceholder, - valuePlaceholder + valuePlaceholder, + hasError, + hasWarning } = this.props; const { isFocused } = this.state; @@ -106,7 +108,9 @@ class KeyValueListInput extends Component { return (
{ diff --git a/frontend/src/Components/Form/TagInput.css b/frontend/src/Components/Form/TagInput.css index 1516bfb1d..9328c762a 100644 --- a/frontend/src/Components/Form/TagInput.css +++ b/frontend/src/Components/Form/TagInput.css @@ -12,6 +12,14 @@ } } +.hasError { + composes: hasError from '~Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from '~Components/Form/Input.css'; +} + .internalInput { flex: 1 1 0%; margin-left: 3px; diff --git a/frontend/src/Components/Form/TagInput.js b/frontend/src/Components/Form/TagInput.js index 45d972631..c66b3f3e4 100644 --- a/frontend/src/Components/Form/TagInput.js +++ b/frontend/src/Components/Form/TagInput.js @@ -210,6 +210,8 @@ class TagInput extends Component { const { className, inputContainerClassName, + hasError, + hasWarning, ...otherProps } = this.props; @@ -226,7 +228,9 @@ class TagInput extends Component { className={styles.internalInput} inputContainerClassName={classNames( inputContainerClassName, - isFocused && styles.isFocused + isFocused && styles.isFocused, + hasError && styles.hasError, + hasWarning && styles.hasWarning )} value={value} suggestions={suggestions} diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index 78a8aa1af..dcdd3698a 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -10,6 +10,7 @@ export const PASSWORD = 'password'; export const PATH = 'path'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect'; +export const INDEXER_SELECT = 'indexerSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const SELECT = 'select'; export const SERIES_TYPE_SELECT = 'seriesTypeSelect'; @@ -30,6 +31,7 @@ export const all = [ PATH, QUALITY_PROFILE_SELECT, LANGUAGE_PROFILE_SELECT, + INDEXER_SELECT, ROOT_FOLDER_SELECT, SELECT, SERIES_TYPE_SELECT, diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js index 773ea7815..ea5cb8f23 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js @@ -30,11 +30,13 @@ function EditReleaseProfileModalContent(props) { const { id, + enabled, required, ignored, preferred, includePreferredWhenRenaming, - tags + tags, + indexerId } = item; return ( @@ -45,6 +47,18 @@ function EditReleaseProfileModalContent(props) {
+ + Enable Profile + + + + Must Contain @@ -99,9 +113,23 @@ function EditReleaseProfileModalContent(props) { + + + + Indexer + + diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js index 447bea3c7..c9de8f735 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js @@ -8,11 +8,13 @@ import { setReleaseProfileValue, saveReleaseProfile } from 'Store/Actions/settin import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; const newReleaseProfile = { + enabled: true, required: '', ignored: '', preferred: [], includePreferredWhenRenaming: false, - tags: [] + tags: [], + indexerId: 0 }; function createMapStateToProps() { diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js index f1b03a68f..3c78a5da4 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import split from 'Utilities/String/split'; @@ -55,11 +56,14 @@ class ReleaseProfile extends Component { render() { const { id, + enabled, required, ignored, preferred, tags, - tagList + indexerId, + tagList, + indexerList } = this.props; const { @@ -67,6 +71,8 @@ class ReleaseProfile extends Component { isDeleteReleaseProfileModalOpen } = this.state; + const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId }); + return ( { - split(ignored).map((item) => { - if (!item) { - return null; - } + preferred.map((item) => { + const isPreferred = item.value >= 0; return ( ); }) @@ -113,15 +117,17 @@ class ReleaseProfile extends Component {
{ - preferred.map((item) => { - const isPreferred = item.value >= 0; + split(ignored).map((item) => { + if (!item) { + return null; + } return ( ); }) @@ -133,6 +139,28 @@ class ReleaseProfile extends Component { tagList={tagList} /> +
+ { + !enabled && + + } + + { + indexer && + + } +
+ @@ -92,6 +94,7 @@ ReleaseProfiles.propTypes = { error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, onConfirmDeleteReleaseProfile: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js b/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js index dd4b41171..3cca69efd 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js @@ -2,24 +2,28 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { fetchReleaseProfiles, deleteReleaseProfile } from 'Store/Actions/settingsActions'; +import { fetchReleaseProfiles, deleteReleaseProfile, fetchIndexers } from 'Store/Actions/settingsActions'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; import ReleaseProfiles from './ReleaseProfiles'; function createMapStateToProps() { return createSelector( (state) => state.settings.releaseProfiles, + (state) => state.settings.indexers, createTagsSelector(), - (releaseProfiles, tagList) => { + (releaseProfiles, indexers, tagList) => { return { ...releaseProfiles, - tagList + tagList, + isIndexersPopulated: indexers.isPopulated, + indexerList: indexers.items }; } ); } const mapDispatchToProps = { + fetchIndexers, fetchReleaseProfiles, deleteReleaseProfile }; @@ -31,6 +35,9 @@ class ReleaseProfilesConnector extends Component { componentDidMount() { this.props.fetchReleaseProfiles(); + if (!this.props.isIndexersPopulated) { + this.props.fetchIndexers(); + } } // @@ -54,8 +61,10 @@ class ReleaseProfilesConnector extends Component { } ReleaseProfilesConnector.propTypes = { + isIndexersPopulated: PropTypes.bool.isRequired, fetchReleaseProfiles: PropTypes.func.isRequired, - deleteReleaseProfile: PropTypes.func.isRequired + deleteReleaseProfile: PropTypes.func.isRequired, + fetchIndexers: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs index d551e2853..208782c6a 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenRestictions(string required, string ignored) { Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(new List { new ReleaseProfile() @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_be_true_when_restrictions_are_empty() { Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(new List()); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _remoteEpisode.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV"; Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(new List { new ReleaseProfile { Required = "x264", Ignored = "www.Speed.cd" } diff --git a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs index 55e7cf476..45af69cd6 100644 --- a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService }); Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(_releaseProfiles); } @@ -52,10 +52,10 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService public void should_return_0_when_there_are_no_release_profiles() { Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(new List()); - Subject.Calculate(_series, _title).Should().Be(0); + Subject.Calculate(_series, _title, 0).Should().Be(0); } [Test] @@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService { GivenMatchingTerms(); - Subject.Calculate(_series, _title).Should().Be(0); + Subject.Calculate(_series, _title, 0).Should().Be(0); } [Test] @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService { GivenMatchingTerms("x264"); - Subject.Calculate(_series, _title).Should().Be(5); + Subject.Calculate(_series, _title, 0).Should().Be(5); } [Test] @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService { GivenMatchingTerms("x265"); - Subject.Calculate(_series, _title).Should().Be(-10); + Subject.Calculate(_series, _title, 0).Should().Be(-10); } [Test] @@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService GivenMatchingTerms("x264"); - Subject.Calculate(_series, _title).Should().Be(10); + Subject.Calculate(_series, _title, 0).Should().Be(10); } } } diff --git a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs index bb91630ae..d60713b5a 100644 --- a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs @@ -40,12 +40,11 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService .Setup(s => s.MatchingTerm(It.IsAny(), _title)) .Returns((term, title) => title.Contains(term) ? term : null); } - - + private void GivenReleaseProfile() { Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(_releaseProfiles); } @@ -53,7 +52,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService public void should_return_empty_list_when_there_are_no_release_profiles() { Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(new List()); Subject.GetMatchingPreferredWords(_series, _title).Should().BeEmpty(); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 3cf47611b..38f8d5c16 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Datastore { IEnumerable All(); int Count(); + TModel Find(int id); TModel Get(int id); IEnumerable Get(IEnumerable ids); TModel SingleOrDefault(); @@ -65,10 +66,17 @@ namespace NzbDrone.Core.Datastore return DataMapper.Query().GetRowCount(); } - public TModel Get(int id) + public TModel Find(int id) { var model = Query.Where(c => c.Id == id).SingleOrDefault(); + return model; + } + + public TModel Get(int id) + { + var model = Find(id); + if (model == null) { throw new ModelNotFoundException(typeof(TModel), id); diff --git a/src/NzbDrone.Core/Datastore/Migration/136_add_indexer_and_enabled_release_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/136_add_indexer_and_enabled_release_profiles.cs new file mode 100644 index 000000000..a620046c2 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/136_add_indexer_and_enabled_release_profiles.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(136)] + public class add_indexer_and_enabled_to_release_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("ReleaseProfiles").AddColumn("Enabled").AsBoolean().WithDefaultValue(true); + Alter.Table("ReleaseProfiles").AddColumn("IndexerId").AsInt32().WithDefaultValue(0); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index 71e8408a6..0804ed690 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications languageProfile, file.Quality, file.Language, - _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), + _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName(), subject.Release.IndexerId), subject.ParsedEpisodeInfo.Quality, subject.PreferredWordScore)) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index e32745a22..0a3f8776e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } _logger.Debug("Checking if existing release in queue meets cutoff. Queued: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language); - var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title); + var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title, subject.Release?.IndexerId ?? 0); if (!_upgradableSpecification.CutoffNotMet(qualityProfile, languageProfile, diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 743e68e20..e2bed10c5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -30,10 +30,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Checking if release meets restrictions: {0}", subject); var title = subject.Release.Title; - var restrictions = _releaseProfileService.AllForTags(subject.Series.Tags); + var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Series.Tags, subject.Release.IndexerId); - var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace()); - var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); + var required = releaseProfiles.Where(r => r.Required.IsNotNullOrWhiteSpace()); + var ignored = releaseProfiles.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); foreach (var r in required) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 0567984f6..51ee3cb5b 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync // The series will be the same as the one in history since it's the same episode. // Instead of fetching the series from the DB reuse the known series. - var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, mostRecent.SourceTitle); + var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, mostRecent.SourceTitle, subject.Release?.IndexerId ?? 0); var cutoffUnmet = _upgradableSpecification.CutoffNotMet( subject.Series.QualityProfile, diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index caf4fbfdb..2db74c886 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications subject.Series.LanguageProfile, file.Quality, file.Language, - _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), + _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName(), subject.Release?.IndexerId ?? 0), subject.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Language, subject.PreferredWordScore)) diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs index 6227ca1e6..5d0a58dd1 100644 --- a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download.Aggregation.Aggregators public RemoteEpisode Aggregate(RemoteEpisode remoteEpisode) { - remoteEpisode.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteEpisode.Series, remoteEpisode.Release.Title); + remoteEpisode.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteEpisode.Series, remoteEpisode.Release.Title, remoteEpisode.Release.IndexerId); return remoteEpisode; } diff --git a/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs index 18ca1c9e5..bee590510 100644 --- a/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs +++ b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Profiles.Releases { public interface IPreferredWordService { - int Calculate(Series series, string title); + int Calculate(Series series, string title, int indexerId); List GetMatchingPreferredWords(Series series, string title); } @@ -25,11 +25,11 @@ namespace NzbDrone.Core.Profiles.Releases _logger = logger; } - public int Calculate(Series series, string title) + public int Calculate(Series series, string title, int indexerId) { _logger.Trace("Calculating preferred word score for '{0}'", title); - var releaseProfiles = _releaseProfileService.AllForTags(series.Tags); + var releaseProfiles = _releaseProfileService.EnabledForTags(series.Tags, indexerId); var matchingPairs = new List>(); foreach (var releaseProfile in releaseProfiles) @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Profiles.Releases public List GetMatchingPreferredWords(Series series, string title) { - var releaseProfiles = _releaseProfileService.AllForTags(series.Tags); + var releaseProfiles = _releaseProfileService.EnabledForTags(series.Tags, 0); var matchingPairs = new List>(); _logger.Trace("Calculating preferred word score for '{0}'", title); diff --git a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs index e642ce094..c8d6433c0 100644 --- a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs +++ b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs @@ -5,17 +5,21 @@ namespace NzbDrone.Core.Profiles.Releases { public class ReleaseProfile : ModelBase { + public bool Enabled { get; set; } public string Required { get; set; } public string Ignored { get; set; } public List> Preferred { get; set; } public bool IncludePreferredWhenRenaming { get; set; } + public int IndexerId { get; set; } public HashSet Tags { get; set; } public ReleaseProfile() { + Enabled = true; Preferred = new List>(); IncludePreferredWhenRenaming = true; Tags = new HashSet(); + IndexerId = 0; } } diff --git a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs index 4d79de81f..84c3e04c7 100644 --- a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Profiles.Releases List All(); List AllForTag(int tagId); List AllForTags(HashSet tagIds); + List EnabledForTags(HashSet tagIds, int indexerId); ReleaseProfile Get(int id); void Delete(int id); ReleaseProfile Add(ReleaseProfile restriction); @@ -48,6 +49,13 @@ namespace NzbDrone.Core.Profiles.Releases return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); } + public List EnabledForTags(HashSet tagIds, int indexerId) + { + return AllForTags(tagIds) + .Where(r => r.Enabled) + .Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList(); + } + public ReleaseProfile Get(int id) { return _repo.Get(id); diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs index 2627cec14..14c47d6e3 100644 --- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.ThingiProvider { List All(); List GetAvailableProviders(); + bool Exists(int id); TProviderDefinition Get(int id); TProviderDefinition Create(TProviderDefinition definition); void Update(TProviderDefinition definition); diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index db98416d3..58eb38479 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -91,6 +91,11 @@ namespace NzbDrone.Core.ThingiProvider return Active().Select(GetInstance).ToList(); } + public bool Exists(int id) + { + return _providerRepository.Find(id) != null; + } + public TProviderDefinition Get(int id) { return _providerRepository.Get(id); diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs index 9e61585c4..1d6c75ab0 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FluentValidation; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Releases; using Sonarr.Http; @@ -10,11 +11,13 @@ namespace Sonarr.Api.V3.Profiles.Release public class ReleaseProfileModule : SonarrRestModule { private readonly IReleaseProfileService _releaseProfileService; + private readonly IIndexerFactory _indexerFactory; - public ReleaseProfileModule(IReleaseProfileService releaseProfileService) + public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory) { _releaseProfileService = releaseProfileService; + _indexerFactory = indexerFactory; GetResourceById = GetReleaseProfile; GetResourceAll = GetAll; @@ -28,6 +31,11 @@ namespace Sonarr.Api.V3.Profiles.Release { context.AddFailure("'Must contain', 'Must not contain' or 'Preferred' is required"); } + + if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId)) + { + context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist"); + } }); } diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs index 28a7e17b9..55575781f 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs @@ -7,10 +7,12 @@ namespace Sonarr.Api.V3.Profiles.Release { public class ReleaseProfileResource : RestResource { + public bool Enabled { get; set; } public string Required { get; set; } public string Ignored { get; set; } public List> Preferred { get; set; } public bool IncludePreferredWhenRenaming { get; set; } + public int IndexerId { get; set; } public HashSet Tags { get; set; } public ReleaseProfileResource() @@ -29,10 +31,12 @@ namespace Sonarr.Api.V3.Profiles.Release { Id = model.Id, + Enabled = model.Enabled, Required = model.Required, Ignored = model.Ignored, Preferred = model.Preferred, IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming, + IndexerId = model.IndexerId, Tags = new HashSet(model.Tags) }; } @@ -45,10 +49,12 @@ namespace Sonarr.Api.V3.Profiles.Release { Id = resource.Id, + Enabled = resource.Enabled, Required = resource.Required, Ignored = resource.Ignored, Preferred = resource.Preferred, IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming, + IndexerId = resource.IndexerId, Tags = new HashSet(resource.Tags) }; }