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 e5dceaf5d..57e64f153 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -9,6 +9,7 @@ import CheckInput from './CheckInput'; import DeviceInputConnector from './DeviceInputConnector'; import EnhancedSelectInput from './EnhancedSelectInput'; import FormInputHelpText from './FormInputHelpText'; +import IndexerSelectInputConnector from './IndexerSelectInputConnector'; import KeyValueListInput from './KeyValueListInput'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput'; @@ -69,6 +70,9 @@ function getComponent(type) { case inputTypes.ALBUM_RELEASE_SELECT: return AlbumReleaseSelectInputConnector; + 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..838107286 --- /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 { fetchIndexers } from 'Store/Actions/settingsActions'; +import sortByName from 'Utilities/Array/sortByName'; +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 1b7f06049..f5dacc0a3 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 7c0e9ae73..41caef44b 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 172ca331c..9edea708d 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -12,6 +12,7 @@ export const PATH = 'path'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const METADATA_PROFILE_SELECT = 'metadataProfileSelect'; export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect'; +export const INDEXER_SELECT = 'indexerSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const SELECT = 'select'; export const SERIES_TYPE_SELECT = 'artistTypeSelect'; @@ -34,6 +35,7 @@ export const all = [ QUALITY_PROFILE_SELECT, METADATA_PROFILE_SELECT, ALBUM_RELEASE_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 8a336849a..b462859cb 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 ee589c026..7c9177194 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js @@ -8,11 +8,13 @@ import selectSettings from 'Store/Selectors/selectSettings'; 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 a8edc38fa..86901f8ee 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 Card from 'Components/Card'; @@ -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 ( +
+ { + preferred.map((item) => { + const isPreferred = item.value >= 0; + + return ( + + ); + }) + } +
+
{ split(ignored).map((item) => { @@ -111,28 +134,33 @@ class ReleaseProfile extends Component { }
-
- { - preferred.map((item) => { - const isPreferred = item.value >= 0; - - return ( - - ); - }) - } -
- +
+ { + !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 148d2826c..9f17109e9 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 { deleteReleaseProfile, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; +import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } 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/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs index 7ef4424db..9718c6728 100644 --- a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs +++ b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using FluentValidation; using Lidarr.Http; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Releases; namespace Lidarr.Api.V1.Profiles.Release @@ -9,10 +10,12 @@ namespace Lidarr.Api.V1.Profiles.Release public class ReleaseProfileModule : LidarrRestModule { private readonly IReleaseProfileService _releaseProfileService; + private readonly IIndexerFactory _indexerFactory; - public ReleaseProfileModule(IReleaseProfileService releaseProfileService) + public ReleaseProfileModule(IReleaseProfileService releaseProfileService, IIndexerFactory indexerFactory) { _releaseProfileService = releaseProfileService; + _indexerFactory = indexerFactory; GetResourceById = GetById; GetResourceAll = GetAll; @@ -26,6 +29,11 @@ namespace Lidarr.Api.V1.Profiles.Release { context.AddFailure("Either 'Must contain' or 'Must not contain' 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/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs index c57c0d07f..203186fa5 100644 --- a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs +++ b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs @@ -7,10 +7,12 @@ namespace Lidarr.Api.V1.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() @@ -32,10 +34,12 @@ namespace Lidarr.Api.V1.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) }; } @@ -51,10 +55,12 @@ namespace Lidarr.Api.V1.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) }; } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs index 4d663038c..f54fafa08 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(_remoteAlbum, null).Accepted.Should().BeTrue(); @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _remoteAlbum.Release.Title = "[ www.Speed.cd ] - Katy Perry - Witness (2017) MP3 [320 kbps] "; Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(new List { new ReleaseProfile { Required = "320", 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 77f0e7e41..a93187f9f 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); } @@ -51,10 +51,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(_artist, _title).Should().Be(0); + Subject.Calculate(_artist, _title, 0).Should().Be(0); } [Test] @@ -62,7 +62,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService { GivenMatchingTerms(); - Subject.Calculate(_artist, _title).Should().Be(0); + Subject.Calculate(_artist, _title, 0).Should().Be(0); } [Test] @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService { GivenMatchingTerms("24bit"); - Subject.Calculate(_artist, _title).Should().Be(5); + Subject.Calculate(_artist, _title, 0).Should().Be(5); } [Test] @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService { GivenMatchingTerms("16bit"); - Subject.Calculate(_artist, _title).Should().Be(-10); + Subject.Calculate(_artist, _title, 0).Should().Be(-10); } [Test] @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService GivenMatchingTerms("24bit"); - Subject.Calculate(_artist, _title).Should().Be(10); + Subject.Calculate(_artist, _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 5353836b6..677f0193d 100644 --- a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService private void GivenReleaseProfile() { Mocker.GetMock() - .Setup(s => s.AllForTags(It.IsAny>())) + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) .Returns(_releaseProfiles); } @@ -51,7 +51,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(_artist, _title).Should().BeEmpty(); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 8367acf05..02e16f2ab 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); TModel Insert(TModel model); TModel Update(TModel model); @@ -87,10 +88,17 @@ namespace NzbDrone.Core.Datastore return Query(Builder()); } - public TModel Get(int id) + public TModel Find(int id) { var model = Query(x => x.Id == id).FirstOrDefault(); + 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/041_add_indexer_and_enabled_release_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/041_add_indexer_and_enabled_release_profiles.cs new file mode 100644 index 000000000..cf3a8aa08 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/041_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(041)] + 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 9dbd13506..4bd5c7089 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (!_upgradableSpecification.CutoffNotMet(qualityProfile, currentQualities, - _preferredWordServiceCalculator.Calculate(subject.Artist, trackFiles[0].GetSceneOrFileName()), + _preferredWordServiceCalculator.Calculate(subject.Artist, trackFiles[0].GetSceneOrFileName(), subject.Release.IndexerId), subject.ParsedAlbumInfo.Quality, subject.PreferredWordScore)) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index e512faffb..f904f8933 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteAlbum.ParsedAlbumInfo.Quality); - var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Artist, queueItem.Title); + var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Artist, queueItem.Title, subject.Release?.IndexerId ?? 0); if (!_upgradableSpecification.CutoffNotMet(qualityProfile, new List { remoteAlbum.ParsedAlbumInfo.Quality }, diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 025d9bcad..e134699a8 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.Artist.Tags); + var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Artist.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 65cac72f3..907f3efdf 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -62,7 +62,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync // The artist will be the same as the one in history since it's the same album. // Instead of fetching the artist from the DB reuse the known artist. - var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Artist, mostRecent.SourceTitle); + var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Artist, mostRecent.SourceTitle, subject.Release?.IndexerId ?? 0); var cutoffUnmet = _upgradableSpecification.CutoffNotMet( subject.Artist.QualityProfile, diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 6f0d485b4..44c4737bb 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (!_upgradableSpecification.IsUpgradable(subject.Artist.QualityProfile, currentQualities, - _preferredWordServiceCalculator.Calculate(subject.Artist, trackFiles[0].GetSceneOrFileName()), + _preferredWordServiceCalculator.Calculate(subject.Artist, trackFiles[0].GetSceneOrFileName(), subject.Release?.IndexerId ?? 0), subject.ParsedAlbumInfo.Quality, subject.PreferredWordScore)) { diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs index cf41c32bc..3e1c859d9 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 RemoteAlbum Aggregate(RemoteAlbum remoteAlbum) { - remoteAlbum.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteAlbum.Artist, remoteAlbum.Release.Title); + remoteAlbum.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteAlbum.Artist, remoteAlbum.Release.Title, remoteAlbum.Release.IndexerId); return remoteAlbum; } diff --git a/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs index 9a60d5eb9..24207d13b 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(Artist artist, string title); + int Calculate(Artist artist, string title, int indexerId); List GetMatchingPreferredWords(Artist artist, string title); } @@ -25,11 +25,11 @@ namespace NzbDrone.Core.Profiles.Releases _logger = logger; } - public int Calculate(Artist series, string title) + public int Calculate(Artist artist, string title, int indexerId) { _logger.Trace("Calculating preferred word score for '{0}'", title); - var releaseProfiles = _releaseProfileService.AllForTags(series.Tags); + var releaseProfiles = _releaseProfileService.EnabledForTags(artist.Tags, indexerId); var matchingPairs = new List>(); foreach (var releaseProfile in releaseProfiles) @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Profiles.Releases public List GetMatchingPreferredWords(Artist artist, string title) { - var releaseProfiles = _releaseProfileService.AllForTags(artist.Tags); + var releaseProfiles = _releaseProfileService.EnabledForTags(artist.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 a2001fb8b..308299cb1 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 b37aefda9..9cbf46615 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);