Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Link indexer to specific download client #2668

Merged
merged 2 commits into from Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
100 changes: 100 additions & 0 deletions 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 (
<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);
4 changes: 4 additions & 0 deletions frontend/src/Components/Form/FormInputGroup.js
Expand Up @@ -7,6 +7,7 @@ import AutoCompleteInput from './AutoCompleteInput';
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';
Expand Down Expand Up @@ -76,6 +77,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;

case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;

case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/Helpers/Props/inputTypes.js
Expand Up @@ -13,6 +13,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
export const SERIES_TYPE_SELECT = 'artistTypeSelect';
Expand All @@ -39,6 +40,7 @@ export const all = [
METADATA_PROFILE_SELECT,
ALBUM_RELEASE_SELECT,
INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,
SELECT,
DYNAMIC_SELECT,
Expand Down
Expand Up @@ -44,7 +44,9 @@ function EditIndexerModalContent(props) {
supportsRss,
supportsSearch,
fields,
priority
priority,
protocol,
downloadClientId
} = item;

return (
Expand Down Expand Up @@ -161,6 +163,23 @@ function EditIndexerModalContent(props) {
onChange={onInputChange}
/>
</FormGroup>

<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>DownloadClient</FormLabel>

<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={'Specify which download client is used for grabs from this indexer'}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
Expand Down
3 changes: 3 additions & 0 deletions src/Lidarr.Api.V1/Indexers/IndexerResource.cs
Expand Up @@ -11,6 +11,7 @@ public class IndexerResource : ProviderResource<IndexerResource>
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>
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
46 changes: 46 additions & 0 deletions src/NzbDrone.Core.Test/Download/DownloadClientProviderFixture.cs
Expand Up @@ -5,6 +5,7 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;

Expand Down Expand Up @@ -67,6 +68,17 @@ private Mock<IDownloadClient> WithTorrentClient(int priority = 0)
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
Expand Down Expand Up @@ -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<DownloadClientUnavailableException>(() => Subject.GetDownloadClient(DownloadProtocol.Torrent, 1));
}
}
}
4 changes: 2 additions & 2 deletions src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs
Expand Up @@ -31,8 +31,8 @@ public void Setup()
.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 episodes = Builder<Album>.CreateListOfSize(2)
.TheFirst(1).With(s => s.Id = 12)
Expand Down
@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;

namespace NzbDrone.Core.Datastore.Migration
{
[Migration(055)]
public class download_client_per_indexer : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Indexers").AddColumn("DownloadClientId").AsInt32().WithDefaultValue(0);
}
}
}
25 changes: 22 additions & 3 deletions src/NzbDrone.Core/Download/DownloadClientProvider.cs
Expand Up @@ -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<IDownloadClient> GetDownloadClients();
IDownloadClient Get(int id);
}
Expand All @@ -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<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();

Expand All @@ -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<int>(_downloadClientStatusService.GetBlockedProviders().Select(v => v.ProviderId));

if (blockedProviders.Any())
Expand Down
2 changes: 1 addition & 1 deletion src/NzbDrone.Core/Download/DownloadService.cs
Expand Up @@ -51,7 +51,7 @@ public void DownloadReport(RemoteAlbum remoteAlbum)
Ensure.That(remoteAlbum.Albums, () => remoteAlbum.Albums).HasItems();

var downloadTitle = remoteAlbum.Release.Title;
var downloadClient = _downloadClientProvider.GetDownloadClient(remoteAlbum.Release.DownloadProtocol);
var downloadClient = _downloadClientProvider.GetDownloadClient(remoteAlbum.Release.DownloadProtocol, remoteAlbum.Release.IndexerId);

if (downloadClient == null)
{
Expand Down
1 change: 1 addition & 0 deletions src/NzbDrone.Core/Indexers/IndexerDefinition.cs
Expand Up @@ -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; }
Expand Down
1 change: 1 addition & 0 deletions src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs
Expand Up @@ -10,6 +10,7 @@ public interface IProviderFactory<TProvider, TProviderDefinition>
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);
Expand Down
5 changes: 5 additions & 0 deletions src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs
Expand Up @@ -102,6 +102,11 @@ 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);
Expand Down