New: Add tag support to indexers

(cherry picked from commit c3d54b312ef18b837d54605ea78f1a263fd6900b)
pull/2438/head
6cUbi57z 3 years ago committed by Bogdan
parent 10e230cc06
commit b97d63cb5b

@ -43,6 +43,7 @@ function EditIndexerModalContent(props) {
enableInteractiveSearch, enableInteractiveSearch,
supportsRss, supportsRss,
supportsSearch, supportsSearch,
tags,
fields, fields,
priority priority
} = item; } = item;
@ -144,6 +145,7 @@ function EditIndexerModalContent(props) {
); );
}) })
} }
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
@ -162,6 +164,20 @@ function EditIndexerModalContent(props) {
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>
{translate('Tags')}
</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagsHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form> </Form>
} }
</ModalBody> </ModalBody>

@ -4,6 +4,7 @@ import Card from 'Components/Card';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditIndexerModalConnector from './EditIndexerModalConnector'; import EditIndexerModalConnector from './EditIndexerModalConnector';
@ -68,6 +69,8 @@ class Indexer extends Component {
enableRss, enableRss,
enableAutomaticSearch, enableAutomaticSearch,
enableInteractiveSearch, enableInteractiveSearch,
tags,
tagList,
supportsRss, supportsRss,
supportsSearch, supportsSearch,
priority, priority,
@ -133,6 +136,11 @@ class Indexer extends Component {
} }
</div> </div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditIndexerModalConnector <EditIndexerModalConnector
id={id} id={id}
isOpen={this.state.isEditIndexerModalOpen} isOpen={this.state.isEditIndexerModalOpen}
@ -161,6 +169,8 @@ Indexer.propTypes = {
enableRss: PropTypes.bool.isRequired, enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired, enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired, enableInteractiveSearch: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
supportsRss: PropTypes.bool.isRequired, supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired, supportsSearch: PropTypes.bool.isRequired,
showPriority: PropTypes.bool.isRequired, showPriority: PropTypes.bool.isRequired,

@ -54,6 +54,7 @@ class Indexers extends Component {
render() { render() {
const { const {
items, items,
tagList,
dispatchCloneIndexer, dispatchCloneIndexer,
onConfirmDeleteIndexer, onConfirmDeleteIndexer,
...otherProps ...otherProps
@ -79,6 +80,7 @@ class Indexers extends Component {
<Indexer <Indexer
key={item.id} key={item.id}
{...item} {...item}
tagList={tagList}
showPriority={showPriority} showPriority={showPriority}
onCloneIndexerPress={this.onCloneIndexerPress} onCloneIndexerPress={this.onCloneIndexerPress}
onConfirmDeleteIndexer={onConfirmDeleteIndexer} onConfirmDeleteIndexer={onConfirmDeleteIndexer}
@ -119,6 +121,7 @@ Indexers.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired, dispatchCloneIndexer: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired onConfirmDeleteIndexer: PropTypes.func.isRequired
}; };

@ -4,13 +4,20 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import Indexers from './Indexers'; import Indexers from './Indexers';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.indexers', sortByName), createSortedSectionSelector('settings.indexers', sortByName),
(indexers) => indexers createTagsSelector(),
(indexers, tagList) => {
return {
...indexers,
tagList
};
}
); );
} }

@ -21,6 +21,7 @@ function TagDetailsModalContent(props) {
importLists, importLists,
notifications, notifications,
releaseProfiles, releaseProfiles,
indexers,
onModalClose, onModalClose,
onDeleteTagPress onDeleteTagPress
} = props; } = props;
@ -40,7 +41,7 @@ function TagDetailsModalContent(props) {
} }
{ {
!!author.length && author.length ?
<FieldSet legend={translate('Authors')}> <FieldSet legend={translate('Authors')}>
{ {
author.map((item) => { author.map((item) => {
@ -51,11 +52,12 @@ function TagDetailsModalContent(props) {
); );
}) })
} }
</FieldSet> </FieldSet> :
null
} }
{ {
!!delayProfiles.length && delayProfiles.length ?
<FieldSet legend={translate('DelayProfile')}> <FieldSet legend={translate('DelayProfile')}>
{ {
delayProfiles.map((item) => { delayProfiles.map((item) => {
@ -80,11 +82,12 @@ function TagDetailsModalContent(props) {
); );
}) })
} }
</FieldSet> </FieldSet> :
null
} }
{ {
!!notifications.length && notifications.length ?
<FieldSet legend={translate('Connections')}> <FieldSet legend={translate('Connections')}>
{ {
notifications.map((item) => { notifications.map((item) => {
@ -95,11 +98,12 @@ function TagDetailsModalContent(props) {
); );
}) })
} }
</FieldSet> </FieldSet> :
null
} }
{ {
!!importLists.length && importLists.length ?
<FieldSet legend={translate('ImportLists')}> <FieldSet legend={translate('ImportLists')}>
{ {
importLists.map((item) => { importLists.map((item) => {
@ -110,11 +114,12 @@ function TagDetailsModalContent(props) {
); );
}) })
} }
</FieldSet> </FieldSet> :
null
} }
{ {
!!releaseProfiles.length && releaseProfiles.length ?
<FieldSet legend={translate('ReleaseProfiles')}> <FieldSet legend={translate('ReleaseProfiles')}>
{ {
releaseProfiles.map((item) => { releaseProfiles.map((item) => {
@ -150,13 +155,30 @@ function TagDetailsModalContent(props) {
</Label> </Label>
); );
}) })
} }s
</div> </div>
</div> </div>
); );
}) })
} }
</FieldSet> </FieldSet> :
null
}
{
indexers.length ?
<FieldSet legend={translate('Indexers')}>
{
indexers.map((item) => {
return (
<div key={item.id}>
{item.name}
</div>
);
})
}
</FieldSet> :
null
} }
</ModalBody> </ModalBody>
@ -191,6 +213,7 @@ TagDetailsModalContent.propTypes = {
importLists: PropTypes.arrayOf(PropTypes.object).isRequired, importLists: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired, notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired onDeleteTagPress: PropTypes.func.isRequired
}; };

@ -69,6 +69,14 @@ function createMatchingReleaseProfilesSelector() {
); );
} }
function createMatchingIndexersSelector() {
return createSelector(
(state, { indexerIds }) => indexerIds,
(state) => state.settings.indexers.items,
findMatchingItems
);
}
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createMatchingAuthorSelector(), createMatchingAuthorSelector(),
@ -76,13 +84,15 @@ function createMapStateToProps() {
createMatchingImportListsSelector(), createMatchingImportListsSelector(),
createMatchingNotificationsSelector(), createMatchingNotificationsSelector(),
createMatchingReleaseProfilesSelector(), createMatchingReleaseProfilesSelector(),
(author, delayProfiles, importLists, notifications, releaseProfiles) => { createMatchingIndexersSelector(),
(author, delayProfiles, importLists, notifications, releaseProfiles, indexers) => {
return { return {
author, author,
delayProfiles, delayProfiles,
importLists, importLists,
notifications, notifications,
releaseProfiles releaseProfiles,
indexers
}; };
} }
); );

@ -57,6 +57,7 @@ class Tag extends Component {
importListIds, importListIds,
notificationIds, notificationIds,
restrictionIds, restrictionIds,
indexerIds,
authorIds authorIds
} = this.props; } = this.props;
@ -70,6 +71,7 @@ class Tag extends Component {
importListIds.length || importListIds.length ||
notificationIds.length || notificationIds.length ||
restrictionIds.length || restrictionIds.length ||
indexerIds.length ||
authorIds.length authorIds.length
); );
@ -120,6 +122,14 @@ class Tag extends Component {
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'} {restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
</div> </div>
} }
{
indexerIds.length ?
<div>
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
</div> :
null
}
</div> </div>
} }
@ -138,6 +148,7 @@ class Tag extends Component {
importListIds={importListIds} importListIds={importListIds}
notificationIds={notificationIds} notificationIds={notificationIds}
restrictionIds={restrictionIds} restrictionIds={restrictionIds}
indexerIds={indexerIds}
isOpen={isDetailsModalOpen} isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose} onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress} onDeleteTagPress={this.onDeleteTagPress}
@ -164,6 +175,7 @@ Tag.propTypes = {
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired, importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired, restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired, authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired onConfirmDeleteTag: PropTypes.func.isRequired
}; };
@ -173,6 +185,7 @@ Tag.defaultProps = {
importListIds: [], importListIds: [],
notificationIds: [], notificationIds: [],
restrictionIds: [], restrictionIds: [],
indexerIds: [],
authorIds: [] authorIds: []
}; };

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; 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 { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags'; import Tags from './Tags';
@ -29,7 +29,8 @@ const mapDispatchToProps = {
dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchImportLists: fetchImportLists, dispatchFetchImportLists: fetchImportLists,
dispatchFetchNotifications: fetchNotifications, dispatchFetchNotifications: fetchNotifications,
dispatchFetchReleaseProfiles: fetchReleaseProfiles dispatchFetchReleaseProfiles: fetchReleaseProfiles,
dispatchFetchIndexers: fetchIndexers
}; };
class MetadatasConnector extends Component { class MetadatasConnector extends Component {
@ -43,7 +44,8 @@ class MetadatasConnector extends Component {
dispatchFetchDelayProfiles, dispatchFetchDelayProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchNotifications, dispatchFetchNotifications,
dispatchFetchReleaseProfiles dispatchFetchReleaseProfiles,
dispatchFetchIndexers
} = this.props; } = this.props;
dispatchFetchTagDetails(); dispatchFetchTagDetails();
@ -51,6 +53,7 @@ class MetadatasConnector extends Component {
dispatchFetchImportLists(); dispatchFetchImportLists();
dispatchFetchNotifications(); dispatchFetchNotifications();
dispatchFetchReleaseProfiles(); dispatchFetchReleaseProfiles();
dispatchFetchIndexers();
} }
// //
@ -70,7 +73,8 @@ MetadatasConnector.propTypes = {
dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchNotifications: 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); export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);

@ -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<IndexerTagSpecification>
{
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<int>()
};
Mocker
.GetMock<IIndexerRepository>()
.Setup(m => m.Get(It.IsAny<int>()))
.Returns(_fakeIndexerDefinition);
_specification = Mocker.Resolve<IndexerTagSpecification>();
_fakeAuthor = Builder<Author>.CreateNew()
.With(c => c.Monitored = true)
.With(c => c.Tags = new HashSet<int>())
.Build();
_fakeRelease = new ReleaseInfo
{
IndexerId = 1
};
_firstBook = new Book { Monitored = true };
_secondBook = new Book { Monitored = true };
var doubleBookList = new List<Book> { _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<int>();
_fakeAuthor.Tags = new HashSet<int>();
_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<int> { 123 };
_fakeAuthor.Tags = new HashSet<int>();
_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<int>();
_fakeAuthor.Tags = new HashSet<int> { 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<int> { 123, 456 };
_fakeAuthor.Tags = new HashSet<int> { 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<int> { 456 };
_fakeAuthor.Tags = new HashSet<int> { 123, 789 };
_specification.IsSatisfiedBy(_parseResultMulti, new BookSearchCriteria { MonitoredBooksOnly = true }).Accepted.Should().BeFalse();
}
}
}

@ -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<ReleaseSearchService>
{
private Mock<IIndexer> _mockIndexer;
private Author _author;
private Book _firstBook;
[SetUp]
public void SetUp()
{
_mockIndexer = Mocker.GetMock<IIndexer>();
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = 1 });
_mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.AutomaticSearchEnabled(true))
.Returns(new List<IIndexer> { _mockIndexer.Object });
Mocker.GetMock<IMakeDownloadDecision>()
.Setup(s => s.GetSearchDecision(It.IsAny<List<Parser.Model.ReleaseInfo>>(), It.IsAny<SearchCriteriaBase>()))
.Returns(new List<DownloadDecision>());
_author = Builder<Author>.CreateNew()
.With(v => v.Monitored = true)
.Build();
_firstBook = Builder<Book>.CreateNew()
.With(e => e.Author = _author)
.Build();
var edition = Builder<Edition>.CreateNew()
.With(e => e.Book = _firstBook)
.With(e => e.Monitored = true)
.Build();
_firstBook.Editions = new List<Edition> { edition };
Mocker.GetMock<IAuthorService>()
.Setup(v => v.GetAuthor(_author.Id))
.Returns(_author);
}
private List<SearchCriteriaBase> WatchForSearchCriteria()
{
var result = new List<SearchCriteriaBase>();
_mockIndexer.Setup(v => v.Fetch(It.IsAny<BookSearchCriteria>()))
.Callback<BookSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
return result;
}
[Test]
public void Tags_IndexerTags_AuthorNoTags_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1,
Tags = new HashSet<int> { 3 }
});
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().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<Author>.CreateNew()
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 3 })
.Build();
Mocker.GetMock<IAuthorService>()
.Setup(v => v.GetAuthor(_author.Id))
.Returns(_author);
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().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<int> { 1, 2, 3 }
});
_author = Builder<Author>.CreateNew()
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 3, 4, 5 })
.Build();
Mocker.GetMock<IAuthorService>()
.Setup(v => v.GetAuthor(_author.Id))
.Returns(_author);
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().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<int> { 1, 2, 3 }
});
_author = Builder<Author>.CreateNew()
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 4, 5, 6 })
.Build();
Mocker.GetMock<IAuthorService>()
.Setup(v => v.GetAuthor(_author.Id))
.Returns(_author);
var allCriteria = WatchForSearchCriteria();
Subject.BookSearch(_firstBook, false, true, false);
var criteria = allCriteria.OfType<BookSearchCriteria>().ToList();
criteria.Count.Should().Be(0);
}
}
}

@ -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();
}
}
}

@ -71,8 +71,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(i => i.Enable) .Ignore(i => i.Enable)
.Ignore(i => i.Protocol) .Ignore(i => i.Protocol)
.Ignore(i => i.SupportsRss) .Ignore(i => i.SupportsRss)
.Ignore(i => i.SupportsSearch) .Ignore(i => i.SupportsSearch);
.Ignore(d => d.Tags);
Mapper.Entity<ImportListDefinition>("ImportLists").RegisterModel() Mapper.Entity<ImportListDefinition>("ImportLists").RegisterModel()
.Ignore(x => x.ImplementationName) .Ignore(x => x.ImplementationName)

@ -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();
}
}
}

@ -20,10 +20,10 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
var usedTags = new[] { "Authors", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists" } var usedTags = new[] { "Authors", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers" }
.SelectMany(v => GetUsedTags(v, mapper)) .SelectMany(v => GetUsedTags(v, mapper))
.Distinct() .Distinct()
.ToArray(); .ToArray();
if (usedTags.Any()) if (usedTags.Any())
{ {

@ -112,6 +112,9 @@ namespace NzbDrone.Core.IndexerSearch
_indexerFactory.InteractiveSearchEnabled() : _indexerFactory.InteractiveSearchEnabled() :
_indexerFactory.AutomaticSearchEnabled(); _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<ReleaseInfo>(); var reports = new List<ReleaseInfo>();
_logger.ProgressInfo("Searching indexers for {0}. {1} active indexers", criteriaBase, indexers.Count); _logger.ProgressInfo("Searching indexers for {0}. {1} active indexers", criteriaBase, indexers.Count);

@ -375,6 +375,7 @@
"IndexerSettings": "Indexer Settings", "IndexerSettings": "Indexer Settings",
"IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures", "IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures",
"IndexerStatusCheckSingleClientMessage": "Indexers unavailable due to failures: {0}", "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", "Indexers": "Indexers",
"IndexersSettingsSummary": "Indexers and release restrictions", "IndexersSettingsSummary": "Indexers and release restrictions",
"InstanceName": "Instance Name", "InstanceName": "Instance Name",

@ -12,13 +12,14 @@ namespace NzbDrone.Core.Tags
public List<int> RestrictionIds { get; set; } public List<int> RestrictionIds { get; set; }
public List<int> DelayProfileIds { get; set; } public List<int> DelayProfileIds { get; set; }
public List<int> ImportListIds { get; set; } public List<int> ImportListIds { get; set; }
public List<int> IndexerIds { get; set; }
public List<int> RootFolderIds { get; set; } public List<int> RootFolderIds { get; set; }
public bool InUse public bool InUse
{ {
get 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();
} }
} }
} }

@ -3,6 +3,7 @@ using System.Linq;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Profiles.Delay;
@ -32,6 +33,7 @@ namespace NzbDrone.Core.Tags
private readonly INotificationFactory _notificationFactory; private readonly INotificationFactory _notificationFactory;
private readonly IReleaseProfileService _releaseProfileService; private readonly IReleaseProfileService _releaseProfileService;
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IIndexerFactory _indexerService;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
public TagService(ITagRepository repo, public TagService(ITagRepository repo,
@ -41,6 +43,7 @@ namespace NzbDrone.Core.Tags
INotificationFactory notificationFactory, INotificationFactory notificationFactory,
IReleaseProfileService releaseProfileService, IReleaseProfileService releaseProfileService,
IAuthorService authorService, IAuthorService authorService,
IIndexerFactory indexerService,
IRootFolderService rootFolderService) IRootFolderService rootFolderService)
{ {
_repo = repo; _repo = repo;
@ -50,6 +53,7 @@ namespace NzbDrone.Core.Tags
_notificationFactory = notificationFactory; _notificationFactory = notificationFactory;
_releaseProfileService = releaseProfileService; _releaseProfileService = releaseProfileService;
_authorService = authorService; _authorService = authorService;
_indexerService = indexerService;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
} }
@ -78,6 +82,7 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.AllForTag(tagId); var notifications = _notificationFactory.AllForTag(tagId);
var restrictions = _releaseProfileService.AllForTag(tagId); var restrictions = _releaseProfileService.AllForTag(tagId);
var author = _authorService.AllForTag(tagId); var author = _authorService.AllForTag(tagId);
var indexers = _indexerService.AllForTag(tagId);
var rootFolders = _rootFolderService.AllForTag(tagId); var rootFolders = _rootFolderService.AllForTag(tagId);
return new TagDetails return new TagDetails
@ -89,6 +94,7 @@ namespace NzbDrone.Core.Tags
NotificationIds = notifications.Select(c => c.Id).ToList(), NotificationIds = notifications.Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Select(c => c.Id).ToList(), RestrictionIds = restrictions.Select(c => c.Id).ToList(),
AuthorIds = author.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() RootFolderIds = rootFolders.Select(c => c.Id).ToList()
}; };
} }
@ -101,6 +107,7 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.All(); var notifications = _notificationFactory.All();
var restrictions = _releaseProfileService.All(); var restrictions = _releaseProfileService.All();
var authors = _authorService.GetAllAuthors(); var authors = _authorService.GetAllAuthors();
var indexers = _indexerService.All();
var rootFolders = _rootFolderService.All(); var rootFolders = _rootFolderService.All();
var details = new List<TagDetails>(); var details = new List<TagDetails>();
@ -116,6 +123,7 @@ namespace NzbDrone.Core.Tags
NotificationIds = notifications.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(), 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(), 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() RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList()
}); });
} }

@ -12,6 +12,7 @@ namespace Readarr.Api.V1.Tags
public List<int> ImportListIds { get; set; } public List<int> ImportListIds { get; set; }
public List<int> NotificationIds { get; set; } public List<int> NotificationIds { get; set; }
public List<int> RestrictionIds { get; set; } public List<int> RestrictionIds { get; set; }
public List<int> IndexerIds { get; set; }
public List<int> AuthorIds { get; set; } public List<int> AuthorIds { get; set; }
} }
@ -32,6 +33,7 @@ namespace Readarr.Api.V1.Tags
ImportListIds = model.ImportListIds, ImportListIds = model.ImportListIds,
NotificationIds = model.NotificationIds, NotificationIds = model.NotificationIds,
RestrictionIds = model.RestrictionIds, RestrictionIds = model.RestrictionIds,
IndexerIds = model.IndexerIds,
AuthorIds = model.AuthorIds AuthorIds = model.AuthorIds
}; };
} }

Loading…
Cancel
Save