New: Link indexer to specific download client

This commit is contained in:
Qstick 2021-12-21 22:48:20 -06:00
parent de05be62d7
commit 974e44ce48
15 changed files with 239 additions and 10 deletions

View File

@ -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 (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
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);

View File

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

View File

@ -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,

View File

@ -45,7 +45,9 @@ function EditIndexerModalContent(props) {
supportsSearch,
tags,
fields,
priority
priority,
protocol,
downloadClientId
} = item;
return (
@ -154,6 +156,23 @@ function EditIndexerModalContent(props) {
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Tags</FormLabel>

View File

@ -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 @@ namespace NzbDrone.Core.Test.Download
return mock;
}
private void WithTorrentIndexer(int downloadClientId)
{
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Find(It.IsAny<int>()))
.Returns(Builder<IndexerDefinition>
.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 @@ namespace NzbDrone.Core.Test.Download
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<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 1));
}
}
}

View File

@ -31,8 +31,8 @@ namespace NzbDrone.Core.Test.Download
.Returns(_downloadClients);
Mocker.GetMock<IProvideDownloadClient>()
.Setup(v => v.GetDownloadClient(It.IsAny<DownloadProtocol>()))
.Returns<DownloadProtocol>(v => _downloadClients.FirstOrDefault(d => d.Protocol == v));
.Setup(v => v.GetDownloadClient(It.IsAny<DownloadProtocol>(), It.IsAny<int>()))
.Returns<DownloadProtocol, int>((v, i) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
var releaseInfo = Builder<ReleaseInfo>.CreateNew()
.With(v => v.DownloadProtocol = DownloadProtocol.Usenet)

View File

@ -17,6 +17,7 @@ namespace NzbDrone.Core.Datastore
IEnumerable<TModel> 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 @@ namespace NzbDrone.Core.Datastore
return model;
}
public TModel Find(int id)
{
var model = Query(c => c.Id == id).SingleOrDefault();
return model;
}
public IEnumerable<TModel> Get(IEnumerable<int> ids)
{
if (!ids.Any())

View File

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

View File

@ -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<IDownloadClient> 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<int> _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<int>(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 @@ namespace NzbDrone.Core.Download
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<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));
if (blockedProviders.Any())

View File

@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download
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)
{

View File

@ -1,4 +1,4 @@
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers
{
@ -7,6 +7,7 @@ namespace NzbDrone.Core.Indexers
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; }

View File

@ -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}",

View File

@ -9,6 +9,8 @@ namespace NzbDrone.Core.ThingiProvider
{
List<TProviderDefinition> All();
List<TProvider> GetAvailableProviders();
bool Exists(int id);
TProviderDefinition Find(int id);
TProviderDefinition Get(int id);
TProviderDefinition Create(TProviderDefinition definition);
void Update(TProviderDefinition definition);

View File

@ -92,11 +92,21 @@ namespace NzbDrone.Core.ThingiProvider
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);

View File

@ -11,6 +11,7 @@ namespace Radarr.Api.V3.Indexers
public bool SupportsSearch { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
public int DownloadClientId { get; set; }
}
public class IndexerResourceMapper : ProviderResourceMapper<IndexerResource, IndexerDefinition>
@ -31,6 +32,7 @@ namespace Radarr.Api.V3.Indexers
resource.SupportsSearch = definition.SupportsSearch;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
resource.DownloadClientId = definition.DownloadClientId;
return resource;
}
@ -48,6 +50,7 @@ namespace Radarr.Api.V3.Indexers
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
definition.Priority = resource.Priority;
definition.DownloadClientId = resource.DownloadClientId;
return definition;
}