From b97d63cb5bd052d2f508a9dbad0dce1541562266 Mon Sep 17 00:00:00 2001 From: 6cUbi57z Date: Mon, 22 Mar 2021 00:00:06 +0000 Subject: [PATCH] New: Add tag support to indexers (cherry picked from commit c3d54b312ef18b837d54605ea78f1a263fd6900b) --- .../Indexers/EditIndexerModalContent.js | 16 ++ .../src/Settings/Indexers/Indexers/Indexer.js | 10 ++ .../Settings/Indexers/Indexers/Indexers.js | 3 + .../Indexers/Indexers/IndexersConnector.js | 9 +- .../Tags/Details/TagDetailsModalContent.js | 45 +++-- .../TagDetailsModalContentConnector.js | 14 +- frontend/src/Settings/Tags/Tag.js | 13 ++ frontend/src/Settings/Tags/TagsConnector.js | 12 +- ...s => MonitoredBookSpecificationFixture.cs} | 0 .../RssSync/IndexerTagSpecificationFixture.cs | 110 ++++++++++++ ...xture.cs => AuthorSearchServiceFixture.cs} | 0 .../ReleaseSearchServiceFixture.cs | 166 ++++++++++++++++++ .../Migration/028_add_indexer_tags.cs | 14 ++ src/NzbDrone.Core/Datastore/TableMapping.cs | 3 +- .../RssSync/IndexerTagSpecification.cs | 39 ++++ .../Housekeepers/CleanupUnusedTags.cs | 8 +- .../IndexerSearch/ReleaseSearchService.cs | 3 + src/NzbDrone.Core/Localization/Core/en.json | 1 + src/NzbDrone.Core/Tags/TagDetails.cs | 3 +- src/NzbDrone.Core/Tags/TagService.cs | 8 + src/Readarr.Api.V1/Tags/TagDetailsResource.cs | 2 + 21 files changed, 454 insertions(+), 25 deletions(-) rename src/NzbDrone.Core.Test/DecisionEngineTests/{MonitoredAlbumSpecificationFixture.cs => MonitoredBookSpecificationFixture.cs} (100%) create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/IndexerTagSpecificationFixture.cs rename src/NzbDrone.Core.Test/IndexerSearchTests/{ArtistSearchServiceFixture.cs => AuthorSearchServiceFixture.cs} (100%) create mode 100644 src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/028_add_indexer_tags.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/IndexerTagSpecification.cs diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js index b57ba17b9..46f004add 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -43,6 +43,7 @@ function EditIndexerModalContent(props) { enableInteractiveSearch, supportsRss, supportsSearch, + tags, fields, priority } = item; @@ -144,6 +145,7 @@ function EditIndexerModalContent(props) { ); }) } + + + + + {translate('Tags')} + + + + } diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js index 93e8d2d57..05198dcf8 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.js +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.js @@ -4,6 +4,7 @@ import Card from 'Components/Card'; import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; import { icons, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import EditIndexerModalConnector from './EditIndexerModalConnector'; @@ -68,6 +69,8 @@ class Indexer extends Component { enableRss, enableAutomaticSearch, enableInteractiveSearch, + tags, + tagList, supportsRss, supportsSearch, priority, @@ -133,6 +136,11 @@ class Indexer extends Component { } + + 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 6e53ecf2e..8fb41f28b 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js @@ -21,6 +21,7 @@ function TagDetailsModalContent(props) { importLists, notifications, releaseProfiles, + indexers, onModalClose, onDeleteTagPress } = props; @@ -40,7 +41,7 @@ function TagDetailsModalContent(props) { } { - !!author.length && + author.length ?
{ author.map((item) => { @@ -51,11 +52,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!delayProfiles.length && + delayProfiles.length ?
{ delayProfiles.map((item) => { @@ -80,11 +82,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!notifications.length && + notifications.length ?
{ notifications.map((item) => { @@ -95,11 +98,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!importLists.length && + importLists.length ?
{ importLists.map((item) => { @@ -110,11 +114,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!releaseProfiles.length && + releaseProfiles.length ?
{ releaseProfiles.map((item) => { @@ -150,13 +155,30 @@ function TagDetailsModalContent(props) { ); }) - } + }s ); }) } -
+ : + null + } + + { + indexers.length ? +
+ { + indexers.map((item) => { + return ( +
+ {item.name} +
+ ); + }) + } +
: + null } @@ -191,6 +213,7 @@ TagDetailsModalContent.propTypes = { importLists: PropTypes.arrayOf(PropTypes.object).isRequired, notifications: PropTypes.arrayOf(PropTypes.object).isRequired, releaseProfiles: 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 3444039f4..64a829d47 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js @@ -69,6 +69,14 @@ function createMatchingReleaseProfilesSelector() { ); } +function createMatchingIndexersSelector() { + return createSelector( + (state, { indexerIds }) => indexerIds, + (state) => state.settings.indexers.items, + findMatchingItems + ); +} + function createMapStateToProps() { return createSelector( createMatchingAuthorSelector(), @@ -76,13 +84,15 @@ function createMapStateToProps() { createMatchingImportListsSelector(), createMatchingNotificationsSelector(), createMatchingReleaseProfilesSelector(), - (author, delayProfiles, importLists, notifications, releaseProfiles) => { + createMatchingIndexersSelector(), + (author, delayProfiles, importLists, notifications, releaseProfiles, indexers) => { return { author, delayProfiles, importLists, notifications, - releaseProfiles + releaseProfiles, + indexers }; } ); diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index b1f44a26a..b68db9519 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -57,6 +57,7 @@ class Tag extends Component { importListIds, notificationIds, restrictionIds, + indexerIds, authorIds } = this.props; @@ -70,6 +71,7 @@ class Tag extends Component { importListIds.length || notificationIds.length || restrictionIds.length || + indexerIds.length || authorIds.length ); @@ -120,6 +122,14 @@ class Tag extends Component { {restrictionIds.length} restriction{restrictionIds.length > 1 && 's'} } + + { + indexerIds.length ? +
+ {indexerIds.length} indexer{indexerIds.length > 1 && 's'} +
: + null + } } @@ -138,6 +148,7 @@ class Tag extends Component { importListIds={importListIds} notificationIds={notificationIds} restrictionIds={restrictionIds} + indexerIds={indexerIds} isOpen={isDetailsModalOpen} onModalClose={this.onDetailsModalClose} onDeleteTagPress={this.onDeleteTagPress} @@ -164,6 +175,7 @@ Tag.propTypes = { importListIds: PropTypes.arrayOf(PropTypes.number).isRequired, notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired, + indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired, authorIds: PropTypes.arrayOf(PropTypes.number).isRequired, onConfirmDeleteTag: PropTypes.func.isRequired }; @@ -173,6 +185,7 @@ Tag.defaultProps = { importListIds: [], notificationIds: [], restrictionIds: [], + indexerIds: [], authorIds: [] }; diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index bbfa5d27e..241ee260a 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, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; +import { fetchDelayProfiles, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; import { fetchTagDetails } from 'Store/Actions/tagActions'; import Tags from './Tags'; @@ -29,7 +29,8 @@ const mapDispatchToProps = { dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchImportLists: fetchImportLists, dispatchFetchNotifications: fetchNotifications, - dispatchFetchReleaseProfiles: fetchReleaseProfiles + dispatchFetchReleaseProfiles: fetchReleaseProfiles, + dispatchFetchIndexers: fetchIndexers }; class MetadatasConnector extends Component { @@ -43,7 +44,8 @@ class MetadatasConnector extends Component { dispatchFetchDelayProfiles, dispatchFetchImportLists, dispatchFetchNotifications, - dispatchFetchReleaseProfiles + dispatchFetchReleaseProfiles, + dispatchFetchIndexers } = this.props; dispatchFetchTagDetails(); @@ -51,6 +53,7 @@ class MetadatasConnector extends Component { dispatchFetchImportLists(); dispatchFetchNotifications(); dispatchFetchReleaseProfiles(); + dispatchFetchIndexers(); } // @@ -70,7 +73,8 @@ MetadatasConnector.propTypes = { dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchNotifications: PropTypes.func.isRequired, - dispatchFetchReleaseProfiles: PropTypes.func.isRequired + dispatchFetchReleaseProfiles: PropTypes.func.isRequired, + dispatchFetchIndexers: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredAlbumSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredBookSpecificationFixture.cs similarity index 100% rename from src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredAlbumSpecificationFixture.cs rename to src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredBookSpecificationFixture.cs 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..9c7c3987b --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/IndexerTagSpecificationFixture.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Books; +using NzbDrone.Core.DecisionEngine.Specifications.RssSync; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch.Definitions; +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 RemoteBook _parseResultMulti; + private IndexerDefinition _fakeIndexerDefinition; + private Author _fakeAuthor; + private Book _firstBook; + private Book _secondBook; + 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(); + + _fakeAuthor = Builder.CreateNew() + .With(c => c.Monitored = true) + .With(c => c.Tags = new HashSet()) + .Build(); + + _fakeRelease = new ReleaseInfo + { + IndexerId = 1 + }; + + _firstBook = new Book { Monitored = true }; + _secondBook = new Book { Monitored = true }; + + var doubleBookList = new List { _firstBook, _secondBook }; + + _parseResultMulti = new RemoteBook + { + Author = _fakeAuthor, + Books = doubleBookList, + Release = _fakeRelease + }; + } + + [Test] + public void indexer_and_author_without_tags_should_return_true() + { + _fakeIndexerDefinition.Tags = new HashSet(); + _fakeAuthor.Tags = new HashSet(); + + _specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue(); + } + + [Test] + public void indexer_with_tags_author_without_tags_should_return_false() + { + _fakeIndexerDefinition.Tags = new HashSet { 123 }; + _fakeAuthor.Tags = new HashSet(); + + _specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeFalse(); + } + + [Test] + public void indexer_without_tags_author_with_tags_should_return_true() + { + _fakeIndexerDefinition.Tags = new HashSet(); + _fakeAuthor.Tags = new HashSet { 123 }; + + _specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue(); + } + + [Test] + public void indexer_with_tags_author_with_matching_tags_should_return_true() + { + _fakeIndexerDefinition.Tags = new HashSet { 123, 456 }; + _fakeAuthor.Tags = new HashSet { 123, 789 }; + + _specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeTrue(); + } + + [Test] + public void indexer_with_tags_author_with_different_tags_should_return_false() + { + _fakeIndexerDefinition.Tags = new HashSet { 456 }; + _fakeAuthor.Tags = new HashSet { 123, 789 }; + + _specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/AuthorSearchServiceFixture.cs similarity index 100% rename from src/NzbDrone.Core.Test/IndexerSearchTests/ArtistSearchServiceFixture.cs rename to src/NzbDrone.Core.Test/IndexerSearchTests/AuthorSearchServiceFixture.cs diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs new file mode 100644 index 000000000..5fc1569f3 --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Books; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.IndexerSearchTests +{ + public class ReleaseSearchServiceFixture : CoreTest + { + private Mock _mockIndexer; + private Author _author; + private Book _firstBook; + + [SetUp] + public void SetUp() + { + _mockIndexer = Mocker.GetMock(); + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = 1 }); + _mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true); + + Mocker.GetMock() + .Setup(s => s.AutomaticSearchEnabled(true)) + .Returns(new List { _mockIndexer.Object }); + + Mocker.GetMock() + .Setup(s => s.GetSearchDecision(It.IsAny>(), It.IsAny())) + .Returns(new List()); + + _author = Builder.CreateNew() + .With(v => v.Monitored = true) + .Build(); + + _firstBook = Builder.CreateNew() + .With(e => e.Author = _author) + .Build(); + + var edition = Builder.CreateNew() + .With(e => e.Book = _firstBook) + .With(e => e.Monitored = true) + .Build(); + + _firstBook.Editions = new List { edition }; + + Mocker.GetMock() + .Setup(v => v.GetAuthor(_author.Id)) + .Returns(_author); + } + + private List WatchForSearchCriteria() + { + var result = new List(); + + _mockIndexer.Setup(v => v.Fetch(It.IsAny())) + .Callback(s => result.Add(s)) + .Returns(new List()); + + return result; + } + + [Test] + public void Tags_IndexerTags_AuthorNoTags_IndexerNotIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1, + Tags = new HashSet { 3 } + }); + + var allCriteria = WatchForSearchCriteria(); + + Subject.BookSearch(_firstBook, false, true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(0); + } + + [Test] + public void Tags_IndexerNoTags_AuthorTags_IndexerIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1 + }); + + _author = Builder.CreateNew() + .With(v => v.Monitored = true) + .With(v => v.Tags = new HashSet { 3 }) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetAuthor(_author.Id)) + .Returns(_author); + + var allCriteria = WatchForSearchCriteria(); + + Subject.BookSearch(_firstBook, false, true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + } + + [Test] + public void Tags_IndexerAndAuthorTagsMatch_IndexerIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1, + Tags = new HashSet { 1, 2, 3 } + }); + + _author = Builder.CreateNew() + .With(v => v.Monitored = true) + .With(v => v.Tags = new HashSet { 3, 4, 5 }) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetAuthor(_author.Id)) + .Returns(_author); + + var allCriteria = WatchForSearchCriteria(); + + Subject.BookSearch(_firstBook, false, true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + } + + [Test] + public void Tags_IndexerAndAuthorTagsMismatch_IndexerNotIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1, + Tags = new HashSet { 1, 2, 3 } + }); + + _author = Builder.CreateNew() + .With(v => v.Monitored = true) + .With(v => v.Tags = new HashSet { 4, 5, 6 }) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetAuthor(_author.Id)) + .Returns(_author); + + var allCriteria = WatchForSearchCriteria(); + + Subject.BookSearch(_firstBook, false, true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(0); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/028_add_indexer_tags.cs b/src/NzbDrone.Core/Datastore/Migration/028_add_indexer_tags.cs new file mode 100644 index 000000000..42902049b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/028_add_indexer_tags.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(028)] + 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 94e077654..e43791951 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -71,8 +71,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..00103ba79 --- /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(RemoteBook subject, SearchCriteriaBase searchCriteria) + { + // If indexer has tags, check that at least one of them is present on the author + var indexerTags = _indexerRepository.Get(subject.Release.IndexerId).Tags; + + if (indexerTags.Any() && indexerTags.Intersect(subject.Author.Tags).Empty()) + { + _logger.Debug("Indexer {0} has tags. None of these are present on author {1}. Rejecting", subject.Release.Indexer, subject.Author); + + return Decision.Reject("Author tags do not match any of the indexer tags"); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 7968e2e3a..d45a6d139 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -20,10 +20,10 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - var usedTags = new[] { "Authors", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists" } - .SelectMany(v => GetUsedTags(v, mapper)) - .Distinct() - .ToArray(); + var usedTags = new[] { "Authors", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers" } + .SelectMany(v => GetUsedTags(v, mapper)) + .Distinct() + .ToArray(); if (usedTags.Any()) { diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index f851ed0ff..63962502b 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -112,6 +112,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.Author.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 edd3cb775..bbebb6ad9 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -375,6 +375,7 @@ "IndexerSettings": "Indexer Settings", "IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures", "IndexerStatusCheckSingleClientMessage": "Indexers unavailable due to failures: {0}", + "IndexerTagsHelpText": "Only use this indexer for authors with at least one matching tag. Leave blank to use with all authors.", "Indexers": "Indexers", "IndexersSettingsSummary": "Indexers and release restrictions", "InstanceName": "Instance Name", diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs index dd4ac1e5c..45eded56b 100644 --- a/src/NzbDrone.Core/Tags/TagDetails.cs +++ b/src/NzbDrone.Core/Tags/TagDetails.cs @@ -12,13 +12,14 @@ namespace NzbDrone.Core.Tags public List RestrictionIds { get; set; } public List DelayProfileIds { get; set; } public List ImportListIds { get; set; } + public List IndexerIds { get; set; } public List RootFolderIds { get; set; } public bool InUse { get { - return AuthorIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || RootFolderIds.Any(); + return AuthorIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || IndexerIds.Any() || RootFolderIds.Any(); } } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index dbf0985c1..dbee84331 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -3,6 +3,7 @@ using System.Linq; using NzbDrone.Core.Books; using NzbDrone.Core.Datastore; using NzbDrone.Core.ImportLists; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Notifications; using NzbDrone.Core.Profiles.Delay; @@ -32,6 +33,7 @@ namespace NzbDrone.Core.Tags private readonly INotificationFactory _notificationFactory; private readonly IReleaseProfileService _releaseProfileService; private readonly IAuthorService _authorService; + private readonly IIndexerFactory _indexerService; private readonly IRootFolderService _rootFolderService; public TagService(ITagRepository repo, @@ -41,6 +43,7 @@ namespace NzbDrone.Core.Tags INotificationFactory notificationFactory, IReleaseProfileService releaseProfileService, IAuthorService authorService, + IIndexerFactory indexerService, IRootFolderService rootFolderService) { _repo = repo; @@ -50,6 +53,7 @@ namespace NzbDrone.Core.Tags _notificationFactory = notificationFactory; _releaseProfileService = releaseProfileService; _authorService = authorService; + _indexerService = indexerService; _rootFolderService = rootFolderService; } @@ -78,6 +82,7 @@ namespace NzbDrone.Core.Tags var notifications = _notificationFactory.AllForTag(tagId); var restrictions = _releaseProfileService.AllForTag(tagId); var author = _authorService.AllForTag(tagId); + var indexers = _indexerService.AllForTag(tagId); var rootFolders = _rootFolderService.AllForTag(tagId); return new TagDetails @@ -89,6 +94,7 @@ namespace NzbDrone.Core.Tags NotificationIds = notifications.Select(c => c.Id).ToList(), RestrictionIds = restrictions.Select(c => c.Id).ToList(), AuthorIds = author.Select(c => c.Id).ToList(), + IndexerIds = indexers.Select(c => c.Id).ToList(), RootFolderIds = rootFolders.Select(c => c.Id).ToList() }; } @@ -101,6 +107,7 @@ namespace NzbDrone.Core.Tags var notifications = _notificationFactory.All(); var restrictions = _releaseProfileService.All(); var authors = _authorService.GetAllAuthors(); + var indexers = _indexerService.All(); var rootFolders = _rootFolderService.All(); var details = new List(); @@ -116,6 +123,7 @@ namespace NzbDrone.Core.Tags 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(), AuthorIds = authors.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList() }); } diff --git a/src/Readarr.Api.V1/Tags/TagDetailsResource.cs b/src/Readarr.Api.V1/Tags/TagDetailsResource.cs index 7d4447e91..1d0ec7067 100644 --- a/src/Readarr.Api.V1/Tags/TagDetailsResource.cs +++ b/src/Readarr.Api.V1/Tags/TagDetailsResource.cs @@ -12,6 +12,7 @@ namespace Readarr.Api.V1.Tags public List ImportListIds { get; set; } public List NotificationIds { get; set; } public List RestrictionIds { get; set; } + public List IndexerIds { get; set; } public List AuthorIds { get; set; } } @@ -32,6 +33,7 @@ namespace Readarr.Api.V1.Tags ImportListIds = model.ImportListIds, NotificationIds = model.NotificationIds, RestrictionIds = model.RestrictionIds, + IndexerIds = model.IndexerIds, AuthorIds = model.AuthorIds }; }