indexers
+ createTagsSelector(),
+ (indexers, tagList) => {
+ return {
+ ...indexers,
+ tagList
+ };
+ }
);
}
diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js
index 96b4f1ce2..d92e5ce4d 100644
--- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js
+++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js
@@ -22,6 +22,7 @@ function TagDetailsModalContent(props) {
notifications,
restrictions,
importLists,
+ indexers,
onModalClose,
onDeleteTagPress
} = props;
@@ -41,7 +42,7 @@ function TagDetailsModalContent(props) {
}
{
- !!movies.length &&
+ movies.length ?
+ :
+ null
}
{
- !!delayProfiles.length &&
+ delayProfiles.length ?
+ :
+ null
}
{
- !!notifications.length &&
+ notifications.length ?
+ :
+ null
}
{
- !!restrictions.length &&
+ restrictions.length ?
+ :
+ null
+ }
+
+ {
+ indexers.length ?
+ :
+ null
}
{
@@ -192,6 +213,7 @@ TagDetailsModalContent.propTypes = {
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
restrictions: PropTypes.arrayOf(PropTypes.object).isRequired,
importLists: PropTypes.arrayOf(PropTypes.object).isRequired,
+ indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired
};
diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js
index defb88635..0aa51a71c 100644
--- a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js
+++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js
@@ -69,6 +69,14 @@ function createMatchingImportListsSelector() {
);
}
+function createMatchingIndexersSelector() {
+ return createSelector(
+ (state, { indexerIds }) => indexerIds,
+ (state) => state.settings.indexers.items,
+ findMatchingItems
+ );
+}
+
function createMapStateToProps() {
return createSelector(
createMatchingMoviesSelector(),
@@ -76,13 +84,15 @@ function createMapStateToProps() {
createMatchingNotificationsSelector(),
createMatchingRestrictionsSelector(),
createMatchingImportListsSelector(),
- (movies, delayProfiles, notifications, restrictions, importLists) => {
+ createMatchingIndexersSelector(),
+ (movies, delayProfiles, notifications, restrictions, importLists, indexers) => {
return {
movies,
delayProfiles,
notifications,
restrictions,
- importLists
+ importLists,
+ indexers
};
}
);
diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js
index 5d4571ebd..d14c3b1bc 100644
--- a/frontend/src/Settings/Tags/Tag.js
+++ b/frontend/src/Settings/Tags/Tag.js
@@ -57,7 +57,8 @@ class Tag extends Component {
notificationIds,
restrictionIds,
importListIds,
- movieIds
+ movieIds,
+ indexerIds
} = this.props;
const {
@@ -70,7 +71,8 @@ class Tag extends Component {
notificationIds.length ||
restrictionIds.length ||
importListIds.length ||
- movieIds.length
+ movieIds.length ||
+ indexerIds.length
);
return (
@@ -120,6 +122,14 @@ class Tag extends Component {
{importListIds.length} list{importListIds.length > 1 && 's'}
}
+
+ {
+ indexerIds.length ?
+
+ {indexerIds.length} indexer{indexerIds.length > 1 && 's'}
+
:
+ null
+ }
}
@@ -138,6 +148,7 @@ class Tag extends Component {
notificationIds={notificationIds}
restrictionIds={restrictionIds}
importListIds={importListIds}
+ indexerIds={indexerIds}
isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress}
@@ -165,6 +176,7 @@ Tag.propTypes = {
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
+ indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired
};
@@ -173,7 +185,8 @@ Tag.defaultProps = {
notificationIds: [],
restrictionIds: [],
importListIds: [],
- movieIds: []
+ movieIds: [],
+ indexerIds: []
};
export default Tag;
diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js
index 88ffcd776..a3ed6b8c6 100644
--- a/frontend/src/Settings/Tags/TagsConnector.js
+++ b/frontend/src/Settings/Tags/TagsConnector.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { fetchDelayProfiles, fetchImportLists, fetchNotifications, fetchRestrictions } from 'Store/Actions/settingsActions';
+import { fetchDelayProfiles, fetchImportLists, fetchIndexers, fetchNotifications, fetchRestrictions } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags';
@@ -29,7 +29,8 @@ const mapDispatchToProps = {
dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchNotifications: fetchNotifications,
dispatchFetchRestrictions: fetchRestrictions,
- dispatchFetchImportLists: fetchImportLists
+ dispatchFetchImportLists: fetchImportLists,
+ dispatchFetchIndexers: fetchIndexers
};
class MetadatasConnector extends Component {
@@ -43,7 +44,8 @@ class MetadatasConnector extends Component {
dispatchFetchDelayProfiles,
dispatchFetchNotifications,
dispatchFetchRestrictions,
- dispatchFetchImportLists
+ dispatchFetchImportLists,
+ dispatchFetchIndexers
} = this.props;
dispatchFetchTagDetails();
@@ -51,6 +53,7 @@ class MetadatasConnector extends Component {
dispatchFetchNotifications();
dispatchFetchRestrictions();
dispatchFetchImportLists();
+ dispatchFetchIndexers();
}
//
@@ -70,7 +73,8 @@ MetadatasConnector.propTypes = {
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchRestrictions: PropTypes.func.isRequired,
- dispatchFetchImportLists: PropTypes.func.isRequired
+ dispatchFetchImportLists: PropTypes.func.isRequired,
+ dispatchFetchIndexers: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/IndexerTagSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/IndexerTagSpecificationFixture.cs
new file mode 100644
index 000000000..7ce13fde0
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/IndexerTagSpecificationFixture.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.IndexerSearch.Definitions;
+using NzbDrone.Core.Movies;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
+{
+ [TestFixture]
+ public class IndexerTagSpecificationFixture : CoreTest
+ {
+ private IndexerTagSpecification _specification;
+
+ private RemoteMovie _parseResultMulti;
+ private IndexerDefinition _fakeIndexerDefinition;
+ private Movie _fakeMovie;
+ private ReleaseInfo _fakeRelease;
+
+ [SetUp]
+ public void Setup()
+ {
+ _fakeIndexerDefinition = new IndexerDefinition
+ {
+ Tags = new HashSet()
+ };
+
+ Mocker
+ .GetMock()
+ .Setup(m => m.Get(It.IsAny()))
+ .Returns(_fakeIndexerDefinition);
+
+ _specification = Mocker.Resolve();
+
+ _fakeMovie = Builder.CreateNew()
+ .With(c => c.Monitored = true)
+ .With(c => c.Tags = new HashSet())
+ .Build();
+
+ _fakeRelease = new ReleaseInfo
+ {
+ IndexerId = 1
+ };
+
+ _parseResultMulti = new RemoteMovie
+ {
+ Movie = _fakeMovie,
+ Release = _fakeRelease
+ };
+ }
+
+ [Test]
+ public void indexer_and_movie_without_tags_should_return_true()
+ {
+ _fakeIndexerDefinition.Tags = new HashSet();
+ _fakeMovie.Tags = new HashSet();
+
+ _specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void indexer_with_tags_movie_without_tags_should_return_false()
+ {
+ _fakeIndexerDefinition.Tags = new HashSet { 123 };
+ _fakeMovie.Tags = new HashSet();
+
+ _specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void indexer_without_tags_movie_with_tags_should_return_true()
+ {
+ _fakeIndexerDefinition.Tags = new HashSet();
+ _fakeMovie.Tags = new HashSet { 123 };
+
+ _specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void indexer_with_tags_movie_with_matching_tags_should_return_true()
+ {
+ _fakeIndexerDefinition.Tags = new HashSet { 123, 456 };
+ _fakeMovie.Tags = new HashSet { 123, 789 };
+
+ _specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void indexer_with_tags_movie_with_different_tags_should_return_false()
+ {
+ _fakeIndexerDefinition.Tags = new HashSet { 456 };
+ _fakeMovie.Tags = new HashSet { 123, 789 };
+
+ _specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeFalse();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/Migration/198_add_indexer_tags.cs b/src/NzbDrone.Core/Datastore/Migration/198_add_indexer_tags.cs
new file mode 100644
index 000000000..a22563ee9
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/198_add_indexer_tags.cs
@@ -0,0 +1,14 @@
+using FluentMigrator;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(198)]
+ public class add_indexer_tags : NzbDroneMigrationBase
+ {
+ protected override void MainDbUpgrade()
+ {
+ Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs
index c8caccd23..097cc2442 100644
--- a/src/NzbDrone.Core/Datastore/TableMapping.cs
+++ b/src/NzbDrone.Core/Datastore/TableMapping.cs
@@ -73,8 +73,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.Enable)
.Ignore(i => i.Protocol)
.Ignore(i => i.SupportsRss)
- .Ignore(i => i.SupportsSearch)
- .Ignore(d => d.Tags);
+ .Ignore(i => i.SupportsSearch);
Mapper.Entity("ImportLists").RegisterModel()
.Ignore(x => x.ImplementationName)
diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs
new file mode 100644
index 000000000..cb5d6b508
--- /dev/null
+++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs
@@ -0,0 +1,39 @@
+using System.Linq;
+using NLog;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.IndexerSearch.Definitions;
+using NzbDrone.Core.Parser.Model;
+
+namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
+{
+ public class IndexerTagSpecification : IDecisionEngineSpecification
+ {
+ private readonly Logger _logger;
+ private readonly IIndexerRepository _indexerRepository;
+
+ public IndexerTagSpecification(Logger logger, IIndexerRepository indexerRepository)
+ {
+ _logger = logger;
+ _indexerRepository = indexerRepository;
+ }
+
+ public SpecificationPriority Priority => SpecificationPriority.Default;
+ public RejectionType Type => RejectionType.Permanent;
+
+ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
+ {
+ // If indexer has tags, check that at least one of them is present on the series
+ var indexerTags = _indexerRepository.Get(subject.Release.IndexerId).Tags;
+
+ if (indexerTags.Any() && indexerTags.Intersect(subject.Movie.Tags).Empty())
+ {
+ _logger.Debug("Indexer {0} has tags. None of these are present on movie {1}. Rejecting", subject.Release.Indexer, subject.Movie);
+
+ return Decision.Reject("Movie tags do not match any of the indexer tags");
+ }
+
+ return Decision.Accept();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
index fbf958f5f..f4ed2715e 100644
--- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
+++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
@@ -102,6 +102,9 @@ namespace NzbDrone.Core.IndexerSearch
_indexerFactory.InteractiveSearchEnabled() :
_indexerFactory.AutomaticSearchEnabled();
+ // Filter indexers to untagged indexers and indexers with intersecting tags
+ indexers = indexers.Where(i => i.Definition.Tags.Empty() || i.Definition.Tags.Intersect(criteriaBase.Movie.Tags).Any()).ToList();
+
var reports = new List();
_logger.ProgressInfo("Searching indexers for {0}. {1} active indexers", criteriaBase, indexers.Count);
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 3b4b233c7..11ca6fa2f 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -433,6 +433,7 @@
"IndexersSettingsSummary": "Indexers and release restrictions",
"IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures",
"IndexerStatusCheckSingleClientMessage": "Indexers unavailable due to failures: {0}",
+ "IndexerTagHelpText": "Only use this indexer for series with at least one matching tag. Leave blank to use with all movies.",
"Info": "Info",
"InstallLatest": "Install Latest",
"InteractiveImport": "Interactive Import",
diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs
index 16c262176..57a66ff37 100644
--- a/src/NzbDrone.Core/Tags/TagDetails.cs
+++ b/src/NzbDrone.Core/Tags/TagDetails.cs
@@ -12,12 +12,13 @@ namespace NzbDrone.Core.Tags
public List RestrictionIds { get; set; }
public List ImportListIds { get; set; }
public List DelayProfileIds { get; set; }
+ public List IndexerIds { get; set; }
public bool InUse
{
get
{
- return MovieIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any();
+ return MovieIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || IndexerIds.Any();
}
}
}
diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs
index a6569c87e..9bc9a174e 100644
--- a/src/NzbDrone.Core/Tags/TagService.cs
+++ b/src/NzbDrone.Core/Tags/TagService.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.ImportLists;
+using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Notifications;
@@ -32,6 +33,7 @@ namespace NzbDrone.Core.Tags
private readonly INotificationFactory _notificationFactory;
private readonly IRestrictionService _restrictionService;
private readonly IMovieService _movieService;
+ private readonly IIndexerFactory _indexerService;
public TagService(ITagRepository repo,
IEventAggregator eventAggregator,
@@ -39,7 +41,8 @@ namespace NzbDrone.Core.Tags
IImportListFactory importListFactory,
INotificationFactory notificationFactory,
IRestrictionService restrictionService,
- IMovieService movieService)
+ IMovieService movieService,
+ IIndexerFactory indexerService)
{
_repo = repo;
_eventAggregator = eventAggregator;
@@ -48,6 +51,7 @@ namespace NzbDrone.Core.Tags
_notificationFactory = notificationFactory;
_restrictionService = restrictionService;
_movieService = movieService;
+ _indexerService = indexerService;
}
public Tag GetTag(int tagId)
@@ -80,6 +84,7 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.AllForTag(tagId);
var restrictions = _restrictionService.AllForTag(tagId);
var movies = _movieService.AllMovieTags().Where(x => x.Value.Contains(tagId)).Select(x => x.Key).ToList();
+ var indexers = _indexerService.AllForTag(tagId);
return new TagDetails
{
@@ -89,7 +94,8 @@ namespace NzbDrone.Core.Tags
ImportListIds = importLists.Select(c => c.Id).ToList(),
NotificationIds = notifications.Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
- MovieIds = movies
+ MovieIds = movies,
+ IndexerIds = indexers.Select(c => c.Id).ToList()
};
}
@@ -101,6 +107,7 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.All();
var restrictions = _restrictionService.All();
var movies = _movieService.AllMovieTags();
+ var indexers = _indexerService.All();
var details = new List();
@@ -114,7 +121,8 @@ namespace NzbDrone.Core.Tags
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
- MovieIds = movies.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList()
+ MovieIds = movies.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(),
+ IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
});
}
diff --git a/src/Radarr.Api.V3/Tags/TagDetailsResource.cs b/src/Radarr.Api.V3/Tags/TagDetailsResource.cs
index 1d9dfbf19..7f61a3470 100644
--- a/src/Radarr.Api.V3/Tags/TagDetailsResource.cs
+++ b/src/Radarr.Api.V3/Tags/TagDetailsResource.cs
@@ -13,6 +13,7 @@ namespace Radarr.Api.V3.Tags
public List RestrictionIds { get; set; }
public List ImportListIds { get; set; }
public List MovieIds { get; set; }
+ public List IndexerIds { get; set; }
}
public static class TagDetailsResourceMapper
@@ -32,7 +33,8 @@ namespace Radarr.Api.V3.Tags
NotificationIds = model.NotificationIds,
RestrictionIds = model.RestrictionIds,
ImportListIds = model.ImportListIds,
- MovieIds = model.MovieIds
+ MovieIds = model.MovieIds,
+ IndexerIds = model.IndexerIds
};
}