From 826828e8eccb0b7e9fff355d0880d9bedaf2fe2f Mon Sep 17 00:00:00 2001 From: ricci2511 Date: Tue, 4 Jul 2023 01:52:12 +0200 Subject: [PATCH] New: Add download client per indexer setting --- .../DownloadClientSelectInputConnector.js | 98 +++++++++++++++++++ .../src/Components/Form/FormInputGroup.js | 4 + frontend/src/Helpers/Props/inputTypes.js | 1 + .../Indexer/Edit/EditIndexerModalContent.js | 21 +++- .../035_download_client_per_indexer.cs | 14 +++ .../Download/DownloadClientProvider.cs | 32 +++++- src/NzbDrone.Core/Download/DownloadService.cs | 14 ++- .../Indexers/IndexerDefinition.cs | 1 + src/NzbDrone.Core/Localization/Core/en.json | 1 + .../Indexers/IndexerResource.cs | 3 + .../Search/SearchController.cs | 4 +- 11 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 frontend/src/Components/Form/DownloadClientSelectInputConnector.js create mode 100644 src/NzbDrone.Core/Datastore/Migration/035_download_client_per_indexer.cs 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) {