diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
new file mode 100644
index 000000000..a83986d8f
--- /dev/null
+++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
@@ -0,0 +1,100 @@
+import _ from 'lodash';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { fetchDownloadClients } from 'Store/Actions/settingsActions';
+import sortByName from 'Utilities/Array/sortByName';
+import EnhancedSelectInput from './EnhancedSelectInput';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.settings.downloadClients,
+ (state, { includeAny }) => includeAny,
+ (state, { protocol }) => protocol,
+ (downloadClients, includeAny, protocolFilter) => {
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ items
+ } = downloadClients;
+
+ const filteredItems = items.filter((item) => item.protocol === protocolFilter);
+
+ const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
+ return {
+ key: downloadClient.id,
+ value: downloadClient.name
+ };
+ });
+
+ if (includeAny) {
+ values.unshift({
+ key: 0,
+ value: '(Any)'
+ });
+ }
+
+ return {
+ isFetching,
+ isPopulated,
+ error,
+ values
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ dispatchFetchDownloadClients: fetchDownloadClients
+};
+
+class DownloadClientSelectInputConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount() {
+ if (!this.props.isPopulated) {
+ this.props.dispatchFetchDownloadClients();
+ }
+ }
+
+ //
+ // Listeners
+
+ onChange = ({ name, value }) => {
+ this.props.onChange({ name, value: parseInt(value) });
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+DownloadClientSelectInputConnector.propTypes = {
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ values: PropTypes.arrayOf(PropTypes.object).isRequired,
+ includeAny: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ dispatchFetchDownloadClients: PropTypes.func.isRequired
+};
+
+DownloadClientSelectInputConnector.defaultProps = {
+ includeAny: false,
+ protocol: 'torrent'
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);
diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js
index 1c50e3398..5327fe616 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -8,6 +8,7 @@ import AvailabilitySelectInput from './AvailabilitySelectInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
+import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
@@ -73,6 +74,9 @@ function getComponent(type) {
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
+ case inputTypes.DOWNLOAD_CLIENT_SELECT:
+ return DownloadClientSelectInputConnector;
+
case inputTypes.LANGUAGE_SELECT:
return LanguageSelectInputConnector;
diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js
index cd4ef89d9..ac031d4f0 100644
--- a/frontend/src/Helpers/Props/inputTypes.js
+++ b/frontend/src/Helpers/Props/inputTypes.js
@@ -13,6 +13,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect';
+export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const SELECT = 'select';
export const DYNAMIC_SELECT = 'dynamicSelect';
export const TAG = 'tag';
@@ -35,6 +36,7 @@ export const all = [
PASSWORD,
PATH,
QUALITY_PROFILE_SELECT,
+ DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT,
LANGUAGE_SELECT,
diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js
index aac13e5c2..0d0aae42c 100644
--- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js
+++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js
@@ -45,7 +45,9 @@ function EditIndexerModalContent(props) {
supportsSearch,
tags,
fields,
- priority
+ priority,
+ protocol,
+ downloadClientId
} = item;
return (
@@ -154,6 +156,23 @@ function EditIndexerModalContent(props) {
/>
+
+ {translate('DownloadClient')}
+
+
+
+
Tags
diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs
index 1387ec3fd..0e6973152 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs
@@ -1,10 +1,11 @@
-using System;
+using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
+using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
@@ -67,6 +68,17 @@ private Mock WithTorrentClient(int priority = 0)
return mock;
}
+ private void WithTorrentIndexer(int downloadClientId)
+ {
+ Mocker.GetMock()
+ .Setup(v => v.Find(It.IsAny()))
+ .Returns(Builder
+ .CreateNew()
+ .With(v => v.Id = _nextId++)
+ .With(v => v.DownloadClientId = downloadClientId)
+ .Build());
+ }
+
private void GivenBlockedClient(int id)
{
_blockedProviders.Add(new DownloadClientStatus
@@ -223,5 +235,39 @@ public void should_not_skip_secondary_prio_torrent_client_if_primary_blocked()
client3.Definition.Id.Should().Be(2);
client4.Definition.Id.Should().Be(3);
}
+
+ [Test]
+ public void should_always_choose_indexer_client()
+ {
+ WithUsenetClient();
+ WithTorrentClient();
+ WithTorrentClient();
+ WithTorrentClient();
+ WithTorrentIndexer(3);
+
+ var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
+ var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
+ var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
+ var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
+ var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 1);
+
+ client1.Definition.Id.Should().Be(3);
+ client2.Definition.Id.Should().Be(3);
+ client3.Definition.Id.Should().Be(3);
+ client4.Definition.Id.Should().Be(3);
+ client5.Definition.Id.Should().Be(3);
+ }
+
+ [Test]
+ public void should_fail_to_choose_client_when_indexer_reference_does_not_exist()
+ {
+ WithUsenetClient();
+ WithTorrentClient();
+ WithTorrentClient();
+ WithTorrentClient();
+ WithTorrentIndexer(5);
+
+ Assert.Throws(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 1));
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
index 6831f5701..55c02bca8 100644
--- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
@@ -31,8 +31,8 @@ public void Setup()
.Returns(_downloadClients);
Mocker.GetMock()
- .Setup(v => v.GetDownloadClient(It.IsAny()))
- .Returns(v => _downloadClients.FirstOrDefault(d => d.Protocol == v));
+ .Setup(v => v.GetDownloadClient(It.IsAny(), It.IsAny()))
+ .Returns((v, i) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
var releaseInfo = Builder.CreateNew()
.With(v => v.DownloadProtocol = DownloadProtocol.Usenet)
diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs
index 8367acf05..33527e89f 100644
--- a/src/NzbDrone.Core/Datastore/BasicRepository.cs
+++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs
@@ -17,6 +17,7 @@ public interface IBasicRepository
IEnumerable All();
int Count();
TModel Get(int id);
+ TModel Find(int id);
TModel Insert(TModel model);
TModel Update(TModel model);
TModel Upsert(TModel model);
@@ -99,6 +100,13 @@ public TModel Get(int id)
return model;
}
+ public TModel Find(int id)
+ {
+ var model = Query(c => c.Id == id).SingleOrDefault();
+
+ return model;
+ }
+
public IEnumerable Get(IEnumerable ids)
{
if (!ids.Any())
diff --git a/src/NzbDrone.Core/Datastore/Migration/205_download_client_per_indexer.cs b/src/NzbDrone.Core/Datastore/Migration/205_download_client_per_indexer.cs
new file mode 100644
index 000000000..7baf89546
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/205_download_client_per_indexer.cs
@@ -0,0 +1,14 @@
+using FluentMigrator;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(205)]
+ public class download_client_per_indexer : NzbDroneMigrationBase
+ {
+ protected override void MainDbUpgrade()
+ {
+ Alter.Table("Indexers").AddColumn("DownloadClientId").AsInt32().WithDefaultValue(0);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs
index 3ad8f6615..3fa2bcc39 100644
--- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs
+++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs
@@ -1,14 +1,15 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
+using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Download
{
public interface IProvideDownloadClient
{
- IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol);
+ IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0);
IEnumerable GetDownloadClients();
IDownloadClient Get(int id);
}
@@ -18,17 +19,23 @@ public class DownloadClientProvider : IProvideDownloadClient
private readonly Logger _logger;
private readonly IDownloadClientFactory _downloadClientFactory;
private readonly IDownloadClientStatusService _downloadClientStatusService;
+ private readonly IIndexerFactory _indexerFactory;
private readonly ICached _lastUsedDownloadClient;
- public DownloadClientProvider(IDownloadClientStatusService downloadClientStatusService, IDownloadClientFactory downloadClientFactory, ICacheManager cacheManager, Logger logger)
+ public DownloadClientProvider(IDownloadClientStatusService downloadClientStatusService,
+ IDownloadClientFactory downloadClientFactory,
+ IIndexerFactory indexerFactory,
+ ICacheManager cacheManager,
+ Logger logger)
{
_logger = logger;
_downloadClientFactory = downloadClientFactory;
_downloadClientStatusService = downloadClientStatusService;
+ _indexerFactory = indexerFactory;
_lastUsedDownloadClient = cacheManager.GetCache(GetType(), "lastDownloadClientId");
}
- public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol)
+ public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol, int indexerId = 0)
{
var availableProviders = _downloadClientFactory.GetAvailableProviders().Where(v => v.Protocol == downloadProtocol).ToList();
@@ -37,6 +44,18 @@ public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol)
return null;
}
+ if (indexerId > 0)
+ {
+ var indexer = _indexerFactory.Find(indexerId);
+
+ if (indexer != null && indexer.DownloadClientId > 0)
+ {
+ var client = availableProviders.SingleOrDefault(d => d.Definition.Id == indexer.DownloadClientId);
+
+ return client ?? throw new DownloadClientUnavailableException($"Indexer specified download client is not available");
+ }
+ }
+
var blockedProviders = new HashSet(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
if (blockedProviders.Any())
diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs
index 48395ab18..4b01ffd98 100644
--- a/src/NzbDrone.Core/Download/DownloadService.cs
+++ b/src/NzbDrone.Core/Download/DownloadService.cs
@@ -50,7 +50,7 @@ public void DownloadReport(RemoteMovie remoteMovie)
Ensure.That(remoteMovie.Movie, () => remoteMovie.Movie).IsNotNull();
var downloadTitle = remoteMovie.Release.Title;
- var downloadClient = _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol);
+ var downloadClient = _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol, remoteMovie.Release.IndexerId);
if (downloadClient == null)
{
diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs
index d43c18198..a6ed69130 100644
--- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs
+++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs
@@ -1,4 +1,4 @@
-using NzbDrone.Core.ThingiProvider;
+using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers
{
@@ -7,6 +7,7 @@ public class IndexerDefinition : ProviderDefinition
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
+ public int DownloadClientId { get; set; }
public DownloadProtocol Protocol { get; set; }
public bool SupportsRss { get; set; }
public bool SupportsSearch { get; set; }
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 9c312ed64..367e6ea2d 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -421,6 +421,7 @@
"IncludeUnknownMovieItemsHelpText": "Show items without a movie in the queue. This could include removed movies or anything else in Radarr's category",
"IncludeUnmonitored": "Include Unmonitored",
"Indexer": "Indexer",
+ "IndexerDownloadClientHelpText": "Specify which download client is used for grabs from this indexer",
"IndexerFlags": "Indexer Flags",
"IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {0}",
diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs
index bc52f9e16..3945d2a69 100644
--- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs
+++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs
@@ -9,6 +9,8 @@ public interface IProviderFactory
{
List All();
List GetAvailableProviders();
+ bool Exists(int id);
+ TProviderDefinition Find(int id);
TProviderDefinition Get(int id);
TProviderDefinition Create(TProviderDefinition definition);
void Update(TProviderDefinition definition);
diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs
index 35ec3d557..dd51a9d7a 100644
--- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs
+++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs
@@ -92,11 +92,21 @@ public List GetAvailableProviders()
return Active().Select(GetInstance).ToList();
}
+ public bool Exists(int id)
+ {
+ return _providerRepository.Find(id) != null;
+ }
+
public TProviderDefinition Get(int id)
{
return _providerRepository.Get(id);
}
+ public TProviderDefinition Find(int id)
+ {
+ return _providerRepository.Find(id);
+ }
+
public virtual TProviderDefinition Create(TProviderDefinition definition)
{
var addedDefinition = _providerRepository.Insert(definition);
diff --git a/src/Radarr.Api.V3/Indexers/IndexerResource.cs b/src/Radarr.Api.V3/Indexers/IndexerResource.cs
index f70852c2c..c5e02c2c2 100644
--- a/src/Radarr.Api.V3/Indexers/IndexerResource.cs
+++ b/src/Radarr.Api.V3/Indexers/IndexerResource.cs
@@ -11,6 +11,7 @@ public class IndexerResource : ProviderResource
public bool SupportsSearch { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
+ public int DownloadClientId { get; set; }
}
public class IndexerResourceMapper : ProviderResourceMapper
@@ -31,6 +32,7 @@ public override IndexerResource ToResource(IndexerDefinition definition)
resource.SupportsSearch = definition.SupportsSearch;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
+ resource.DownloadClientId = definition.DownloadClientId;
return resource;
}
@@ -48,6 +50,7 @@ public override IndexerDefinition ToModel(IndexerResource resource)
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
definition.Priority = resource.Priority;
+ definition.DownloadClientId = resource.DownloadClientId;
return definition;
}