New: Add tag support to indexers

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
pull/3011/head
Qstick 2 years ago
parent 77041a5401
commit a26cbdf61f

@ -43,6 +43,7 @@ function EditIndexerModalContent(props) {
enableInteractiveSearch, enableInteractiveSearch,
supportsRss, supportsRss,
supportsSearch, supportsSearch,
tags,
fields, fields,
priority, priority,
protocol, protocol,
@ -168,18 +169,30 @@ function EditIndexerModalContent(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel>DownloadClient</FormLabel> <FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT} type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId" name="downloadClientId"
helpText={'Specify which download client is used for grabs from this indexer'} helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId} {...downloadClientId}
includeAny={true} includeAny={true}
protocol={protocol.value} protocol={protocol.value}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagHelpText')}
{...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
};
}
); );
} }

@ -22,6 +22,7 @@ function TagDetailsModalContent(props) {
importLists, importLists,
notifications, notifications,
releaseProfiles, releaseProfiles,
indexers,
onModalClose, onModalClose,
onDeleteTagPress onDeleteTagPress
} = props; } = props;
@ -41,7 +42,7 @@ function TagDetailsModalContent(props) {
} }
{ {
!!artist.length && artist.length ?
<FieldSet legend={translate('Artists')}> <FieldSet legend={translate('Artists')}>
{ {
artist.map((item) => { artist.map((item) => {
@ -52,11 +53,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) => {
@ -81,11 +83,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) => {
@ -96,11 +99,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) => {
@ -111,11 +115,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) => {
@ -157,7 +162,24 @@ function TagDetailsModalContent(props) {
); );
}) })
} }
</FieldSet> </FieldSet> :
null
}
{
indexers.length ?
<FieldSet legend="Indexers">
{
indexers.map((item) => {
return (
<div key={item.id}>
{item.name}
</div>
);
})
}
</FieldSet> :
null
} }
</ModalBody> </ModalBody>
@ -192,6 +214,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(
createMatchingArtistSelector(), createMatchingArtistSelector(),
@ -76,13 +84,15 @@ function createMapStateToProps() {
createMatchingImportListsSelector(), createMatchingImportListsSelector(),
createMatchingNotificationsSelector(), createMatchingNotificationsSelector(),
createMatchingReleaseProfilesSelector(), createMatchingReleaseProfilesSelector(),
(artist, delayProfiles, importLists, notifications, releaseProfiles) => { createMatchingIndexersSelector(),
(artist, delayProfiles, importLists, notifications, releaseProfiles, indexers) => {
return { return {
artist, artist,
delayProfiles, delayProfiles,
importLists, importLists,
notifications, notifications,
releaseProfiles releaseProfiles,
indexers
}; };
} }
); );

@ -57,6 +57,7 @@ class Tag extends Component {
importListIds, importListIds,
notificationIds, notificationIds,
restrictionIds, restrictionIds,
indexerIds,
artistIds artistIds
} = 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 ||
artistIds.length artistIds.length
); );
@ -87,38 +89,50 @@ class Tag extends Component {
isTagUsed && isTagUsed &&
<div> <div>
{ {
!!artistIds.length && artistIds.length ?
<div> <div>
{artistIds.length} artists {artistIds.length} artists
</div> </div> :
null
} }
{ {
!!delayProfileIds.length && delayProfileIds.length ?
<div> <div>
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'} {delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
</div> </div> :
null
} }
{ {
!!importListIds.length && importListIds.length ?
<div> <div>
{importListIds.length} import list{importListIds.length > 1 && 's'} {importListIds.length} import list{importListIds.length > 1 && 's'}
</div> </div> :
null
} }
{ {
!!notificationIds.length && notificationIds.length ?
<div> <div>
{notificationIds.length} connection{notificationIds.length > 1 && 's'} {notificationIds.length} connection{notificationIds.length > 1 && 's'}
</div> </div> :
null
} }
{ {
!!restrictionIds.length && restrictionIds.length ?
<div> <div>
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'} {restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
</div> </div> :
null
}
{
indexerIds.length ?
<div>
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
</div> :
null
} }
</div> </div>
} }
@ -138,6 +152,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 +179,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,
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired onConfirmDeleteTag: PropTypes.func.isRequired
}; };
@ -173,6 +189,7 @@ Tag.defaultProps = {
importListIds: [], importListIds: [],
notificationIds: [], notificationIds: [],
restrictionIds: [], restrictionIds: [],
indexerIds: [],
artistIds: [] artistIds: []
}; };

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

@ -12,6 +12,7 @@ namespace Lidarr.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> ArtistIds { get; set; } public List<int> ArtistIds { get; set; }
} }
@ -32,6 +33,7 @@ namespace Lidarr.Api.V1.Tags
ImportListIds = model.ImportListIds, ImportListIds = model.ImportListIds,
NotificationIds = model.NotificationIds, NotificationIds = model.NotificationIds,
RestrictionIds = model.RestrictionIds, RestrictionIds = model.RestrictionIds,
IndexerIds = model.IndexerIds,
ArtistIds = model.ArtistIds ArtistIds = model.ArtistIds
}; };
} }

@ -0,0 +1,136 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Music;
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 RemoteAlbum _parseResultMulti;
private IndexerDefinition _fakeIndexerDefinition;
private Artist _fakeArtist;
private Album _firstAlbum;
private Album _secondAlbum;
private ReleaseInfo _fakeRelease;
[SetUp]
public void Setup()
{
_fakeIndexerDefinition = new IndexerDefinition
{
Tags = new HashSet<int>()
};
Mocker
.GetMock<IIndexerFactory>()
.Setup(m => m.Get(It.IsAny<int>()))
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), -1));
Mocker
.GetMock<IIndexerFactory>()
.Setup(m => m.Get(1))
.Returns(_fakeIndexerDefinition);
_specification = Mocker.Resolve<IndexerTagSpecification>();
_fakeArtist = Builder<Artist>.CreateNew()
.With(c => c.Monitored = true)
.With(c => c.Tags = new HashSet<int>())
.Build();
_fakeRelease = new ReleaseInfo
{
IndexerId = 1
};
_firstAlbum = new Album { Monitored = true };
_secondAlbum = new Album { Monitored = true };
var doubleEpisodeList = new List<Album> { _firstAlbum, _secondAlbum };
_parseResultMulti = new RemoteAlbum
{
Artist = _fakeArtist,
Albums = doubleEpisodeList,
Release = _fakeRelease
};
}
[Test]
public void indexer_and_series_without_tags_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int>();
_fakeArtist.Tags = new HashSet<int>();
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void indexer_with_tags_series_without_tags_should_return_false()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 123 };
_fakeArtist.Tags = new HashSet<int>();
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse();
}
[Test]
public void indexer_without_tags_series_with_tags_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int>();
_fakeArtist.Tags = new HashSet<int> { 123 };
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void indexer_with_tags_series_with_matching_tags_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 123, 456 };
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void indexer_with_tags_series_with_different_tags_should_return_false()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeFalse();
}
[Test]
public void release_without_indexerid_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
_fakeRelease.IndexerId = 0;
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void release_with_invalid_indexerid_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeArtist.Tags = new HashSet<int> { 123, 789 };
_fakeRelease.IndexerId = 2;
_specification.IsSatisfiedBy(_parseResultMulti, new AlbumSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
}
}

@ -1,10 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.FileList;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Indexers.Omgwtfnzbs;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.IndexerTests
_indexers = new List<IIndexer>(); _indexers = new List<IIndexer>();
_indexers.Add(Mocker.Resolve<Newznab>()); _indexers.Add(Mocker.Resolve<Newznab>());
_indexers.Add(Mocker.Resolve<Omgwtfnzbs>()); _indexers.Add(Mocker.Resolve<FileList>());
Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers); Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers);
} }

@ -1,55 +0,0 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Omgwtfnzbs;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests
{
[TestFixture]
public class OmgwtfnzbsFixture : CoreTest<Omgwtfnzbs>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Omgwtfnzbs",
Settings = new OmgwtfnzbsSettings()
{
ApiKey = "xxx",
Username = "me@my.domain"
}
};
}
[Test]
public void should_parse_recent_feed_from_omgwtfnzbs()
{
var recentFeed = ReadAllText(@"Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(100);
var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Stephen.Fry.Gadget.Man.S01E05.HDTV.x264-C4TV");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
releaseInfo.DownloadUrl.Should().Be("http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone");
releaseInfo.InfoUrl.Should().Be("http://omgwtfnzbs.org/details.php?id=OAl4g");
releaseInfo.CommentUrl.Should().BeNullOrEmpty();
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2012/12/17 23:30:13"));
releaseInfo.Size.Should().Be(236822906);
}
}
}

@ -1,56 +0,0 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Waffles;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.WafflesTests
{
[TestFixture]
public class WafflesFixture : CoreTest<Waffles>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Waffles",
Settings = new WafflesSettings()
{
UserId = "xxx",
RssPasskey = "123456789"
}
};
}
[Test]
public void should_parse_recent_feed_from_waffles()
{
var recentFeed = ReadAllText(@"Files/Indexers/Waffles/waffles.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(15);
var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("Coldplay - Kaleidoscope EP (FLAC HD) [2017-Web-FLAC-Lossless]");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
releaseInfo.DownloadUrl.Should().Be("https://waffles.ch/download.php/xxx/1166992/" +
"Coldplay%20-%20Kaleidoscope%20EP%20%28FLAC%20HD%29%20%5B2017-Web-FLAC-Lossless%5D.torrent?passkey=123456789&uid=xxx&rss=1");
releaseInfo.InfoUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1");
releaseInfo.CommentUrl.Should().Be("https://waffles.ch/details.php?id=1166992&hit=1");
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2017-07-16 09:51:54"));
releaseInfo.Size.Should().Be(552668227);
}
}
}

@ -0,0 +1,17 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(059)]
public class add_indexer_tags : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Omgwtfnzbs'");
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Waffles'");
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
}
}
}

@ -68,8 +68,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,56 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
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 IIndexerFactory _indexerFactory;
public IndexerTagSpecification(Logger logger, IIndexerFactory indexerFactory)
{
_logger = logger;
_indexerFactory = indexerFactory;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria)
{
if (subject.Release == null || subject.Artist?.Tags == null || subject.Release.IndexerId == 0)
{
return Decision.Accept();
}
IndexerDefinition indexer;
try
{
indexer = _indexerFactory.Get(subject.Release.IndexerId);
}
catch (ModelNotFoundException)
{
_logger.Debug("Indexer with id {0} does not exist, skipping indexer tags check", subject.Release.IndexerId);
return Decision.Accept();
}
// If indexer has tags, check that at least one of them is present on the series
var indexerTags = indexer.Tags;
if (indexerTags.Any() && indexerTags.Intersect(subject.Artist.Tags).Empty())
{
_logger.Debug("Indexer {0} has tags. None of these are present on artist {1}. Rejecting", subject.Release.Indexer, subject.Artist);
return Decision.Reject("Artist tags do not match any of the indexer tags");
}
return Decision.Accept();
}
}
}

@ -125,6 +125,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.Artist.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);

@ -47,7 +47,6 @@ namespace NzbDrone.Core.Indexers.Newznab
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws")); yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws"));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info")); yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net")); yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net"));
yield return GetDefinition("omgwtfnzbs", GetSettings("https://api.omgwtfnzbs.me"));
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com")); yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com")); yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com"));
yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api")); yield return GetDefinition("Tabula Rasa", GetSettings("https://www.tabula-rasa.pw", apiPath: @"/api/v1/api"));

@ -1,29 +0,0 @@
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
{
public class Omgwtfnzbs : HttpIndexerBase<OmgwtfnzbsSettings>
{
public override string Name => "omgwtfnzbs";
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
public Omgwtfnzbs(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new OmgwtfnzbsRequestGenerator() { Settings = Settings };
}
public override IParseIndexerResponse GetParser()
{
return new OmgwtfnzbsRssParser();
}
}
}

@ -1,65 +0,0 @@
using System.Collections.Generic;
using System.Text;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
{
public class OmgwtfnzbsRequestGenerator : IIndexerRequestGenerator
{
public string BaseUrl { get; set; }
public OmgwtfnzbsSettings Settings { get; set; }
public OmgwtfnzbsRequestGenerator()
{
BaseUrl = "https://rss.omgwtfnzbs.me/rss-download.php";
}
public virtual IndexerPageableRequestChain GetRecentRequests()
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(null));
return pageableRequests;
}
public virtual IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}+{1}",
searchCriteria.ArtistQuery,
searchCriteria.AlbumQuery)));
return pageableRequests;
}
public virtual IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}",
searchCriteria.ArtistQuery)));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(string query)
{
var url = new StringBuilder();
// Category 22 is Music-FLAC, category 7 is Music-MP3
url.AppendFormat("{0}?catid=22,7&user={1}&api={2}&eng=1&delay={3}", BaseUrl, Settings.Username, Settings.ApiKey, Settings.Delay);
if (query.IsNotNullOrWhiteSpace())
{
url = url.Replace("rss-download.php", "rss-search.php");
url.AppendFormat("&search={0}", query);
}
yield return new IndexerRequest(url.ToString(), HttpAccept.Rss);
}
}
}

@ -1,50 +0,0 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers.Exceptions;
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
{
public class OmgwtfnzbsRssParser : RssParser
{
public OmgwtfnzbsRssParser()
{
UseEnclosureUrl = true;
UseEnclosureLength = true;
}
protected override bool PreProcess(IndexerResponse indexerResponse)
{
var xdoc = LoadXmlDocument(indexerResponse);
var notice = xdoc.Descendants("notice").FirstOrDefault();
if (notice == null)
{
return true;
}
if (!notice.Value.ContainsIgnoreCase("api"))
{
return true;
}
throw new ApiKeyException(notice.Value);
}
protected override string GetInfoUrl(XElement item)
{
//Todo: Me thinks I need to parse details to get this...
var match = Regex.Match(item.Description(),
@"(?:\<b\>View NZB\:\<\/b\>\s\<a\shref\=\"")(?<URL>.+)(?:\""\starget)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (match.Success)
{
return match.Groups["URL"].Value;
}
return string.Empty;
}
}
}

@ -1,46 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Omgwtfnzbs
{
public class OmgwtfnzbsSettingsValidator : AbstractValidator<OmgwtfnzbsSettings>
{
public OmgwtfnzbsSettingsValidator()
{
RuleFor(c => c.Username).NotEmpty();
RuleFor(c => c.ApiKey).NotEmpty();
RuleFor(c => c.Delay).GreaterThanOrEqualTo(0);
}
}
public class OmgwtfnzbsSettings : IIndexerSettings
{
private static readonly OmgwtfnzbsSettingsValidator Validator = new OmgwtfnzbsSettingsValidator();
public OmgwtfnzbsSettings()
{
Delay = 30;
}
// Unused since Omg has a hardcoded url.
public string BaseUrl { get; set; }
[FieldDefinition(0, Label = "Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(1, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(2, Label = "Delay", HelpText = "Time in minutes to delay new nzbs before they appear on the RSS feed", Advanced = true)]
public int Delay { get; set; }
[FieldDefinition(3, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)]
public int? EarlyReleaseLimit { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -1,30 +0,0 @@
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers.Waffles
{
public class Waffles : HttpIndexerBase<WafflesSettings>
{
public override string Name => "Waffles";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override int PageSize => 15;
public Waffles(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new WafflesRequestGenerator() { Settings = Settings };
}
public override IParseIndexerResponse GetParser()
{
return new WafflesRssParser() { ParseSizeInDescription = true, ParseSeedersInDescription = true };
}
}
}

@ -1,63 +0,0 @@
using System.Collections.Generic;
using System.Text;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.Indexers.Waffles
{
public class WafflesRequestGenerator : IIndexerRequestGenerator
{
public WafflesSettings Settings { get; set; }
public int MaxPages { get; set; }
public WafflesRequestGenerator()
{
MaxPages = 5;
}
public virtual IndexerPageableRequestChain GetRecentRequests()
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(MaxPages, null));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(MaxPages, string.Format("&q=artist:{0} album:{1}", searchCriteria.ArtistQuery, searchCriteria.AlbumQuery)));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(MaxPages, string.Format("&q=artist:{0}", searchCriteria.ArtistQuery)));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, string query)
{
var url = new StringBuilder();
url.AppendFormat("{0}/browse.php?rss=1&c0=1&uid={1}&passkey={2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.UserId, Settings.RssPasskey);
if (query.IsNotNullOrWhiteSpace())
{
url.AppendFormat(query);
}
for (var page = 0; page < maxPages; page++)
{
yield return new IndexerRequest(string.Format("{0}&p={1}", url, page), HttpAccept.Rss);
}
}
}
}

@ -1,93 +0,0 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Waffles
{
public class WafflesRssParser : TorrentRssParser
{
public const string ns = "{http://purl.org/rss/1.0/}";
public const string dc = "{http://purl.org/dc/elements/1.1/}";
protected override bool PreProcess(IndexerResponse indexerResponse)
{
var xdoc = LoadXmlDocument(indexerResponse);
var error = xdoc.Descendants("error").FirstOrDefault();
if (error == null)
{
return true;
}
var code = Convert.ToInt32(error.Attribute("code").Value);
var errorMessage = error.Attribute("description").Value;
if (code >= 100 && code <= 199)
{
throw new ApiKeyException("Invalid Pass key");
}
if (!indexerResponse.Request.Url.FullUri.Contains("passkey=") && errorMessage == "Missing parameter")
{
throw new ApiKeyException("Indexer requires an Pass key");
}
if (errorMessage == "Request limit reached")
{
throw new RequestLimitReachedException("API limit reached");
}
throw new IndexerException(indexerResponse, errorMessage);
}
protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo)
{
var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo;
return torrentInfo;
}
protected override string GetInfoUrl(XElement item)
{
return ParseUrl(item.TryGetValue("comments").TrimEnd("#comments"));
}
protected override string GetCommentUrl(XElement item)
{
return ParseUrl(item.TryGetValue("comments"));
}
private static readonly Regex ParseSizeRegex = new Regex(@"(?:Size: )(?<value>\d+)<",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
protected override long GetSize(XElement item)
{
var match = ParseSizeRegex.Matches(item.Element("description").Value);
if (match.Count != 0)
{
var value = decimal.Parse(Regex.Replace(match[0].Groups["value"].Value, "\\,", ""), CultureInfo.InvariantCulture);
return (long)value;
}
return 0;
}
protected override DateTime GetPublishDate(XElement item)
{
var dateString = item.TryGetValue(dc + "date");
if (dateString.IsNullOrWhiteSpace())
{
throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date.");
}
return XElementExtensions.ParseDate(dateString);
}
}
}

@ -1,50 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Waffles
{
public class WafflesSettingsValidator : AbstractValidator<WafflesSettings>
{
public WafflesSettingsValidator()
{
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.UserId).NotEmpty();
RuleFor(c => c.RssPasskey).NotEmpty();
}
}
public class WafflesSettings : ITorrentIndexerSettings
{
private static readonly WafflesSettingsValidator Validator = new WafflesSettingsValidator();
public WafflesSettings()
{
BaseUrl = "https://www.waffles.ch";
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
}
[FieldDefinition(0, Label = "Website URL")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "UserId", Privacy = PrivacyLevel.UserName)]
public string UserId { get; set; }
[FieldDefinition(2, Label = "RSS Passkey", Privacy = PrivacyLevel.ApiKey)]
public string RssPasskey { get; set; }
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
public int MinimumSeeders { get; set; }
[FieldDefinition(4)]
public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings();
[FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)]
public int? EarlyReleaseLimit { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -269,6 +269,7 @@
"IncludeUnknownArtistItemsHelpText": "Show items without a artist in the queue, this could include removed artists, movies or anything else in Lidarr's category", "IncludeUnknownArtistItemsHelpText": "Show items without a artist in the queue, this could include removed artists, movies or anything else in Lidarr's category",
"IncludeUnmonitored": "Include Unmonitored", "IncludeUnmonitored": "Include Unmonitored",
"Indexer": "Indexer", "Indexer": "Indexer",
"IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
"IndexerIdHelpText": "Specify what indexer the profile applies to", "IndexerIdHelpText": "Specify what indexer the profile applies to",
"IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed", "IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed",
"IndexerIdvalue0IncludeInPreferredWordsRenamingFormat": "Include in {Preferred Words} renaming format", "IndexerIdvalue0IncludeInPreferredWordsRenamingFormat": "Include in {Preferred Words} renaming format",
@ -276,6 +277,7 @@
"IndexerPriority": "Indexer Priority", "IndexerPriority": "Indexer Priority",
"Indexers": "Indexers", "Indexers": "Indexers",
"IndexerSettings": "Indexer Settings", "IndexerSettings": "Indexer Settings",
"IndexerTagHelpText": "Only use this indexer for artist with at least one matching tag. Leave blank to use with all artists.",
"InstanceName": "Instance Name", "InstanceName": "Instance Name",
"InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name",
"InteractiveSearch": "Interactive Search", "InteractiveSearch": "Interactive Search",
@ -321,8 +323,8 @@
"ManualImport": "Manual Import", "ManualImport": "Manual Import",
"MarkAsFailed": "Mark as Failed", "MarkAsFailed": "Mark as Failed",
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?", "MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
"MassAlbumsSearchWarning": "Are you sure you want to search for all '{0}' missing albums?",
"MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?", "MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?",
"MassAlbumsSearchWarning": "Are you sure you want to search for all '{0}' missing albums?",
"MaximumLimits": "Maximum Limits", "MaximumLimits": "Maximum Limits",
"MaximumSize": "Maximum Size", "MaximumSize": "Maximum Size",
"MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.", "MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.",
@ -550,9 +552,9 @@
"SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.", "SetPermissionsLinuxHelpTextWarning": "If you're unsure what these settings do, do not alter them.",
"Settings": "Settings", "Settings": "Settings",
"ShortDateFormat": "Short Date Format", "ShortDateFormat": "Short Date Format",
"ShouldMonitorHelpText": "Monitor artists and albums added from this list",
"ShouldMonitorExisting": "Monitor existing albums", "ShouldMonitorExisting": "Monitor existing albums",
"ShouldMonitorExistingHelpText": "Automatically monitor albums on this list which are already in Lidarr", "ShouldMonitorExistingHelpText": "Automatically monitor albums on this list which are already in Lidarr",
"ShouldMonitorHelpText": "Monitor artists and albums added from this list",
"ShouldSearch": "Search for New Items", "ShouldSearch": "Search for New Items",
"ShouldSearchHelpText": "Search indexers for newly added items. Use with caution for large lists.", "ShouldSearchHelpText": "Search indexers for newly added items. Use with caution for large lists.",
"ShowAlbumCount": "Show Album Count", "ShowAlbumCount": "Show Album Count",

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

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
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.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
@ -33,6 +34,7 @@ namespace NzbDrone.Core.Tags
private readonly IReleaseProfileService _releaseProfileService; private readonly IReleaseProfileService _releaseProfileService;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
private readonly IIndexerFactory _indexerService;
public TagService(ITagRepository repo, public TagService(ITagRepository repo,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
@ -41,7 +43,8 @@ namespace NzbDrone.Core.Tags
INotificationFactory notificationFactory, INotificationFactory notificationFactory,
IReleaseProfileService releaseProfileService, IReleaseProfileService releaseProfileService,
IArtistService artistService, IArtistService artistService,
IRootFolderService rootFolderService) IRootFolderService rootFolderService,
IIndexerFactory indexerService)
{ {
_repo = repo; _repo = repo;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@ -51,6 +54,7 @@ namespace NzbDrone.Core.Tags
_releaseProfileService = releaseProfileService; _releaseProfileService = releaseProfileService;
_artistService = artistService; _artistService = artistService;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
_indexerService = indexerService;
} }
public Tag GetTag(int tagId) public Tag GetTag(int tagId)
@ -79,6 +83,7 @@ namespace NzbDrone.Core.Tags
var restrictions = _releaseProfileService.AllForTag(tagId); var restrictions = _releaseProfileService.AllForTag(tagId);
var artist = _artistService.AllForTag(tagId); var artist = _artistService.AllForTag(tagId);
var rootFolders = _rootFolderService.AllForTag(tagId); var rootFolders = _rootFolderService.AllForTag(tagId);
var indexers = _indexerService.AllForTag(tagId);
return new TagDetails return new TagDetails
{ {
@ -89,7 +94,8 @@ 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(),
ArtistIds = artist.Select(c => c.Id).ToList(), ArtistIds = artist.Select(c => c.Id).ToList(),
RootFolderIds = rootFolders.Select(c => c.Id).ToList() RootFolderIds = rootFolders.Select(c => c.Id).ToList(),
IndexerIds = indexers.Select(c => c.Id).ToList()
}; };
} }
@ -102,6 +108,7 @@ namespace NzbDrone.Core.Tags
var restrictions = _releaseProfileService.All(); var restrictions = _releaseProfileService.All();
var artists = _artistService.GetAllArtists(); var artists = _artistService.GetAllArtists();
var rootFolders = _rootFolderService.All(); var rootFolders = _rootFolderService.All();
var indexers = _indexerService.All();
var details = new List<TagDetails>(); var details = new List<TagDetails>();
@ -116,7 +123,8 @@ 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(),
ArtistIds = artists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), ArtistIds = artists.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(),
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
}); });
} }

Loading…
Cancel
Save