From c3d54b312ef18b837d54605ea78f1a263fd6900b 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 Closes #487 --- .../Indexers/EditIndexerModalContent.js | 14 +++ .../src/Settings/Indexers/Indexers/Indexer.js | 10 ++ .../Settings/Indexers/Indexers/Indexers.js | 3 + .../Indexers/Indexers/IndexersConnector.js | 9 +- .../Tags/Details/TagDetailsModalContent.js | 43 +++++-- .../TagDetailsModalContentConnector.js | 14 ++- frontend/src/Settings/Tags/Tag.js | 13 +++ frontend/src/Settings/Tags/TagsConnector.js | 12 +- .../MonitoredEpisodeSpecificationFixture.cs | 2 +- .../RssSync/IndexerTagSpecificationFixture.cs | 110 ++++++++++++++++++ .../NzbSearchServiceFixture.cs | 108 +++++++++++++++++ .../Migration/159_add_indexer_tags.cs | 14 +++ src/NzbDrone.Core/Datastore/TableMapping.cs | 3 +- .../RssSync/IndexerTagSpecification.cs | 39 +++++++ .../IndexerSearch/NzbSearchService.cs | 3 + src/NzbDrone.Core/Tags/TagDetails.cs | 3 +- src/NzbDrone.Core/Tags/TagService.cs | 44 ++++--- src/Sonarr.Api.V3/Tags/TagDetailsResource.cs | 2 + 18 files changed, 407 insertions(+), 39 deletions(-) create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/IndexerTagSpecificationFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/159_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 b5dfbbce0..59f0d38d1 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -42,6 +42,7 @@ function EditIndexerModalContent(props) { enableInteractiveSearch, supportsRss, supportsSearch, + tags, fields, priority } = item; @@ -132,6 +133,7 @@ function EditIndexerModalContent(props) { ); }) } + + + + Tags + + + } diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js index 92758c331..4ab8516df 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.js +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { icons, kinds } from 'Helpers/Props'; import Card from 'Components/Card'; import Label from 'Components/Label'; +import TagList from 'Components/TagList'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import EditIndexerModalConnector from './EditIndexerModalConnector'; @@ -67,6 +68,8 @@ class Indexer extends Component { enableRss, enableAutomaticSearch, enableInteractiveSearch, + tags, + tagList, supportsRss, supportsSearch, priority, @@ -132,6 +135,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 1809aa264..7a08123bb 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; @@ -38,7 +39,7 @@ function TagDetailsModalContent(props) { } { - !!series.length && + series.length ?
{ series.map((item) => { @@ -49,11 +50,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!delayProfiles.length && + delayProfiles.length ?
{ delayProfiles.map((item) => { @@ -78,11 +80,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!notifications.length && + notifications.length ?
{ notifications.map((item) => { @@ -93,11 +96,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!importLists.length && + importLists.length ?
{ importLists.map((item) => { @@ -108,11 +112,12 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null } { - !!releaseProfiles.length && + releaseProfiles.length ?
{ releaseProfiles.map((item) => { @@ -154,7 +159,24 @@ function TagDetailsModalContent(props) { ); }) } -
+ : + null + } + + { + indexers.length ? +
+ { + indexers.map((item) => { + return ( +
+ {item.name} +
+ ); + }) + } +
: + null } @@ -189,6 +211,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 470dbb059..71948e1e2 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( createMatchingSeriesSelector(), @@ -76,13 +84,15 @@ function createMapStateToProps() { createMatchingImportListsSelector(), createMatchingNotificationsSelector(), createMatchingReleaseProfilesSelector(), - (series, delayProfiles, importLists, notifications, releaseProfiles) => { + createMatchingIndexersSelector(), + (series, delayProfiles, importLists, notifications, releaseProfiles, indexers) => { return { series, delayProfiles, importLists, notifications, - releaseProfiles + releaseProfiles, + indexers }; } ); diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js index 00a13ec82..b610c1cbb 100644 --- a/frontend/src/Settings/Tags/Tag.js +++ b/frontend/src/Settings/Tags/Tag.js @@ -56,6 +56,7 @@ class Tag extends Component { importListIds, notificationIds, restrictionIds, + indexerIds, seriesIds } = this.props; @@ -69,6 +70,7 @@ class Tag extends Component { importListIds.length || notificationIds.length || restrictionIds.length || + indexerIds.length || seriesIds.length ); @@ -124,6 +126,14 @@ class Tag extends Component { : null } + + { + indexerIds.length ? +
+ {indexerIds.length} indexer{indexerIds.length > 1 && 's'} +
: + null + } } @@ -142,6 +152,7 @@ class Tag extends Component { importListIds={importListIds} notificationIds={notificationIds} restrictionIds={restrictionIds} + indexerIds={indexerIds} isOpen={isDetailsModalOpen} onModalClose={this.onDetailsModalClose} onDeleteTagPress={this.onDeleteTagPress} @@ -168,6 +179,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, seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired, onConfirmDeleteTag: PropTypes.func.isRequired }; @@ -177,6 +189,7 @@ Tag.defaultProps = { importListIds: [], notificationIds: [], restrictionIds: [], + indexerIds: [], seriesIds: [] }; diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js index 70b727387..4b3cce22b 100644 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchTagDetails } from 'Store/Actions/tagActions'; -import { fetchDelayProfiles, fetchNotifications, fetchReleaseProfiles, fetchImportLists } from 'Store/Actions/settingsActions'; +import { fetchDelayProfiles, fetchNotifications, fetchReleaseProfiles, fetchImportLists, fetchIndexers } from 'Store/Actions/settingsActions'; import Tags from './Tags'; function createMapStateToProps() { @@ -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/MonitoredEpisodeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs index b83e5a1ed..d81e67f27 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/MonitoredEpisodeSpecificationFixture.cs @@ -5,8 +5,8 @@ using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Test.DecisionEngineTests { 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..bc15bf276 --- /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.DecisionEngine.Specifications.RssSync; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync +{ + [TestFixture] + public class IndexerTagSpecificationFixture : CoreTest + { + private IndexerTagSpecification _specification; + + private RemoteEpisode _parseResultMulti; + private IndexerDefinition _fakeIndexerDefinition; + private Series _fakeSeries; + private Episode _firstEpisode; + private Episode _secondEpisode; + 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(); + + _fakeSeries = Builder.CreateNew() + .With(c => c.Monitored = true) + .With(c => c.Tags = new HashSet()) + .Build(); + + _fakeRelease = new ReleaseInfo + { + IndexerId = 1 + }; + + _firstEpisode = new Episode { Monitored = true }; + _secondEpisode = new Episode { Monitored = true }; + + var doubleEpisodeList = new List { _firstEpisode, _secondEpisode }; + + _parseResultMulti = new RemoteEpisode + { + Series = _fakeSeries, + Episodes = doubleEpisodeList, + Release = _fakeRelease + }; + } + + [Test] + public void indexer_and_series_without_tags_should_return_true() + { + _fakeIndexerDefinition.Tags = new HashSet(); + _fakeSeries.Tags = new HashSet(); + + _specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue(); + } + + [Test] + public void indexer_with_tags_series_without_tags_should_return_false() + { + _fakeIndexerDefinition.Tags = new HashSet { 123 }; + _fakeSeries.Tags = new HashSet(); + + _specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse(); + } + + [Test] + public void indexer_without_tags_series_with_tags_should_return_true() + { + _fakeIndexerDefinition.Tags = new HashSet(); + _fakeSeries.Tags = new HashSet { 123 }; + + _specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue(); + } + + [Test] + public void indexer_with_tags_series_with_matching_tags_should_return_true() + { + _fakeIndexerDefinition.Tags = new HashSet { 123, 456 }; + _fakeSeries.Tags = new HashSet { 123, 789 }; + + _specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue(); + } + + [Test] + public void indexer_with_tags_series_with_different_tags_should_return_false() + { + _fakeIndexerDefinition.Tags = new HashSet { 456 }; + _fakeSeries.Tags = new HashSet { 123, 789 }; + + _specification.IsSatisfiedBy(_parseResultMulti, new SingleEpisodeSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs index da95f78d8..44482d1c1 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs @@ -135,6 +135,114 @@ namespace NzbDrone.Core.Test.IndexerSearchTests return result; } + [Test] + public void Tags_IndexerTags_SeriesNoTags_IndexerNotIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { + Id = 1, + Tags = new HashSet { 3 } + }); + + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.EpisodeSearch(_xemEpisodes.First(), true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(0); + } + + [Test] + public void Tags_IndexerNoTags_SeriesTags_IndexerIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1 + }); + + _xemSeries = Builder.CreateNew() + .With(v => v.UseSceneNumbering = true) + .With(v => v.Monitored = true) + .With(v => v.Tags = new HashSet { 3 }) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetSeries(_xemSeries.Id)) + .Returns(_xemSeries); + + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.EpisodeSearch(_xemEpisodes.First(), true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + } + + [Test] + public void Tags_IndexerAndSeriesTagsMatch_IndexerIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1, + Tags = new HashSet { 1, 2, 3 } + }); + + _xemSeries = Builder.CreateNew() + .With(v => v.UseSceneNumbering = true) + .With(v => v.Monitored = true) + .With(v => v.Tags = new HashSet { 3, 4, 5 }) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetSeries(_xemSeries.Id)) + .Returns(_xemSeries); + + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.EpisodeSearch(_xemEpisodes.First(), true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + } + + [Test] + public void Tags_IndexerAndSeriesTagsMismatch_IndexerNotIncluded() + { + _mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition + { + Id = 1, + Tags = new HashSet { 1, 2, 3 } + }); + + _xemSeries = Builder.CreateNew() + .With(v => v.UseSceneNumbering = true) + .With(v => v.Monitored = true) + .With(v => v.Tags = new HashSet { 4, 5, 6 }) + .Build(); + + Mocker.GetMock() + .Setup(v => v.GetSeries(_xemSeries.Id)) + .Returns(_xemSeries); + + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.EpisodeSearch(_xemEpisodes.First(), true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(0); + } + [Test] public void scene_episodesearch() { diff --git a/src/NzbDrone.Core/Datastore/Migration/159_add_indexer_tags.cs b/src/NzbDrone.Core/Datastore/Migration/159_add_indexer_tags.cs new file mode 100644 index 000000000..28f9eaf62 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/159_add_indexer_tags.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(159)] + 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 5f56f890f..8a1f49ac1 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -66,8 +66,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().RegisterDefinition("Notifications") .Ignore(i => i.SupportsOnGrab) 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..5ab1c1423 --- /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(RemoteEpisode 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.Series.Tags).Empty()) + { + _logger.Debug("Indexer {0} has tags. None of these are present on series {1}. Rejecting", subject.Release.Indexer, subject.Series); + + return Decision.Reject("Series 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 d0f7f6c15..42438ecab 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -503,6 +503,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.Series.Tags).Any()).ToList(); + var reports = new List(); _logger.ProgressInfo("Searching {0} indexers for {1}", indexers.Count, criteriaBase); diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs index ddd5717b6..4369f3c72 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 DelayProfileIds { get; set; } public List ImportListIds { get; set; } + public List IndexerIds { get; set; } public bool InUse { get { - return (SeriesIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any()); + return (SeriesIds.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 b804c7954..ac1ededfd 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -3,6 +3,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.Notifications; using NzbDrone.Core.Profiles.Delay; @@ -33,6 +34,7 @@ namespace NzbDrone.Core.Tags private readonly INotificationFactory _notificationFactory; private readonly IReleaseProfileService _releaseProfileService; private readonly ISeriesService _seriesService; + private readonly IIndexerFactory _indexerService; public TagService(ITagRepository repo, IEventAggregator eventAggregator, @@ -40,7 +42,8 @@ namespace NzbDrone.Core.Tags IImportListFactory importListFactory, INotificationFactory notificationFactory, IReleaseProfileService releaseProfileService, - ISeriesService seriesService) + ISeriesService seriesService, + IIndexerFactory indexerService) { _repo = repo; _eventAggregator = eventAggregator; @@ -49,6 +52,7 @@ namespace NzbDrone.Core.Tags _notificationFactory = notificationFactory; _releaseProfileService = releaseProfileService; _seriesService = seriesService; + _indexerService = indexerService; } public Tag GetTag(int tagId) @@ -81,16 +85,18 @@ namespace NzbDrone.Core.Tags var notifications = _notificationFactory.AllForTag(tagId); var restrictions = _releaseProfileService.AllForTag(tagId); var series = _seriesService.AllForTag(tagId); + var indexers = _indexerService.AllForTag(tagId); return new TagDetails - { - Id = tagId, - Label = tag.Label, - DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(), - ImportListIds = importLists.Select(c => c.Id).ToList(), - NotificationIds = notifications.Select(c => c.Id).ToList(), - RestrictionIds = restrictions.Select(c => c.Id).ToList(), - SeriesIds = series.Select(c => c.Id).ToList() + { + Id = tagId, + Label = tag.Label, + DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(), + ImportListIds = importLists.Select(c => c.Id).ToList(), + NotificationIds = notifications.Select(c => c.Id).ToList(), + RestrictionIds = restrictions.Select(c => c.Id).ToList(), + SeriesIds = series.Select(c => c.Id).ToList(), + IndexerIds = indexers.Select(c => c.Id).ToList() }; } @@ -102,21 +108,23 @@ namespace NzbDrone.Core.Tags var notifications = _notificationFactory.All(); var restrictions = _releaseProfileService.All(); var series = _seriesService.GetAllSeries(); + var indexers = _indexerService.All(); var details = new List(); foreach (var tag in tags) { details.Add(new TagDetails - { - Id = tag.Id, - Label = tag.Label, - DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), - 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(), - SeriesIds = series.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList() - } + { + Id = tag.Id, + Label = tag.Label, + DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + 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(), + SeriesIds = series.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() + } ); } diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs index 7a55aa410..2cc7059af 100644 --- a/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs +++ b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs @@ -12,6 +12,7 @@ namespace Sonarr.Api.V3.Tags public List ImportListIds { get; set; } public List NotificationIds { get; set; } public List RestrictionIds { get; set; } + public List IndexerIds { get; set; } public List SeriesIds { get; set; } } @@ -29,6 +30,7 @@ namespace Sonarr.Api.V3.Tags ImportListIds = model.ImportListIds, NotificationIds = model.NotificationIds, RestrictionIds = model.RestrictionIds, + IndexerIds = model.IndexerIds, SeriesIds = model.SeriesIds }; }