diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
new file mode 100644
index 000000000..162c79885
--- /dev/null
+++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
@@ -0,0 +1,98 @@
+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 values = items
+ .filter((downloadClient) => downloadClient.protocol === protocolFilter)
+ .sort(sortByName)
+ .map((downloadClient) => ({
+ 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 f25946f59..9601be8de 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -10,6 +10,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
+import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
@@ -72,6 +73,9 @@ function getComponent(type) {
case inputTypes.CATEGORY_SELECT:
return NewznabCategorySelectInputConnector;
+ case inputTypes.DOWNLOAD_CLIENT_SELECT:
+ return DownloadClientSelectInputConnector;
+
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js
index 7a11bb0c7..d26d08616 100644
--- a/frontend/src/Helpers/Props/inputTypes.js
+++ b/frontend/src/Helpers/Props/inputTypes.js
@@ -9,6 +9,7 @@ export const KEY_VALUE_LIST = 'keyValueList';
export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
+export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js
index b83522fcf..5afc22a08 100644
--- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js
+++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js
@@ -48,7 +48,9 @@ function EditIndexerModalContent(props) {
appProfileId,
tags,
fields,
- priority
+ priority,
+ protocol,
+ downloadClientId
} = item;
const indexerDisplayName = implementationName === definitionName ? implementationName : `${implementationName} (${definitionName})`;
@@ -156,6 +158,23 @@ function EditIndexerModalContent(props) {
/>
+
+ {translate('DownloadClient')}
+
+
+
+
{translate('Tags')}
diff --git a/src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs b/src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs
new file mode 100644
index 000000000..658bbaaaa
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs
@@ -0,0 +1,14 @@
+using FluentMigrator;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(035)]
+ 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..c69f4a01d 100644
--- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs
+++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs
@@ -2,13 +2,14 @@
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 @@ namespace NzbDrone.Core.Download
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,23 @@ namespace NzbDrone.Core.Download
return null;
}
+ if (indexerId > 0)
+ {
+ var indexer = _indexerFactory.Find(indexerId);
+
+ if (indexer is { DownloadClientId: > 0 })
+ {
+ var client = availableProviders.SingleOrDefault(d => d.Definition.Id == indexer.DownloadClientId);
+
+ if (client == null)
+ {
+ throw new DownloadClientUnavailableException("Indexer specified download client is not available");
+ }
+
+ return client;
+ }
+ }
+
var blockedProviders = new HashSet(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
if (blockedProviders.Any())
@@ -54,7 +78,7 @@ namespace NzbDrone.Core.Download
}
// Use the first priority clients first
- availableProviders = availableProviders.GroupBy(v => (v.Definition as DownloadClientDefinition).Priority)
+ availableProviders = availableProviders.GroupBy(v => ((DownloadClientDefinition)v.Definition).Priority)
.OrderBy(v => v.Key)
.First().OrderBy(v => v.Definition.Id).ToList();
diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs
index d0a15115e..d5e6368f1 100644
--- a/src/NzbDrone.Core/Download/DownloadService.cs
+++ b/src/NzbDrone.Core/Download/DownloadService.cs
@@ -16,7 +16,7 @@ namespace NzbDrone.Core.Download
{
public interface IDownloadService
{
- Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect);
+ Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect, int? downloadClientId);
Task DownloadReport(string link, int indexerId, string source, string host, string title);
void RecordRedirect(string link, int indexerId, string source, string host, string title);
}
@@ -48,10 +48,18 @@ namespace NzbDrone.Core.Download
_logger = logger;
}
- public async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect)
+ public async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect, int? downloadClientId)
+ {
+ var downloadClient = downloadClientId.HasValue
+ ? _downloadClientProvider.Get(downloadClientId.Value)
+ : _downloadClientProvider.GetDownloadClient(release.DownloadProtocol, release.IndexerId);
+
+ await SendReportToClient(release, source, host, redirect, downloadClient);
+ }
+
+ private async Task SendReportToClient(ReleaseInfo release, string source, string host, bool redirect, IDownloadClient downloadClient)
{
var downloadTitle = release.Title;
- var downloadClient = _downloadClientProvider.GetDownloadClient(release.DownloadProtocol);
if (downloadClient == null)
{
diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs
index e51d5f983..a0b1df0a9 100644
--- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs
+++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs
@@ -24,6 +24,7 @@ namespace NzbDrone.Core.Indexers
public IndexerCapabilities Capabilities { get; set; }
public int Priority { get; set; } = 25;
public bool Redirect { get; set; }
+ public int DownloadClientId { get; set; }
public DateTime Added { get; set; }
public int AppProfileId { get; set; }
public LazyLoaded AppProfile { get; set; }
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 161e05ef3..497e5b573 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -204,6 +204,7 @@
"IndexerCategories": "Indexer Categories",
"IndexerDetails": "Indexer Details",
"IndexerDisabled": "Indexer Disabled",
+ "IndexerDownloadClientHelpText": "Specify which download client is used for grabs made within Prowlarr from this indexer",
"IndexerFailureRate": "Indexer Failure Rate",
"IndexerFlags": "Indexer Flags",
"IndexerHealthCheckNoIndexers": "No indexers enabled, Prowlarr will not return search results",
diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs
index b0182bc04..9ba47c50b 100644
--- a/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs
+++ b/src/Prowlarr.Api.V1/Indexers/IndexerResource.cs
@@ -30,6 +30,7 @@ namespace Prowlarr.Api.V1.Indexers
public IndexerPrivacy Privacy { get; set; }
public IndexerCapabilityResource Capabilities { get; set; }
public int Priority { get; set; }
+ public int DownloadClientId { get; set; }
public DateTime Added { get; set; }
public IndexerStatusResource Status { get; set; }
public string SortName { get; set; }
@@ -96,6 +97,7 @@ namespace Prowlarr.Api.V1.Indexers
resource.Protocol = definition.Protocol;
resource.Privacy = definition.Privacy;
resource.Priority = definition.Priority;
+ resource.DownloadClientId = definition.DownloadClientId;
resource.Added = definition.Added;
resource.SortName = definition.Name.NormalizeTitle();
@@ -142,6 +144,7 @@ namespace Prowlarr.Api.V1.Indexers
definition.IndexerUrls = resource.IndexerUrls;
definition.Priority = resource.Priority;
definition.Privacy = resource.Privacy;
+ definition.DownloadClientId = resource.DownloadClientId;
definition.Added = resource.Added;
return definition;
diff --git a/src/Prowlarr.Api.V1/Search/SearchController.cs b/src/Prowlarr.Api.V1/Search/SearchController.cs
index a0deb14ca..7b81df58f 100644
--- a/src/Prowlarr.Api.V1/Search/SearchController.cs
+++ b/src/Prowlarr.Api.V1/Search/SearchController.cs
@@ -73,7 +73,7 @@ namespace Prowlarr.Api.V1.Search
try
{
- _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect);
+ _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, null).GetAwaiter().GetResult();
}
catch (ReleaseDownloadException ex)
{
@@ -106,7 +106,7 @@ namespace Prowlarr.Api.V1.Search
try
{
- _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect);
+ _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, null).GetAwaiter().GetResult();
}
catch (ReleaseDownloadException ex)
{