Upstream Changes to DownloadClients and Indexers

This commit is contained in:
Qstick 2017-10-26 23:21:06 -04:00
commit 13bfb73ee9
80 changed files with 1521 additions and 654 deletions

View file

@ -60,6 +60,11 @@ namespace NzbDrone.Common.Reflection
return (T)attribute; return (T)attribute;
} }
public static T[] GetAttributes<T>(this MemberInfo member) where T : Attribute
{
return member.GetCustomAttributes(typeof(T), false).OfType<T>().ToArray();
}
public static Type FindTypeByName(this Assembly assembly, string name) public static Type FindTypeByName(this Assembly assembly, string name)
{ {
return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); return assembly.GetTypes().SingleOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));

View file

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests
{
[TestFixture]
public class BlockedIndexerSpecificationFixture : CoreTest<BlockedIndexerSpecification>
{
private RemoteAlbum _remoteAlbum;
[SetUp]
public void Setup()
{
_remoteAlbum = new RemoteAlbum
{
Release = new ReleaseInfo { IndexerId = 1 }
};
Mocker.GetMock<IIndexerStatusService>()
.Setup(v => v.GetBlockedProviders())
.Returns(new List<IndexerStatus>());
}
private void WithBlockedIndexer()
{
Mocker.GetMock<IIndexerStatusService>()
.Setup(v => v.GetBlockedProviders())
.Returns(new List<IndexerStatus> { new IndexerStatus { ProviderId = 1, DisabledTill = DateTime.UtcNow } });
}
[Test]
public void should_return_true_if_no_blocked_indexer()
{
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_false_if_blocked_indexer()
{
WithBlockedIndexer();
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
Subject.Type.Should().Be(RejectionType.Temporary);
}
}
}

View file

@ -0,0 +1,111 @@
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.Search;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.TorrentRss;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.DecisionEngineTests.Search
{
[TestFixture]
public class TorrentSeedingSpecificationFixture : TestBase<TorrentSeedingSpecification>
{
private Artist _artist;
private RemoteAlbum _remoteAlbum;
private IndexerDefinition _indexerDefinition;
[SetUp]
public void Setup()
{
_artist = Builder<Artist>.CreateNew().With(s => s.Id = 1).Build();
_remoteAlbum = new RemoteAlbum
{
Artist = _artist,
Release = new TorrentInfo
{
IndexerId = 1,
Title = "Artist - Album [FLAC-RlsGrp]",
Seeders = 0
}
};
_indexerDefinition = new IndexerDefinition
{
Settings = new TorrentRssIndexerSettings { MinimumSeeders = 5 }
};
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(1))
.Returns(_indexerDefinition);
}
private void GivenReleaseSeeders(int? seeders)
{
(_remoteAlbum.Release as TorrentInfo).Seeders = seeders;
}
[Test]
public void should_return_true_if_not_torrent()
{
_remoteAlbum.Release = new ReleaseInfo
{
IndexerId = 1,
Title = "Artist - Album [FLAC-RlsGrp]"
};
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_if_indexer_not_specified()
{
_remoteAlbum.Release.IndexerId = 0;
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_if_indexer_no_longer_exists()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(It.IsAny<int>()))
.Callback<int>(i => { throw new ModelNotFoundException(typeof(IndexerDefinition), i); });
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_if_seeds_unknown()
{
GivenReleaseSeeders(null);
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
[TestCase(5)]
[TestCase(6)]
public void should_return_true_if_seeds_above_or_equal_to_limit(int seeders)
{
GivenReleaseSeeders(seeders);
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
[TestCase(0)]
[TestCase(4)]
public void should_return_false_if_seeds_belove_limit(int seeders)
{
GivenReleaseSeeders(seeders);
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
}
}
}

View file

@ -6,7 +6,9 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@ -34,7 +36,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
.Build(); .Build();
} }
private RemoteAlbum GetRemoteAlbum(List<Album> albums, QualityModel quality) private RemoteAlbum GetRemoteAlbum(List<Album> albums, QualityModel quality, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet)
{ {
var remoteAlbum = new RemoteAlbum(); var remoteAlbum = new RemoteAlbum();
remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo();
@ -44,6 +46,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
remoteAlbum.Albums.AddRange(albums); remoteAlbum.Albums.AddRange(albums);
remoteAlbum.Release = new ReleaseInfo(); remoteAlbum.Release = new ReleaseInfo();
remoteAlbum.Release.DownloadProtocol = downloadProtocol;
remoteAlbum.Release.PublishDate = DateTime.UtcNow; remoteAlbum.Release.PublishDate = DateTime.UtcNow;
remoteAlbum.Artist = Builder<Artist>.CreateNew() remoteAlbum.Artist = Builder<Artist>.CreateNew()
@ -191,7 +194,6 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
var decisions = new List<DownloadDecision>(); var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary)));
decisions.Add(new DownloadDecision(remoteAlbum));
Subject.ProcessDecisions(decisions); Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Never()); Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Never());
@ -208,7 +210,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions); Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>()), Times.Never()); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>(), It.IsAny<PendingReleaseReason>()), Times.Never());
} }
[Test] [Test]
@ -222,7 +224,43 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary))); decisions.Add(new DownloadDecision(remoteAlbum, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions); Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>()), Times.Exactly(2)); Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>(), It.IsAny<PendingReleaseReason>()), Times.Exactly(2));
}
[Test]
public void should_add_to_failed_if_already_failed_for_that_protocol()
{
var albums = new List<Album> { GetAlbum(1) };
var remoteAlbum = GetRemoteAlbum(albums, new QualityModel(Quality.MP3_320));
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteAlbum));
decisions.Add(new DownloadDecision(remoteAlbum));
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteAlbum>()))
.Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteAlbum>()), Times.Once());
}
[Test]
public void should_not_add_to_failed_if_failed_for_a_different_protocol()
{
var episodes = new List<Album> { GetAlbum(1) };
var remoteEpisode = GetRemoteAlbum(episodes, new QualityModel(Quality.MP3_320), DownloadProtocol.Usenet);
var remoteEpisode2 = GetRemoteAlbum(episodes, new QualityModel(Quality.MP3_320), DownloadProtocol.Torrent);
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteEpisode));
decisions.Add(new DownloadDecision(remoteEpisode2));
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
.Throws(new DownloadClientUnavailableException("Download client failed"));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteAlbum>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
} }
} }
} }

View file

@ -0,0 +1,156 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download
{
public class DownloadClientStatusServiceFixture : CoreTest<DownloadClientStatusService>
{
private DateTime _epoch;
[SetUp]
public void SetUp()
{
_epoch = DateTime.UtcNow;
}
private DownloadClientStatus WithStatus(DownloadClientStatus status)
{
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(v => v.FindByProviderId(1))
.Returns(status);
Mocker.GetMock<IDownloadClientStatusRepository>()
.Setup(v => v.All())
.Returns(new[] { status });
return status;
}
private void VerifyUpdate()
{
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<DownloadClientStatus>()), Times.Once());
}
private void VerifyNoUpdate()
{
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<DownloadClientStatus>()), Times.Never());
}
[Test]
public void should_not_consider_blocked_within_5_minutes_since_initial_failure()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(4),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
EscalationLevel = 3
});
Subject.RecordFailure(1);
VerifyUpdate();
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().BeNull();
}
[Test]
public void should_consider_blocked_after_5_minutes_since_initial_failure()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
EscalationLevel = 3
});
Subject.RecordFailure(1);
VerifyUpdate();
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
}
[Test]
public void should_not_escalate_further_till_after_5_minutes_since_initial_failure()
{
var origStatus = WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(4),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
EscalationLevel = 3
});
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().BeNull();
origStatus.EscalationLevel.Should().Be(3);
}
[Test]
public void should_escalate_further_after_5_minutes_since_initial_failure()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
EscalationLevel = 3
});
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
status.EscalationLevel.Should().BeGreaterThan(3);
}
[Test]
public void should_not_escalate_beyond_3_hours()
{
WithStatus(new DownloadClientStatus
{
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
EscalationLevel = 3
});
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue();
status.DisabledTill.Should().NotBeAfter(_epoch + TimeSpan.FromHours(3.1));
}
}
}

View file

@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
[Test] [Test]
public void should_add() public void should_add()
{ {
Subject.Add(_temporarilyRejected); Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay);
VerifyInsert(); VerifyInsert();
} }
@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{ {
GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate); GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate);
Subject.Add(_temporarilyRejected); Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay);
VerifyNoInsert(); VerifyNoInsert();
} }
@ -122,7 +122,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{ {
GivenHeldRelease(_release.Title + "-RP", _release.Indexer, _release.PublishDate); GivenHeldRelease(_release.Title + "-RP", _release.Indexer, _release.PublishDate);
Subject.Add(_temporarilyRejected); Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay);
VerifyInsert(); VerifyInsert();
} }
@ -132,7 +132,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{ {
GivenHeldRelease(_release.Title, "AnotherIndexer", _release.PublishDate); GivenHeldRelease(_release.Title, "AnotherIndexer", _release.PublishDate);
Subject.Add(_temporarilyRejected); Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay);
VerifyInsert(); VerifyInsert();
} }
@ -142,7 +142,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{ {
GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate.AddHours(1)); GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate.AddHours(1));
Subject.Add(_temporarilyRejected); Subject.Add(_temporarilyRejected, PendingReleaseReason.Delay);
VerifyInsert(); VerifyInsert();
} }

View file

@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
public void should_not_ignore_pending_items_from_available_indexer() public void should_not_ignore_pending_items_from_available_indexer()
{ {
Mocker.GetMock<IIndexerStatusService>() Mocker.GetMock<IIndexerStatusService>()
.Setup(v => v.GetBlockedIndexers()) .Setup(v => v.GetBlockedProviders())
.Returns(new List<IndexerStatus>()); .Returns(new List<IndexerStatus>());
GivenPendingRelease(); GivenPendingRelease();
@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
public void should_ignore_pending_items_from_unavailable_indexer() public void should_ignore_pending_items_from_unavailable_indexer()
{ {
Mocker.GetMock<IIndexerStatusService>() Mocker.GetMock<IIndexerStatusService>()
.Setup(v => v.GetBlockedIndexers()) .Setup(v => v.GetBlockedProviders())
.Returns(new List<IndexerStatus> { new IndexerStatus { ProviderId = 1, DisabledTill = DateTime.UtcNow.AddHours(2) } }); .Returns(new List<IndexerStatus> { new IndexerStatus { ProviderId = 1, DisabledTill = DateTime.UtcNow.AddHours(2) } });
GivenPendingRelease(); GivenPendingRelease();

View file

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@ -26,7 +25,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public void should_return_error_when_download_client_throws() public void should_return_error_when_download_client_throws()
{ {
var downloadClient = Mocker.GetMock<IDownloadClient>(); var downloadClient = Mocker.GetMock<IDownloadClient>();
downloadClient.Setup(s => s.Definition).Returns(new IndexerDefinition{Name = "Test"}); downloadClient.Setup(s => s.Definition).Returns(new DownloadClientDefinition{Name = "Test"});
downloadClient.Setup(s => s.GetItems()) downloadClient.Setup(s => s.GetItems())
.Throws<Exception>(); .Throws<Exception>();
@ -36,8 +35,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Returns(new IDownloadClient[] { downloadClient.Object }); .Returns(new IDownloadClient[] { downloadClient.Object });
Subject.Check().ShouldBeError(); Subject.Check().ShouldBeError();
ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]

View file

@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Returns(_indexers); .Returns(_indexers);
Mocker.GetMock<IIndexerStatusService>() Mocker.GetMock<IIndexerStatusService>()
.Setup(v => v.GetBlockedIndexers()) .Setup(v => v.GetBlockedProviders())
.Returns(_blockedIndexers); .Returns(_blockedIndexers);
} }
@ -57,13 +57,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{ {
Subject.Check().ShouldBeOk(); Subject.Check().ShouldBeOk();
} }
[Test]
public void should_not_return_error_when_indexer_failed_less_than_an_hour()
{
GivenIndexer(1, 0.1, 0.5);
Subject.Check().ShouldBeOk();
}
[Test] [Test]
public void should_return_warning_if_indexer_unavailable() public void should_return_warning_if_indexer_unavailable()

View file

@ -0,0 +1,60 @@
using System;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupDownloadClientUnavailablePendingReleasesFixture : DbTest<CleanupDownloadClientUnavailablePendingReleases, PendingRelease>
{
[Test]
public void should_delete_old_DownloadClientUnavailable_pending_items()
{
var pendingRelease = Builder<PendingRelease>.CreateNew()
.With(h => h.Reason = PendingReleaseReason.DownloadClientUnavailable)
.With(h => h.Added = DateTime.UtcNow.AddDays(-21))
.With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo())
.With(h => h.Release = new ReleaseInfo())
.BuildNew();
Db.Insert(pendingRelease);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_delete_old_Fallback_pending_items()
{
var pendingRelease = Builder<PendingRelease>.CreateNew()
.With(h => h.Reason = PendingReleaseReason.Fallback)
.With(h => h.Added = DateTime.UtcNow.AddDays(-21))
.With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo())
.With(h => h.Release = new ReleaseInfo())
.BuildNew();
Db.Insert(pendingRelease);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_old_Delay_pending_items()
{
var pendingRelease = Builder<PendingRelease>.CreateNew()
.With(h => h.Reason = PendingReleaseReason.Delay)
.With(h => h.Added = DateTime.UtcNow.AddDays(-21))
.With(h => h.ParsedAlbumInfo = new ParsedAlbumInfo())
.With(h => h.Release = new ReleaseInfo())
.BuildNew();
Db.Insert(pendingRelease);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
}
}

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@ -11,7 +11,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public class IndexerStatusServiceFixture : CoreTest<IndexerStatusService> public class IndexerStatusServiceFixture : CoreTest<IndexerStatusService>
{ {
private DateTime _epoch; private DateTime _epoch;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests
private void WithStatus(IndexerStatus status) private void WithStatus(IndexerStatus status)
{ {
Mocker.GetMock<IIndexerStatusRepository>() Mocker.GetMock<IIndexerStatusRepository>()
.Setup(v => v.FindByIndexerId(1)) .Setup(v => v.FindByProviderId(1))
.Returns(status); .Returns(status);
Mocker.GetMock<IIndexerStatusRepository>() Mocker.GetMock<IIndexerStatusRepository>()
@ -29,25 +29,16 @@ namespace NzbDrone.Core.Test.IndexerTests
.Returns(new[] { status }); .Returns(new[] { status });
} }
private void VerifyUpdate(bool updated = true) private void VerifyUpdate()
{ {
Mocker.GetMock<IIndexerStatusRepository>() Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<IndexerStatus>()), Times.Exactly(updated ? 1 : 0)); .Verify(v => v.Upsert(It.IsAny<IndexerStatus>()), Times.Once());
} }
[Test] private void VerifyNoUpdate()
public void should_start_backoff_on_first_failure()
{ {
WithStatus(new IndexerStatus()); Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<IndexerStatus>()), Times.Never());
Subject.RecordFailure(1);
VerifyUpdate();
var status = Subject.GetBlockedIndexers().FirstOrDefault();
status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
} }
[Test] [Test]
@ -59,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests
VerifyUpdate(); VerifyUpdate();
var status = Subject.GetBlockedIndexers().FirstOrDefault(); var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().BeNull(); status.Should().BeNull();
} }
@ -70,22 +61,7 @@ namespace NzbDrone.Core.Test.IndexerTests
Subject.RecordSuccess(1); Subject.RecordSuccess(1);
VerifyUpdate(false); VerifyNoUpdate();
}
[Test]
public void should_preserve_escalation_on_intermittent_success()
{
WithStatus(new IndexerStatus { MostRecentFailure = _epoch - TimeSpan.FromSeconds(4), EscalationLevel = 3 });
Subject.RecordSuccess(1);
Subject.RecordSuccess(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedIndexers().FirstOrDefault();
status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
} }
} }
} }

View file

@ -117,6 +117,7 @@
<Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" /> <Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" />
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" /> <Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\BlockedIndexerSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" /> <Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
@ -135,10 +136,12 @@
<Compile Include="DecisionEngineTests\RssSync\ProperSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\RssSync\ProperSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\Search\ArtistSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\Search\ArtistSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RawDiskSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\RawDiskSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\Search\TorrentSeedingSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\UpgradeDiskSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\UpgradeDiskSpecificationFixture.cs" />
<Compile Include="DiskSpace\DiskSpaceServiceFixture.cs" /> <Compile Include="DiskSpace\DiskSpaceServiceFixture.cs" />
<Compile Include="Download\CompletedDownloadServiceFixture.cs" /> <Compile Include="Download\CompletedDownloadServiceFixture.cs" />
<Compile Include="Download\DownloadApprovedReportsTests\DownloadApprovedFixture.cs" /> <Compile Include="Download\DownloadApprovedReportsTests\DownloadApprovedFixture.cs" />
<Compile Include="Download\DownloadClientStatusServiceFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\ScanWatchFolderFixture.cs" /> <Compile Include="Download\DownloadClientTests\Blackhole\ScanWatchFolderFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\TorrentBlackholeFixture.cs" /> <Compile Include="Download\DownloadClientTests\Blackhole\TorrentBlackholeFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\UsenetBlackholeFixture.cs" /> <Compile Include="Download\DownloadClientTests\Blackhole\UsenetBlackholeFixture.cs" />
@ -198,6 +201,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalUsersFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAdditionalUsersFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAbsolutePathMetadataFilesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAbsolutePathMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDownloadClientUnavailablePendingReleasesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedAlbumsFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedAlbumsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklistFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklistFixture.cs" />
@ -324,11 +328,12 @@
<Compile Include="Qualities\QualityFixture.cs" /> <Compile Include="Qualities\QualityFixture.cs" />
<Compile Include="Qualities\QualityModelComparerFixture.cs" /> <Compile Include="Qualities\QualityModelComparerFixture.cs" />
<Compile Include="RootFolderTests\RootFolderServiceFixture.cs" /> <Compile Include="RootFolderTests\RootFolderServiceFixture.cs" />
<Compile Include="ThingiProvider\ProviderBaseFixture.cs" /> <Compile Include="ThingiProviderTests\ProviderBaseFixture.cs" />
<Compile Include="ThingiProviderTests\NullConfigFixture.cs" /> <Compile Include="ThingiProviderTests\NullConfigFixture.cs" />
<Compile Include="MusicTests\MoveArtistServiceFixture.cs" /> <Compile Include="MusicTests\MoveArtistServiceFixture.cs" />
<Compile Include="MusicTests\RefreshArtistServiceFixture.cs" /> <Compile Include="MusicTests\RefreshArtistServiceFixture.cs" />
<Compile Include="MusicTests\ShouldRefreshArtistFixture.cs" /> <Compile Include="MusicTests\ShouldRefreshArtistFixture.cs" />
<Compile Include="ThingiProviderTests\ProviderStatusServiceFixture.cs" />
<Compile Include="UpdateTests\UpdatePackageProviderFixture.cs" /> <Compile Include="UpdateTests\UpdatePackageProviderFixture.cs" />
<Compile Include="UpdateTests\UpdateServiceFixture.cs" /> <Compile Include="UpdateTests\UpdateServiceFixture.cs" />
<Compile Include="XbmcVersionTests.cs" /> <Compile Include="XbmcVersionTests.cs" />
@ -522,6 +527,7 @@
<Folder Include="InstrumentationTests\" /> <Folder Include="InstrumentationTests\" />
<Folder Include="Providers\" /> <Folder Include="Providers\" />
<Folder Include="ProviderTests\UpdateProviderTests\" /> <Folder Include="ProviderTests\UpdateProviderTests\" />
<Folder Include="ThingiProvider\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" /> <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

View file

@ -1,13 +1,12 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ThingiProvider namespace NzbDrone.Core.Test.ThingiProviderTests
{ {
public class ProviderRepositoryFixture : DbTest<IndexerRepository, IndexerDefinition> public class ProviderRepositoryFixture : DbTest<IndexerRepository, IndexerDefinition>
{ {
[Test] [Test]
@ -27,4 +26,4 @@ namespace NzbDrone.Core.Test.ThingiProvider
storedSetting.ShouldBeEquivalentTo(newznabSettings, o=>o.IncludingAllRuntimeProperties()); storedSetting.ShouldBeEquivalentTo(newznabSettings, o=>o.IncludingAllRuntimeProperties());
} }
} }
} }

View file

@ -0,0 +1,126 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NLog;
using NUnit.Framework;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Test.ThingiProviderTests
{
public class MockProviderStatus : ProviderStatusBase
{
}
public interface IMockProvider : IProvider
{
}
public interface IMockProviderStatusRepository : IProviderStatusRepository<MockProviderStatus>
{
}
public class MockProviderStatusService : ProviderStatusServiceBase<IMockProvider, MockProviderStatus>
{
public MockProviderStatusService(IMockProviderStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
: base(providerStatusRepository, eventAggregator, logger)
{
}
}
public class ProviderStatusServiceFixture : CoreTest<MockProviderStatusService>
{
private DateTime _epoch;
[SetUp]
public void SetUp()
{
_epoch = DateTime.UtcNow;
}
private void WithStatus(MockProviderStatus status)
{
Mocker.GetMock<IMockProviderStatusRepository>()
.Setup(v => v.FindByProviderId(1))
.Returns(status);
Mocker.GetMock<IMockProviderStatusRepository>()
.Setup(v => v.All())
.Returns(new[] { status });
}
private void VerifyUpdate()
{
Mocker.GetMock<IMockProviderStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<MockProviderStatus>()), Times.Once());
}
private void VerifyNoUpdate()
{
Mocker.GetMock<IMockProviderStatusRepository>()
.Verify(v => v.Upsert(It.IsAny<MockProviderStatus>()), Times.Never());
}
[Test]
public void should_start_backoff_on_first_failure()
{
WithStatus(new MockProviderStatus());
Subject.RecordFailure(1);
VerifyUpdate();
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
}
[Test]
public void should_cancel_backoff_on_success()
{
WithStatus(new MockProviderStatus { EscalationLevel = 2 });
Subject.RecordSuccess(1);
VerifyUpdate();
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().BeNull();
}
[Test]
public void should_not_store_update_if_already_okay()
{
WithStatus(new MockProviderStatus { EscalationLevel = 0 });
Subject.RecordSuccess(1);
VerifyNoUpdate();
}
[Test]
public void should_preserve_escalation_on_intermittent_success()
{
WithStatus(new MockProviderStatus
{
InitialFailure = _epoch - TimeSpan.FromSeconds(20),
MostRecentFailure = _epoch - TimeSpan.FromSeconds(4),
EscalationLevel = 3
});
Subject.RecordSuccess(1);
Subject.RecordSuccess(1);
Subject.RecordFailure(1);
var status = Subject.GetBlockedProviders().FirstOrDefault();
status.Should().NotBeNull();
status.DisabledTill.Should().HaveValue();
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
}
}
}

View file

@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(2)]
public class add_reason_to_pending_releases : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("PendingReleases").AddColumn("Reason").AsInt32().WithDefaultValue(0);
}
}
}

View file

@ -123,6 +123,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(c => c.Message); .Ignore(c => c.Message);
Mapper.Entity<IndexerStatus>().RegisterModel("IndexerStatus"); Mapper.Entity<IndexerStatus>().RegisterModel("IndexerStatus");
Mapper.Entity<DownloadClientStatus>().RegisterModel("DownloadClientStatus");
} }
private static void RegisterMappers() private static void RegisterMappers()

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class BlockedIndexerSpecification : IDecisionEngineSpecification
{
private readonly IIndexerStatusService _indexerStatusService;
private readonly Logger _logger;
private readonly ICachedDictionary<IndexerStatus> _blockedIndexerCache;
public BlockedIndexerSpecification(IIndexerStatusService indexerStatusService, ICacheManager cacheManager, Logger logger)
{
_indexerStatusService = indexerStatusService;
_logger = logger;
_blockedIndexerCache = cacheManager.GetCacheDictionary(GetType(), "blocked", FetchBlockedIndexer, TimeSpan.FromSeconds(15));
}
public SpecificationPriority Priority => SpecificationPriority.Database;
public RejectionType Type => RejectionType.Temporary;
public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria)
{
var status = _blockedIndexerCache.Find(subject.Release.IndexerId.ToString());
if (status != null)
{
return Decision.Reject($"Indexer {subject.Release.Indexer} is blocked till {status.DisabledTill} due to failures, cannot grab release.");
}
return Decision.Accept();
}
private IDictionary<string, IndexerStatus> FetchBlockedIndexer()
{
return _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId.ToString());
}
}
}

View file

@ -1,6 +1,5 @@
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Reflection; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -9,10 +8,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{ {
public class TorrentSeedingSpecification : IDecisionEngineSpecification public class TorrentSeedingSpecification : IDecisionEngineSpecification
{ {
private readonly IndexerFactory _indexerFactory; private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger; private readonly Logger _logger;
public TorrentSeedingSpecification(IndexerFactory indexerFactory, Logger logger) public TorrentSeedingSpecification(IIndexerFactory indexerFactory, Logger logger)
{ {
_indexerFactory = indexerFactory; _indexerFactory = indexerFactory;
_logger = logger; _logger = logger;
@ -26,12 +25,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{ {
var torrentInfo = remoteAlbum.Release as TorrentInfo; var torrentInfo = remoteAlbum.Release as TorrentInfo;
if (torrentInfo == null) if (torrentInfo == null || torrentInfo.IndexerId == 0)
{ {
return Decision.Accept(); return Decision.Accept();
} }
var indexer = _indexerFactory.Get(torrentInfo.IndexerId); IndexerDefinition indexer;
try
{
indexer = _indexerFactory.Get(torrentInfo.IndexerId);
}
catch (ModelNotFoundException)
{
_logger.Debug("Indexer with id {0} does not exist, skipping seeders check", torrentInfo.IndexerId);
return Decision.Accept();
}
var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings; var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings;
if (torrentIndexerSettings != null) if (torrentIndexerSettings != null)

View file

@ -81,21 +81,13 @@ namespace NzbDrone.Core.Download.Clients.Deluge
{ {
IEnumerable<DelugeTorrent> torrents; IEnumerable<DelugeTorrent> torrents;
try if (!Settings.TvCategory.IsNullOrWhiteSpace())
{ {
if (!Settings.TvCategory.IsNullOrWhiteSpace()) torrents = _proxy.GetTorrentsByLabel(Settings.TvCategory, Settings);
{
torrents = _proxy.GetTorrentsByLabel(Settings.TvCategory, Settings);
}
else
{
torrents = _proxy.GetTorrents(Settings);
}
} }
catch (DownloadClientException ex) else
{ {
_logger.Error(ex, "Couldn't get list of torrents"); torrents = _proxy.GetTorrents(Settings);
return Enumerable.Empty<DownloadClientItem>();
} }
var items = new List<DownloadClientItem>(); var items = new List<DownloadClientItem>();

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -231,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
} }
catch (WebException ex) catch (WebException ex)
{ {
throw new DownloadClientException("Unable to connect to Deluge, please check your settings", ex); throw new DownloadClientUnavailableException("Unable to connect to Deluge, please check your settings", ex);
} }
} }

View file

@ -1,4 +1,4 @@
using System; using System;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Download.Clients namespace NzbDrone.Core.Download.Clients
@ -8,19 +8,16 @@ namespace NzbDrone.Core.Download.Clients
public DownloadClientException(string message, params object[] args) public DownloadClientException(string message, params object[] args)
: base(string.Format(message, args)) : base(string.Format(message, args))
{ {
} }
public DownloadClientException(string message) public DownloadClientException(string message)
: base(message) : base(message)
{ {
} }
public DownloadClientException(string message, Exception innerException, params object[] args) public DownloadClientException(string message, Exception innerException, params object[] args)
: base(string.Format(message, args), innerException) : base(string.Format(message, args), innerException)
{ {
} }
public DownloadClientException(string message, Exception innerException) public DownloadClientException(string message, Exception innerException)

View file

@ -0,0 +1,27 @@
using System;
namespace NzbDrone.Core.Download.Clients
{
public class DownloadClientUnavailableException : DownloadClientException
{
public DownloadClientUnavailableException(string message, params object[] args)
: base(string.Format(message, args))
{
}
public DownloadClientUnavailableException(string message)
: base(message)
{
}
public DownloadClientUnavailableException(string message, Exception innerException, params object[] args)
: base(string.Format(message, args), innerException)
{
}
public DownloadClientUnavailableException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -72,7 +72,20 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
DownloadStationSettings settings) where T : new() DownloadStationSettings settings) where T : new()
{ {
var request = requestBuilder.Build(); var request = requestBuilder.Build();
var response = _httpClient.Execute(request); HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
throw new DownloadClientException("Unable to connect to Diskstation, please check your settings", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Unable to connect to Diskstation, please check your settings", ex);
}
_logger.Debug("Trying to {0}", operation); _logger.Debug("Trying to {0}", operation);

View file

@ -35,17 +35,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
HadoukenTorrent[] torrents; var torrents = _proxy.GetTorrents(Settings);
try
{
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
return Enumerable.Empty<DownloadClientItem>();
}
var items = new List<DownloadClientItem>(); var items = new List<DownloadClientItem>();

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using NLog; using NLog;
@ -77,7 +77,21 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate"); requestBuilder.Headers.Add("Accept-Encoding", "gzip,deflate");
var httpRequest = requestBuilder.Build(); var httpRequest = requestBuilder.Build();
var response = _httpClient.Execute(httpRequest); HttpResponse response;
try
{
response = _httpClient.Execute(httpRequest);
}
catch (HttpException ex)
{
throw new DownloadClientException("Unable to connect to Hadouken, please check your settings", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Unable to connect to Hadouken, please check your settings", ex);
}
var result = Json.Deserialize<JsonRpcResponse<T>>(response.Content); var result = Json.Deserialize<JsonRpcResponse<T>>(response.Content);
if (result.Error != null) if (result.Error != null)
@ -160,4 +174,4 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
return HadoukenTorrentState.Unknown; return HadoukenTorrentState.Unknown;
} }
} }
} }

View file

@ -47,17 +47,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
List<NzbVortexQueueItem> vortexQueue; var vortexQueue = _proxy.GetQueue(30, Settings);
try
{
vortexQueue = _proxy.GetQueue(30, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn("Couldn't get download queue. {0}", ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>(); var queueItems = new List<DownloadClientItem>();

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -164,7 +164,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
} }
catch (WebException ex) catch (WebException ex)
{ {
throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", ex); throw new DownloadClientUnavailableException("Unable to connect to NZBVortex, please check your settings", ex);
} }
} }

View file

@ -51,19 +51,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
private IEnumerable<DownloadClientItem> GetQueue() private IEnumerable<DownloadClientItem> GetQueue()
{ {
NzbgetGlobalStatus globalStatus; var globalStatus = _proxy.GetGlobalStatus(Settings);
List<NzbgetQueueItem> queue; var queue = _proxy.GetQueue(Settings);
try
{
globalStatus = _proxy.GetGlobalStatus(Settings);
queue = _proxy.GetQueue(Settings);
}
catch (DownloadClientException ex)
{
_logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>(); var queueItems = new List<DownloadClientItem>();
@ -119,17 +108,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
private IEnumerable<DownloadClientItem> GetHistory() private IEnumerable<DownloadClientItem> GetHistory()
{ {
List<NzbgetHistoryItem> history; var history = _proxy.GetHistory(Settings).Take(_configService.DownloadClientHistoryLimit).ToList();
try
{
history = _proxy.GetHistory(Settings).Take(_configService.DownloadClientHistoryLimit).ToList();
}
catch (DownloadClientException ex)
{
_logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var historyItems = new List<DownloadClientItem>(); var historyItems = new List<DownloadClientItem>();

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -235,14 +235,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{ {
throw new DownloadClientException("Authentication failed for NzbGet, please check your settings", ex); throw new DownloadClientAuthenticationException("Authentication failed for NzbGet, please check your settings", ex);
} }
throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex); throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex);
} }
catch (WebException ex) catch (WebException ex)
{ {
throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex); throw new DownloadClientUnavailableException("Unable to connect to NzbGet. " + ex.Message, ex);
} }
var result = Json.Deserialize<JsonRpcResponse<T>>(response.Content); var result = Json.Deserialize<JsonRpcResponse<T>>(response.Content);

View file

@ -89,19 +89,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
QBittorrentPreferences config; var config = _proxy.GetConfig(Settings);
List<QBittorrentTorrent> torrents; var torrents = _proxy.GetTorrents(Settings);
try
{
config = _proxy.GetConfig(Settings);
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
_logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>(); var queueItems = new List<DownloadClientItem>();

View file

@ -226,7 +226,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
catch (WebException ex) catch (WebException ex)
{ {
throw new DownloadClientException("Failed to connect to qBitTorrent, please check your settings.", ex); throw new DownloadClientUnavailableException("Failed to connect to qBitTorrent, please check your settings.", ex);
} }
if (response.Content != "Ok.") // returns "Fails." on bad login if (response.Content != "Ok.") // returns "Fails." on bad login

View file

@ -112,17 +112,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
private IEnumerable<DownloadClientItem> GetHistory() private IEnumerable<DownloadClientItem> GetHistory()
{ {
SabnzbdHistory sabHistory; var sabHistory = _proxy.GetHistory(0, _configService.DownloadClientHistoryLimit, Settings.TvCategory, Settings);
try
{
sabHistory = _proxy.GetHistory(0, _configService.DownloadClientHistoryLimit, Settings.TvCategory, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn(ex, "Couldn't get download queue. {0}", ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var historyItems = new List<DownloadClientItem>(); var historyItems = new List<DownloadClientItem>();

View file

@ -1,4 +1,4 @@
using System; using System;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -183,7 +183,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
} }
catch (WebException ex) catch (WebException ex)
{ {
throw new DownloadClientException("Unable to connect to SABnzbd, please check your settings", ex); throw new DownloadClientUnavailableException("Unable to connect to SABnzbd, please check your settings", ex);
} }
CheckForError(response); CheckForError(response);

View file

@ -33,17 +33,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
List<TransmissionTorrent> torrents; var torrents = _proxy.GetTorrents(Settings);
try
{
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
_logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var items = new List<DownloadClientItem>(); var items = new List<DownloadClientItem>();
@ -211,17 +201,13 @@ namespace NzbDrone.Core.Download.Clients.Transmission
DetailedDescription = string.Format("Please verify your username and password. Also verify if the host running Lidarr isn't blocked from accessing {0} by WhiteList limitations in the {0} configuration.", Name) DetailedDescription = string.Format("Please verify your username and password. Also verify if the host running Lidarr isn't blocked from accessing {0} by WhiteList limitations in the {0} configuration.", Name)
}; };
} }
catch (WebException ex) catch (DownloadClientUnavailableException ex)
{ {
_logger.Error(ex, ex.Message); _logger.Error(ex, ex.Message);
if (ex.Status == WebExceptionStatus.ConnectFailure) return new NzbDroneValidationFailure("Host", "Unable to connect")
{ {
return new NzbDroneValidationFailure("Host", "Unable to connect") DetailedDescription = "Please verify the hostname and port."
{ };
DetailedDescription = "Please verify the hostname and port."
};
}
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Net; using System.Net;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -238,54 +238,65 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings) public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings)
{ {
var requestBuilder = BuildRequest(settings); try
requestBuilder.Headers.ContentType = "application/json";
requestBuilder.SuppressHttpError = true;
AuthenticateClient(requestBuilder, settings);
var request = requestBuilder.Post().Build();
var data = new Dictionary<string, object>();
data.Add("method", action);
if (arguments != null)
{ {
data.Add("arguments", arguments); var requestBuilder = BuildRequest(settings);
} requestBuilder.Headers.ContentType = "application/json";
requestBuilder.SuppressHttpError = true;
request.SetContent(data.ToJson()); AuthenticateClient(requestBuilder, settings);
request.ContentSummary = string.Format("{0}(...)", action);
var response = _httpClient.Execute(request); var request = requestBuilder.Post().Build();
if (response.StatusCode == HttpStatusCode.Conflict)
{
AuthenticateClient(requestBuilder, settings, true);
request = requestBuilder.Post().Build(); var data = new Dictionary<string, object>();
data.Add("method", action);
if (arguments != null)
{
data.Add("arguments", arguments);
}
request.SetContent(data.ToJson()); request.SetContent(data.ToJson());
request.ContentSummary = string.Format("{0}(...)", action); request.ContentSummary = string.Format("{0}(...)", action);
response = _httpClient.Execute(request); var response = _httpClient.Execute(request);
}
else if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new DownloadClientAuthenticationException("User authentication failed.");
}
var transmissionResponse = Json.Deserialize<TransmissionResponse>(response.Content); if (response.StatusCode == HttpStatusCode.Conflict)
{
AuthenticateClient(requestBuilder, settings, true);
if (transmissionResponse == null) request = requestBuilder.Post().Build();
{
throw new TransmissionException("Unexpected response");
}
else if (transmissionResponse.Result != "success")
{
throw new TransmissionException(transmissionResponse.Result);
}
return transmissionResponse; request.SetContent(data.ToJson());
request.ContentSummary = string.Format("{0}(...)", action);
response = _httpClient.Execute(request);
}
else if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new DownloadClientAuthenticationException("User authentication failed.");
}
var transmissionResponse = Json.Deserialize<TransmissionResponse>(response.Content);
if (transmissionResponse == null)
{
throw new TransmissionException("Unexpected response");
}
else if (transmissionResponse.Result != "success")
{
throw new TransmissionException(transmissionResponse.Result);
}
return transmissionResponse;
}
catch (HttpException ex)
{
throw new DownloadClientException("Unable to connect to Transmission, please check your settings", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Unable to connect to Transmission, please check your settings", ex);
}
} }
} }
} }

View file

@ -81,69 +81,60 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
try var torrents = _proxy.GetTorrents(Settings);
_logger.Debug("Retrieved metadata of {0} torrents in client", torrents.Count);
var items = new List<DownloadClientItem>();
foreach (RTorrentTorrent torrent in torrents)
{ {
var torrents = _proxy.GetTorrents(Settings); // Don't concern ourselves with categories other than specified
if (torrent.Category != Settings.TvCategory) continue;
_logger.Debug("Retrieved metadata of {0} torrents in client", torrents.Count); if (torrent.Path.StartsWith("."))
var items = new List<DownloadClientItem>();
foreach (RTorrentTorrent torrent in torrents)
{ {
// Don't concern ourselves with categories other than specified throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent.");
if (torrent.Category != Settings.TvCategory) continue;
if (torrent.Path.StartsWith("."))
{
throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent.");
}
var item = new DownloadClientItem();
item.DownloadClient = Definition.Name;
item.Title = torrent.Name;
item.DownloadId = torrent.Hash;
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path));
item.TotalSize = torrent.TotalSize;
item.RemainingSize = torrent.RemainingSize;
item.Category = torrent.Category;
if (torrent.DownRate > 0)
{
var secondsLeft = torrent.RemainingSize / torrent.DownRate;
item.RemainingTime = TimeSpan.FromSeconds(secondsLeft);
}
else
{
item.RemainingTime = TimeSpan.Zero;
}
if (torrent.IsFinished)
{
item.Status = DownloadItemStatus.Completed;
}
else if (torrent.IsActive)
{
item.Status = DownloadItemStatus.Downloading;
}
else if (!torrent.IsActive)
{
item.Status = DownloadItemStatus.Paused;
}
// No stop ratio data is present, so do not delete
item.CanMoveFiles = item.CanBeRemoved = false;
items.Add(item);
} }
return items; var item = new DownloadClientItem();
} item.DownloadClient = Definition.Name;
catch (DownloadClientException ex) item.Title = torrent.Name;
{ item.DownloadId = torrent.Hash;
_logger.Error(ex, ex.Message); item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path));
return Enumerable.Empty<DownloadClientItem>(); item.TotalSize = torrent.TotalSize;
item.RemainingSize = torrent.RemainingSize;
item.Category = torrent.Category;
if (torrent.DownRate > 0)
{
var secondsLeft = torrent.RemainingSize / torrent.DownRate;
item.RemainingTime = TimeSpan.FromSeconds(secondsLeft);
}
else
{
item.RemainingTime = TimeSpan.Zero;
}
if (torrent.IsFinished)
{
item.Status = DownloadItemStatus.Completed;
}
else if (torrent.IsActive)
{
item.Status = DownloadItemStatus.Downloading;
}
else if (!torrent.IsActive)
{
item.Status = DownloadItemStatus.Paused;
}
// No stop ratio data is present, so do not delete
item.CanMoveFiles = item.CanBeRemoved = false;
items.Add(item);
} }
return items;
} }
public override void RemoveItem(string downloadId, bool deleteData) public override void RemoveItem(string downloadId, bool deleteData)

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices.ComTypes;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using CookComputing.XmlRpc; using CookComputing.XmlRpc;
@ -54,8 +56,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_logger.Debug("Executing remote method: system.client_version"); _logger.Debug("Executing remote method: system.client_version");
var client = BuildClient(settings); var client = BuildClient(settings);
var version = ExecuteRequest(() => client.GetVersion());
var version = client.GetVersion();
return version; return version;
} }
@ -65,20 +66,22 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_logger.Debug("Executing remote method: d.multicall2"); _logger.Debug("Executing remote method: d.multicall2");
var client = BuildClient(settings); var client = BuildClient(settings);
var ret = client.TorrentMulticall("", "", var ret = ExecuteRequest(() => client.TorrentMulticall("", "",
"d.name=", // string "d.name=", // string
"d.hash=", // string "d.hash=", // string
"d.base_path=", // string "d.base_path=", // string
"d.custom1=", // string (label) "d.custom1=", // string (label)
"d.size_bytes=", // long "d.size_bytes=", // long
"d.left_bytes=", // long "d.left_bytes=", // long
"d.down.rate=", // long (in bytes / s) "d.down.rate=", // long (in bytes / s)
"d.ratio=", // long "d.ratio=", // long
"d.is_open=", // long "d.is_open=", // long
"d.is_active=", // long "d.is_active=", // long
"d.complete="); //long "d.complete=") //long
);
var items = new List<RTorrentTorrent>(); var items = new List<RTorrentTorrent>();
foreach (object[] torrent in ret) foreach (object[] torrent in ret)
{ {
var labelDecoded = System.Web.HttpUtility.UrlDecode((string) torrent[3]); var labelDecoded = System.Web.HttpUtility.UrlDecode((string) torrent[3]);
@ -107,8 +110,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_logger.Debug("Executing remote method: load.normal"); _logger.Debug("Executing remote method: load.normal");
var client = BuildClient(settings); var client = BuildClient(settings);
var response = ExecuteRequest(() => client.LoadStart("", torrentUrl, GetCommands(label, priority, directory)));
var response = client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
if (response != 0) if (response != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl); throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
@ -120,8 +123,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_logger.Debug("Executing remote method: load.raw"); _logger.Debug("Executing remote method: load.raw");
var client = BuildClient(settings); var client = BuildClient(settings);
var response = ExecuteRequest(() => client.LoadRawStart("", fileContent, GetCommands(label, priority, directory)));
var response = client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
if (response != 0) if (response != 0)
{ {
throw new DownloadClientException("Could not add torrent: {0}.", fileName); throw new DownloadClientException("Could not add torrent: {0}.", fileName);
@ -133,14 +136,39 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_logger.Debug("Executing remote method: d.erase"); _logger.Debug("Executing remote method: d.erase");
var client = BuildClient(settings); var client = BuildClient(settings);
var response = ExecuteRequest(() => client.Remove(hash));
var response = client.Remove(hash);
if (response != 0) if (response != 0)
{ {
throw new DownloadClientException("Could not remove torrent: {0}.", hash); throw new DownloadClientException("Could not remove torrent: {0}.", hash);
} }
} }
public bool HasHashTorrent(string hash, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try
{
var name = ExecuteRequest(() => client.GetName(hash));
if (name.IsNullOrWhiteSpace())
{
return false;
}
var metaTorrent = name == (hash + ".meta");
return !metaTorrent;
}
catch (Exception)
{
return false;
}
}
private string[] GetCommands(string label, RTorrentPriority priority, string directory) private string[] GetCommands(string label, RTorrentPriority priority, string directory)
{ {
var result = new List<string>(); var result = new List<string>();
@ -163,25 +191,6 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return result.ToArray(); return result.ToArray();
} }
public bool HasHashTorrent(string hash, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: d.name");
var client = BuildClient(settings);
try
{
var name = client.GetName(hash);
if (name.IsNullOrWhiteSpace()) return false;
bool metaTorrent = name == (hash + ".meta");
return !metaTorrent;
}
catch (Exception)
{
return false;
}
}
private IRTorrent BuildClient(RTorrentSettings settings) private IRTorrent BuildClient(RTorrentSettings settings)
{ {
var client = XmlRpcProxyGen.Create<IRTorrent>(); var client = XmlRpcProxyGen.Create<IRTorrent>();
@ -201,5 +210,21 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
return client; return client;
} }
private T ExecuteRequest<T>(Func<T> task)
{
try
{
return task();
}
catch (XmlRpcServerException ex)
{
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex);
}
}
} }
} }

View file

@ -72,42 +72,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
public override IEnumerable<DownloadClientItem> GetItems() public override IEnumerable<DownloadClientItem> GetItems()
{ {
List<UTorrentTorrent> torrents; var torrents = GetTorrents();
try
{
var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.TvCategory);
var cache = _torrentCache.Find(cacheKey);
var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings);
if (cache != null && response.Torrents == null)
{
var removedAndUpdated = new HashSet<string>(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved));
torrents = cache.Torrents
.Where(v => !removedAndUpdated.Contains(v.Hash))
.Concat(response.TorrentsChanged)
.ToList();
}
else
{
torrents = response.Torrents;
}
cache = new UTorrentTorrentCache
{
CacheID = response.CacheNumber,
Torrents = torrents
};
_torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15));
}
catch (DownloadClientException ex)
{
_logger.Error(ex, ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>(); var queueItems = new List<DownloadClientItem>();
@ -173,6 +138,40 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
return queueItems; return queueItems;
} }
private List<UTorrentTorrent> GetTorrents()
{
List<UTorrentTorrent> torrents;
var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.TvCategory);
var cache = _torrentCache.Find(cacheKey);
var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings);
if (cache != null && response.Torrents == null)
{
var removedAndUpdated = new HashSet<string>(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved));
torrents = cache.Torrents
.Where(v => !removedAndUpdated.Contains(v.Hash))
.Concat(response.TorrentsChanged)
.ToList();
}
else
{
torrents = response.Torrents;
}
cache = new UTorrentTorrentCache
{
CacheID = response.CacheNumber,
Torrents = torrents
};
_torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15));
return torrents;
}
public override void RemoveItem(string downloadId, bool deleteData) public override void RemoveItem(string downloadId, bool deleteData)
{ {
_proxy.RemoveTorrent(downloadId, deleteData, Settings); _proxy.RemoveTorrent(downloadId, deleteData, Settings);

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using NLog; using NLog;
@ -244,7 +244,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
} }
catch (WebException ex) catch (WebException ex)
{ {
throw new DownloadClientException("Unable to connect to uTorrent, please check your settings", ex); throw new DownloadClientUnavailableException("Unable to connect to uTorrent, please check your settings", ex);
} }
cookies = response.GetCookies(); cookies = response.GetCookies();

View file

@ -1,5 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -9,17 +11,24 @@ namespace NzbDrone.Core.Download
{ {
public interface IDownloadClientFactory : IProviderFactory<IDownloadClient, DownloadClientDefinition> public interface IDownloadClientFactory : IProviderFactory<IDownloadClient, DownloadClientDefinition>
{ {
List<IDownloadClient> DownloadHandlingEnabled(bool filterBlockedClients = true);
} }
public class DownloadClientFactory : ProviderFactory<IDownloadClient, DownloadClientDefinition>, IDownloadClientFactory public class DownloadClientFactory : ProviderFactory<IDownloadClient, DownloadClientDefinition>, IDownloadClientFactory
{ {
private readonly IDownloadClientRepository _providerRepository; private readonly IDownloadClientStatusService _downloadClientStatusService;
private readonly Logger _logger;
public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable<IDownloadClient> providers, IContainer container, IEventAggregator eventAggregator, Logger logger) public DownloadClientFactory(IDownloadClientStatusService downloadClientStatusService,
IDownloadClientRepository providerRepository,
IEnumerable<IDownloadClient> providers,
IContainer container,
IEventAggregator eventAggregator,
Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
_providerRepository = providerRepository; _downloadClientStatusService = downloadClientStatusService;
_logger = logger;
} }
protected override List<DownloadClientDefinition> Active() protected override List<DownloadClientDefinition> Active()
@ -33,5 +42,46 @@ namespace NzbDrone.Core.Download
definition.Protocol = provider.Protocol; definition.Protocol = provider.Protocol;
} }
public List<IDownloadClient> DownloadHandlingEnabled(bool filterBlockedClients = true)
{
var enabledClients = GetAvailableProviders();
if (filterBlockedClients)
{
return FilterBlockedClients(enabledClients).ToList();
}
return enabledClients.ToList();
}
private IEnumerable<IDownloadClient> FilterBlockedClients(IEnumerable<IDownloadClient> clients)
{
var blockedIndexers = _downloadClientStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v);
foreach (var client in clients)
{
DownloadClientStatus downloadClientStatus;
if (blockedIndexers.TryGetValue(client.Definition.Id, out downloadClientStatus))
{
_logger.Debug("Temporarily ignoring download client {0} till {1} due to recent failures.", client.Definition.Name, downloadClientStatus.DisabledTill.Value.ToLocalTime());
continue;
}
yield return client;
}
}
public override ValidationResult Test(DownloadClientDefinition definition)
{
var result = base.Test(definition);
if ((result == null || result.IsValid) && definition.Id != 0)
{
_downloadClientStatusService.RecordSuccess(definition.Id);
}
return result;
}
} }
} }

View file

@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@ -27,19 +27,12 @@ namespace NzbDrone.Core.Download
public IEnumerable<IDownloadClient> GetDownloadClients() public IEnumerable<IDownloadClient> GetDownloadClients()
{ {
return _downloadClientFactory.GetAvailableProviders();//.Select(MapDownloadClient); return _downloadClientFactory.GetAvailableProviders();
} }
public IDownloadClient Get(int id) public IDownloadClient Get(int id)
{ {
return _downloadClientFactory.GetAvailableProviders().Single(d => d.Definition.Id == id); return _downloadClientFactory.GetAvailableProviders().Single(d => d.Definition.Id == id);
} }
public IDownloadClient MapDownloadClient(IDownloadClient downloadClient)
{
_downloadClientFactory.SetProviderCharacteristics(downloadClient, (DownloadClientDefinition)downloadClient.Definition);
return downloadClient;
}
} }
} }

View file

@ -0,0 +1,9 @@
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Download
{
public class DownloadClientStatus : ProviderStatusBase
{
}
}

View file

@ -0,0 +1,19 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Download
{
public interface IDownloadClientStatusRepository : IProviderStatusRepository<DownloadClientStatus>
{
}
public class DownloadClientStatusRepository : ProviderStatusRepository<DownloadClientStatus>, IDownloadClientStatusRepository
{
public DownloadClientStatusRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using NLog;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Download
{
public interface IDownloadClientStatusService : IProviderStatusServiceBase<DownloadClientStatus>
{
}
public class DownloadClientStatusService : ProviderStatusServiceBase<IDownloadClient, DownloadClientStatus>, IDownloadClientStatusService
{
public DownloadClientStatusService(IDownloadClientStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
: base(providerStatusRepository, eventAggregator, logger)
{
MinimumTimeSinceInitialFailure = TimeSpan.FromMinutes(5);
MaximumEscalationLevel = 5;
}
}
}

View file

@ -1,4 +1,4 @@
using System; using System;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -20,18 +20,21 @@ namespace NzbDrone.Core.Download
public class DownloadService : IDownloadService public class DownloadService : IDownloadService
{ {
private readonly IProvideDownloadClient _downloadClientProvider; private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IDownloadClientStatusService _downloadClientStatusService;
private readonly IIndexerStatusService _indexerStatusService; private readonly IIndexerStatusService _indexerStatusService;
private readonly IRateLimitService _rateLimitService; private readonly IRateLimitService _rateLimitService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
public DownloadService(IProvideDownloadClient downloadClientProvider, public DownloadService(IProvideDownloadClient downloadClientProvider,
IIndexerStatusService indexerStatusService, IDownloadClientStatusService downloadClientStatusService,
IRateLimitService rateLimitService, IIndexerStatusService indexerStatusService,
IEventAggregator eventAggregator, IRateLimitService rateLimitService,
Logger logger) IEventAggregator eventAggregator,
Logger logger)
{ {
_downloadClientProvider = downloadClientProvider; _downloadClientProvider = downloadClientProvider;
_downloadClientStatusService = downloadClientStatusService;
_indexerStatusService = indexerStatusService; _indexerStatusService = indexerStatusService;
_rateLimitService = rateLimitService; _rateLimitService = rateLimitService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@ -63,6 +66,7 @@ namespace NzbDrone.Core.Download
try try
{ {
downloadClientId = downloadClient.Download(remoteAlbum); downloadClientId = downloadClient.Download(remoteAlbum);
_downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id);
_indexerStatusService.RecordSuccess(remoteAlbum.Release.IndexerId); _indexerStatusService.RecordSuccess(remoteAlbum.Release.IndexerId);
} }
catch (ReleaseDownloadException ex) catch (ReleaseDownloadException ex)
@ -91,4 +95,4 @@ namespace NzbDrone.Core.Download
_eventAggregator.PublishEvent(albumGrabbedEvent); _eventAggregator.PublishEvent(albumGrabbedEvent);
} }
} }
} }

View file

@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download.Pending
public DateTime Added { get; set; } public DateTime Added { get; set; }
public ParsedAlbumInfo ParsedAlbumInfo { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
public ReleaseInfo Release { get; set; } public ReleaseInfo Release { get; set; }
public PendingReleaseReason Reason { get; set; }
//Not persisted //Not persisted
public RemoteAlbum RemoteAlbum { get; set; } public RemoteAlbum RemoteAlbum { get; set; }

View file

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Pending
{
public enum PendingReleaseReason
{
Delay = 0,
DownloadClientUnavailable = 1,
Fallback = 2
}
}

View file

@ -20,8 +20,7 @@ namespace NzbDrone.Core.Download.Pending
{ {
public interface IPendingReleaseService public interface IPendingReleaseService
{ {
void Add(DownloadDecision decision); void Add(DownloadDecision decision, PendingReleaseReason reason);
List<ReleaseInfo> GetPending(); List<ReleaseInfo> GetPending();
List<RemoteAlbum> GetPendingRemoteAlbums(int artistId); List<RemoteAlbum> GetPendingRemoteAlbums(int artistId);
List<Queue.Queue> GetPendingQueue(); List<Queue.Queue> GetPendingQueue();
@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Pending
} }
public void Add(DownloadDecision decision) public void Add(DownloadDecision decision, PendingReleaseReason reason)
{ {
var alreadyPending = GetPendingReleases(); var alreadyPending = GetPendingReleases();
@ -77,14 +76,32 @@ namespace NzbDrone.Core.Download.Pending
.Intersect(albumIds) .Intersect(albumIds)
.Any()); .Any());
if (existingReports.Any(MatchingReleasePredicate(decision.RemoteAlbum.Release))) var matchingReports = existingReports.Where(MatchingReleasePredicate(decision.RemoteAlbum.Release)).ToList();
if (matchingReports.Any())
{ {
_logger.Debug("This release is already pending, not adding again"); var sameReason = true;
return;
foreach (var matchingReport in matchingReports)
{
if (matchingReport.Reason != reason)
{
_logger.Debug("This release is already pending with reason {0}, changing to {1}", matchingReport.Reason, reason);
matchingReport.Reason = reason;
_repository.Update(matchingReport);
sameReason = false;
}
}
if (sameReason)
{
_logger.Debug("This release is already pending with reason {0}, not adding again", reason);
return;
}
} }
_logger.Debug("Adding release to pending releases"); _logger.Debug("Adding release to pending releases with reason {0}", reason);
Insert(decision); Insert(decision, reason);
} }
public List<ReleaseInfo> GetPending() public List<ReleaseInfo> GetPending()
@ -101,7 +118,7 @@ namespace NzbDrone.Core.Download.Pending
private List<ReleaseInfo> FilterBlockedIndexers(List<ReleaseInfo> releases) private List<ReleaseInfo> FilterBlockedIndexers(List<ReleaseInfo> releases)
{ {
var blockedIndexers = new HashSet<int>(_indexerStatusService.GetBlockedIndexers().Select(v => v.ProviderId)); var blockedIndexers = new HashSet<int>(_indexerStatusService.GetBlockedProviders().Select(v => v.ProviderId));
return releases.Where(release => !blockedIndexers.Contains(release.IndexerId)).ToList(); return releases.Where(release => !blockedIndexers.Contains(release.IndexerId)).ToList();
} }
@ -117,7 +134,7 @@ namespace NzbDrone.Core.Download.Pending
var nextRssSync = new Lazy<DateTime>(() => _taskManager.GetNextExecution(typeof(RssSyncCommand))); var nextRssSync = new Lazy<DateTime>(() => _taskManager.GetNextExecution(typeof(RssSyncCommand)));
foreach (var pendingRelease in GetPendingReleases()) foreach (var pendingRelease in GetPendingReleases().Where(p => p.Reason != PendingReleaseReason.Fallback))
{ {
foreach (var album in pendingRelease.RemoteAlbum.Albums) foreach (var album in pendingRelease.RemoteAlbum.Albums)
{ {
@ -132,6 +149,13 @@ namespace NzbDrone.Core.Download.Pending
ect = ect.AddMinutes(_configService.RssSyncInterval); ect = ect.AddMinutes(_configService.RssSyncInterval);
} }
var timeleft = ect.Subtract(DateTime.UtcNow);
if (timeleft.TotalSeconds < 0)
{
timeleft = TimeSpan.Zero;
}
var queue = new Queue.Queue var queue = new Queue.Queue
{ {
Id = GetQueueId(pendingRelease, album), Id = GetQueueId(pendingRelease, album),
@ -142,12 +166,13 @@ namespace NzbDrone.Core.Download.Pending
Size = pendingRelease.RemoteAlbum.Release.Size, Size = pendingRelease.RemoteAlbum.Release.Size,
Sizeleft = pendingRelease.RemoteAlbum.Release.Size, Sizeleft = pendingRelease.RemoteAlbum.Release.Size,
RemoteAlbum = pendingRelease.RemoteAlbum, RemoteAlbum = pendingRelease.RemoteAlbum,
Timeleft = ect.Subtract(DateTime.UtcNow), Timeleft = timeleft,
EstimatedCompletionTime = ect, EstimatedCompletionTime = ect,
Status = "Pending", Status = pendingRelease.Reason.ToString(),
Protocol = pendingRelease.RemoteAlbum.Release.DownloadProtocol, Protocol = pendingRelease.RemoteAlbum.Release.DownloadProtocol,
Indexer = pendingRelease.RemoteAlbum.Release.Indexer Indexer = pendingRelease.RemoteAlbum.Release.Indexer
}; };
queued.Add(queue); queued.Add(queue);
} }
} }
@ -230,7 +255,7 @@ namespace NzbDrone.Core.Download.Pending
}; };
} }
private void Insert(DownloadDecision decision) private void Insert(DownloadDecision decision, PendingReleaseReason reason)
{ {
_repository.Insert(new PendingRelease _repository.Insert(new PendingRelease
{ {
@ -238,7 +263,8 @@ namespace NzbDrone.Core.Download.Pending
ParsedAlbumInfo = decision.RemoteAlbum.ParsedAlbumInfo, ParsedAlbumInfo = decision.RemoteAlbum.ParsedAlbumInfo,
Release = decision.RemoteAlbum.Release, Release = decision.RemoteAlbum.Release,
Title = decision.RemoteAlbum.Release.Title, Title = decision.RemoteAlbum.Release.Title,
Added = DateTime.UtcNow Added = DateTime.UtcNow,
Reason = reason
}); });
_eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent()); _eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent());

View file

@ -1,9 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using NLog; using NLog;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
{ {
@ -36,36 +39,33 @@ namespace NzbDrone.Core.Download
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
var grabbed = new List<DownloadDecision>(); var grabbed = new List<DownloadDecision>();
var pending = new List<DownloadDecision>(); var pending = new List<DownloadDecision>();
var failed = new List<DownloadDecision>();
var usenetFailed = false;
var torrentFailed = false;
foreach (var report in prioritizedDecisions) foreach (var report in prioritizedDecisions)
{ {
var remoteAlbum = report.RemoteAlbum; var remoteAlbum = report.RemoteAlbum;
var downloadProtocol = report.RemoteAlbum.Release.DownloadProtocol;
var albumIds = remoteAlbum.Albums.Select(e => e.Id).ToList();
//Skip if already grabbed //Skip if already grabbed
if (grabbed.SelectMany(r => r.RemoteAlbum.Albums) if (IsAlbumProcessed(grabbed, report))
.Select(e => e.Id)
.ToList()
.Intersect(albumIds)
.Any())
{ {
continue; continue;
} }
if (report.TemporarilyRejected) if (report.TemporarilyRejected)
{ {
_pendingReleaseService.Add(report); _pendingReleaseService.Add(report, PendingReleaseReason.Delay);
pending.Add(report); pending.Add(report);
continue; continue;
} }
if (pending.SelectMany(r => r.RemoteAlbum.Albums) if (downloadProtocol == DownloadProtocol.Usenet && usenetFailed ||
.Select(e => e.Id) downloadProtocol == DownloadProtocol.Torrent && torrentFailed)
.ToList()
.Intersect(albumIds)
.Any())
{ {
failed.Add(report);
continue; continue;
} }
@ -74,14 +74,31 @@ namespace NzbDrone.Core.Download
_downloadService.DownloadReport(remoteAlbum); _downloadService.DownloadReport(remoteAlbum);
grabbed.Add(report); grabbed.Add(report);
} }
catch (Exception e) catch (Exception ex)
{ {
//TODO: support for store & forward if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException)
//We'll need to differentiate between a download client error and an indexer error {
_logger.Warn(e, "Couldn't add report to download queue. " + remoteAlbum); _logger.Debug("Failed to send release to download client, storing until later");
failed.Add(report);
if (downloadProtocol == DownloadProtocol.Usenet)
{
usenetFailed = true;
}
else if (downloadProtocol == DownloadProtocol.Torrent)
{
torrentFailed = true;
}
}
else
{
_logger.Warn(ex, "Couldn't add report to download queue. " + remoteAlbum);
}
} }
} }
pending.AddRange(ProcessFailedGrabs(grabbed, failed));
return new ProcessedDecisions(grabbed, pending, decisions.Where(d => d.Rejected).ToList()); return new ProcessedDecisions(grabbed, pending, decisions.Where(d => d.Rejected).ToList());
} }
@ -90,5 +107,50 @@ namespace NzbDrone.Core.Download
//Process both approved and temporarily rejected //Process both approved and temporarily rejected
return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteAlbum.Albums.Any()).ToList(); return decisions.Where(c => (c.Approved || c.TemporarilyRejected) && c.RemoteAlbum.Albums.Any()).ToList();
} }
private bool IsAlbumProcessed(List<DownloadDecision> decisions, DownloadDecision report)
{
var albumIds = report.RemoteAlbum.Albums.Select(e => e.Id).ToList();
return decisions.SelectMany(r => r.RemoteAlbum.Albums)
.Select(e => e.Id)
.ToList()
.Intersect(albumIds)
.Any();
}
private List<DownloadDecision> ProcessFailedGrabs(List<DownloadDecision> grabbed, List<DownloadDecision> failed)
{
var pending = new List<DownloadDecision>();
var stored = new List<DownloadDecision>();
foreach (var report in failed)
{
// If a release was already grabbed with matching albums we should store it as a fallback
// and filter it out the next time it is processed incase a higher quality release failed to
// add to the download client, but a lower quality release was sent to another client
// If the release wasn't grabbed already, but was already stored, store it as a fallback,
// otherwise store it as DownloadClientUnavailable.
if (IsAlbumProcessed(grabbed, report))
{
_pendingReleaseService.Add(report, PendingReleaseReason.Fallback);
pending.Add(report);
}
else if (IsAlbumProcessed(stored, report))
{
_pendingReleaseService.Add(report, PendingReleaseReason.Fallback);
pending.Add(report);
}
else
{
_pendingReleaseService.Add(report, PendingReleaseReason.DownloadClientUnavailable);
pending.Add(report);
stored.Add(report);
}
}
return pending;
}
} }
} }

View file

@ -17,7 +17,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads
IHandle<TrackedDownloadsRemovedEvent> IHandle<TrackedDownloadsRemovedEvent>
{ {
private readonly IProvideDownloadClient _downloadClientProvider; private readonly IDownloadClientStatusService _downloadClientStatusService;
private readonly IDownloadClientFactory _downloadClientFactory;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _manageCommandQueue; private readonly IManageCommandQueue _manageCommandQueue;
private readonly IConfigService _configService; private readonly IConfigService _configService;
@ -27,16 +28,18 @@ namespace NzbDrone.Core.Download.TrackedDownloads
private readonly Logger _logger; private readonly Logger _logger;
private readonly Debouncer _refreshDebounce; private readonly Debouncer _refreshDebounce;
public DownloadMonitoringService(IProvideDownloadClient downloadClientProvider, public DownloadMonitoringService(IDownloadClientStatusService downloadClientStatusService,
IEventAggregator eventAggregator, IDownloadClientFactory downloadClientFactory,
IManageCommandQueue manageCommandQueue, IEventAggregator eventAggregator,
IConfigService configService, IManageCommandQueue manageCommandQueue,
IFailedDownloadService failedDownloadService, IConfigService configService,
ICompletedDownloadService completedDownloadService, IFailedDownloadService failedDownloadService,
ITrackedDownloadService trackedDownloadService, ICompletedDownloadService completedDownloadService,
Logger logger) ITrackedDownloadService trackedDownloadService,
Logger logger)
{ {
_downloadClientProvider = downloadClientProvider; _downloadClientStatusService = downloadClientStatusService;
_downloadClientFactory = downloadClientFactory;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_manageCommandQueue = manageCommandQueue; _manageCommandQueue = manageCommandQueue;
_configService = configService; _configService = configService;
@ -58,7 +61,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
_refreshDebounce.Pause(); _refreshDebounce.Pause();
try try
{ {
var downloadClients = _downloadClientProvider.GetDownloadClients(); var downloadClients = _downloadClientFactory.DownloadHandlingEnabled();
var trackedDownloads = new List<TrackedDownload>(); var trackedDownloads = new List<TrackedDownload>();
@ -86,9 +89,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads
try try
{ {
downloadClientHistory = downloadClient.GetItems().ToList(); downloadClientHistory = downloadClient.GetItems().ToList();
_downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id);
} }
catch (Exception ex) catch (Exception ex)
{ {
_downloadClientStatusService.RecordFailure(downloadClient.Definition.Id);
_logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name); _logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name);
} }

View file

@ -0,0 +1,16 @@
using System;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.HealthCheck
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class CheckOnAttribute : Attribute
{
public Type EventType { get; set; }
public CheckOnAttribute(Type eventType)
{
EventType = eventType;
}
}
}

View file

@ -1,5 +1,6 @@
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
@ -22,7 +23,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
public override bool CheckOnConfigChange => false;
} }
} }

View file

@ -1,10 +1,13 @@
using System; using System;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ProviderUpdatedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
public class DownloadClientCheck : HealthCheckBase public class DownloadClientCheck : HealthCheckBase
{ {
private readonly IProvideDownloadClient _downloadClientProvider; private readonly IProvideDownloadClient _downloadClientProvider;
@ -33,11 +36,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Debug(ex, "Unable to communicate with {0}", downloadClient.Definition.Name);
_logger.Error(ex, "Unable to communicate with {0}", downloadClient.Definition.Name);
var message = $"Unable to communicate with {downloadClient.Definition.Name}."; var message = $"Unable to communicate with {downloadClient.Definition.Name}.";
return new HealthCheck(GetType(), HealthCheckResult.Error, $"{message} {ex.Message}"); return new HealthCheck(GetType(), HealthCheckResult.Error, $"{message} {ex.Message}", "#unable-to-communicate-with-download-client");
} }
} }

View file

@ -0,0 +1,45 @@
using System;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IDownloadClient>))]
public class DownloadClientStatusCheck : HealthCheckBase
{
private readonly IDownloadClientFactory _providerFactory;
private readonly IDownloadClientStatusService _providerStatusService;
public DownloadClientStatusCheck(IDownloadClientFactory providerFactory, IDownloadClientStatusService providerStatusService)
{
_providerFactory = providerFactory;
_providerStatusService = providerStatusService;
}
public override HealthCheck Check()
{
var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id,
s => s.ProviderId,
(i, s) => new { Provider = i, Status = s })
.ToList();
if (backOffProviders.Empty())
{
return new HealthCheck(GetType());
}
if (backOffProviders.Count == enabledProviders.Count)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, "All download clients are unavailable due to failures", "#download-clients-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Download clients unavailable due to failures: {0}", string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))), "#download-clients-are-unavailable-due-to-failures");
}
}
}

View file

@ -2,13 +2,18 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Core.Download.Clients.Sabnzbd; using NzbDrone.Core.Download.Clients.Sabnzbd;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ProviderUpdatedEvent<IDownloadClient>))]
[CheckOn(typeof(ProviderDeletedEvent<IDownloadClient>))]
[CheckOn(typeof(ConfigSavedEvent))]
public class ImportMechanismCheck : HealthCheckBase public class ImportMechanismCheck : HealthCheckBase
{ {
private readonly IConfigService _configService; private readonly IConfigService _configService;
@ -33,7 +38,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
Status = v.GetStatus() Status = v.GetStatus()
}).ToList(); }).ToList();
} }
catch (DownloadClientException) catch (Exception)
{ {
// One or more download clients failed, assume the health is okay and verify later // One or more download clients failed, assume the health is okay and verify later
return new HealthCheck(GetType()); return new HealthCheck(GetType());

View file

@ -1,9 +1,13 @@
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerRssCheck : HealthCheckBase public class IndexerRssCheck : HealthCheckBase
{ {
private readonly IIndexerFactory _indexerFactory; private readonly IIndexerFactory _indexerFactory;

View file

@ -1,9 +1,13 @@
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerSearchCheck : HealthCheckBase public class IndexerSearchCheck : HealthCheckBase
{ {
private readonly IIndexerFactory _indexerFactory; private readonly IIndexerFactory _indexerFactory;

View file

@ -1,42 +1,45 @@
using System; using System;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerStatusCheck : HealthCheckBase public class IndexerStatusCheck : HealthCheckBase
{ {
private readonly IIndexerFactory _indexerFactory; private readonly IIndexerFactory _providerFactory;
private readonly IIndexerStatusService _indexerStatusService; private readonly IIndexerStatusService _providerStatusService;
public IndexerStatusCheck(IIndexerFactory indexerFactory, IIndexerStatusService indexerStatusService) public IndexerStatusCheck(IIndexerFactory providerFactory, IIndexerStatusService providerStatusService)
{ {
_indexerFactory = indexerFactory; _providerFactory = providerFactory;
_indexerStatusService = indexerStatusService; _providerStatusService = providerStatusService;
} }
public override HealthCheck Check() public override HealthCheck Check()
{ {
var enabledIndexers = _indexerFactory.GetAvailableProviders(); var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffIndexers = enabledIndexers.Join(_indexerStatusService.GetBlockedIndexers(), var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id, i => i.Definition.Id,
s => s.ProviderId, s => s.ProviderId,
(i, s) => new { Indexer = i, Status = s }) (i, s) => new { Indexer = i, Status = s })
.Where(v => (v.Status.MostRecentFailure - v.Status.InitialFailure) > TimeSpan.FromHours(1))
.ToList(); .ToList();
if (backOffIndexers.Empty()) if (backOffProviders.Empty())
{ {
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
if (backOffIndexers.Count == enabledIndexers.Count) if (backOffProviders.Count == enabledProviders.Count)
{ {
return new HealthCheck(GetType(), HealthCheckResult.Error, "All indexers are unavailable due to failures", "#indexers-are-unavailable-due-to-failures"); return new HealthCheck(GetType(), HealthCheckResult.Error, "All indexers are unavailable due to failures", "#indexers-are-unavailable-due-to-failures");
} }
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Indexers unavailable due to failures: {0}", string.Join(", ", backOffIndexers.Select(v => v.Indexer.Definition.Name))), "#indexers-are-unavailable-due-to-failures"); return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Indexers unavailable due to failures: {0}", string.Join(", ", backOffProviders.Select(v => v.Indexer.Definition.Name))), "#indexers-are-unavailable-due-to-failures");
} }
} }
} }

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MediaInfo;
@ -20,7 +20,5 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
public override bool CheckOnConfigChange => false;
} }
} }

View file

@ -41,8 +41,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability."); return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability.");
} }
public override bool CheckOnConfigChange => false;
public override bool CheckOnSchedule => false; public override bool CheckOnSchedule => false;
} }

View file

@ -1,13 +1,15 @@
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Core.Configuration.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ConfigSavedEvent))]
public class ProxyCheck : HealthCheckBase public class ProxyCheck : HealthCheckBase
{ {
private readonly Logger _logger; private readonly Logger _logger;
@ -43,7 +45,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
{ {
var response = _client.Execute(request); var response = _client.Execute(request);
// We only care about 400 responses, other error codes can be ignored // We only care about 400 responses, other error codes can be ignored
if (response.StatusCode == HttpStatusCode.BadRequest) if (response.StatusCode == HttpStatusCode.BadRequest)
{ {
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode); _logger.Error("Proxy Health Check failed: {0}", response.StatusCode);

View file

@ -1,9 +1,12 @@
using System.Linq; using System.Linq;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ArtistDeletedEvent))]
[CheckOn(typeof(ArtistMovedEvent))]
public class RootFolderCheck : HealthCheckBase public class RootFolderCheck : HealthCheckBase
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
@ -36,7 +39,5 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
public override bool CheckOnConfigChange => false;
} }
} }

View file

@ -1,13 +1,15 @@
using System; using System;
using System.IO; using System.IO;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
[CheckOn(typeof(ConfigFileSavedEvent))]
public class UpdateCheck : HealthCheckBase public class UpdateCheck : HealthCheckBase
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
@ -66,7 +68,5 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
public override bool CheckOnConfigChange => false;
} }
} }

View file

@ -1,4 +1,4 @@
namespace NzbDrone.Core.HealthCheck namespace NzbDrone.Core.HealthCheck
{ {
public abstract class HealthCheckBase : IProvideHealthCheck public abstract class HealthCheckBase : IProvideHealthCheck
{ {
@ -6,8 +6,6 @@
public virtual bool CheckOnStartup => true; public virtual bool CheckOnStartup => true;
public virtual bool CheckOnConfigChange => true;
public virtual bool CheckOnSchedule => true; public virtual bool CheckOnSchedule => true;
} }
} }

View file

@ -1,8 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Messaging;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@ -21,13 +24,12 @@ namespace NzbDrone.Core.HealthCheck
public class HealthCheckService : IHealthCheckService, public class HealthCheckService : IHealthCheckService,
IExecute<CheckHealthCommand>, IExecute<CheckHealthCommand>,
IHandleAsync<ApplicationStartedEvent>, IHandleAsync<ApplicationStartedEvent>,
IHandleAsync<ConfigSavedEvent>, IHandleAsync<IEvent>
IHandleAsync<ProviderUpdatedEvent<IIndexer>>,
IHandleAsync<ProviderDeletedEvent<IIndexer>>,
IHandleAsync<ProviderUpdatedEvent<IDownloadClient>>,
IHandleAsync<ProviderDeletedEvent<IDownloadClient>>
{ {
private readonly IEnumerable<IProvideHealthCheck> _healthChecks; private readonly IProvideHealthCheck[] _healthChecks;
private readonly IProvideHealthCheck[] _startupHealthChecks;
private readonly IProvideHealthCheck[] _scheduledHealthChecks;
private readonly Dictionary<Type, IProvideHealthCheck[]> _eventDrivenHealthChecks;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ICacheManager _cacheManager; private readonly ICacheManager _cacheManager;
private readonly Logger _logger; private readonly Logger _logger;
@ -39,12 +41,16 @@ namespace NzbDrone.Core.HealthCheck
ICacheManager cacheManager, ICacheManager cacheManager,
Logger logger) Logger logger)
{ {
_healthChecks = healthChecks; _healthChecks = healthChecks.ToArray();
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_cacheManager = cacheManager; _cacheManager = cacheManager;
_logger = logger; _logger = logger;
_healthCheckResults = _cacheManager.GetCache<HealthCheck>(GetType()); _healthCheckResults = _cacheManager.GetCache<HealthCheck>(GetType());
_startupHealthChecks = _healthChecks.Where(v => v.CheckOnStartup).ToArray();
_scheduledHealthChecks = _healthChecks.Where(v => v.CheckOnSchedule).ToArray();
_eventDrivenHealthChecks = GetEventDrivenHealthChecks();
} }
public List<HealthCheck> Results() public List<HealthCheck> Results()
@ -52,11 +58,18 @@ namespace NzbDrone.Core.HealthCheck
return _healthCheckResults.Values.ToList(); return _healthCheckResults.Values.ToList();
} }
private void PerformHealthCheck(Func<IProvideHealthCheck, bool> predicate) private Dictionary<Type, IProvideHealthCheck[]> GetEventDrivenHealthChecks()
{ {
var results = _healthChecks.Where(predicate) return _healthChecks
.Select(c => c.Check()) .SelectMany(h => h.GetType().GetAttributes<CheckOnAttribute>().Select(a => Tuple.Create(a.EventType, h)))
.ToList(); .GroupBy(t => t.Item1, t => t.Item2)
.ToDictionary(g => g.Key, g => g.ToArray());
}
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks)
{
var results = healthChecks.Select(c => c.Check())
.ToList();
foreach (var result in results) foreach (var result in results)
{ {
@ -76,37 +89,37 @@ namespace NzbDrone.Core.HealthCheck
public void Execute(CheckHealthCommand message) public void Execute(CheckHealthCommand message)
{ {
PerformHealthCheck(c => message.Trigger == CommandTrigger.Manual || c.CheckOnSchedule); if (message.Trigger == CommandTrigger.Manual)
{
PerformHealthCheck(_healthChecks);
}
else
{
PerformHealthCheck(_scheduledHealthChecks);
}
} }
public void HandleAsync(ApplicationStartedEvent message) public void HandleAsync(ApplicationStartedEvent message)
{ {
PerformHealthCheck(c => c.CheckOnStartup); PerformHealthCheck(_startupHealthChecks);
} }
public void HandleAsync(ConfigSavedEvent message) public void HandleAsync(IEvent message)
{ {
PerformHealthCheck(c => c.CheckOnConfigChange); if (message is HealthCheckCompleteEvent)
} {
return;
}
public void HandleAsync(ProviderUpdatedEvent<IIndexer> message) IProvideHealthCheck[] checks;
{ if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks))
PerformHealthCheck(c => c.CheckOnConfigChange); {
} return;
}
public void HandleAsync(ProviderDeletedEvent<IIndexer> message) // TODO: Add debounce
{
PerformHealthCheck(c => c.CheckOnConfigChange);
}
public void HandleAsync(ProviderUpdatedEvent<IDownloadClient> message) PerformHealthCheck(checks);
{
PerformHealthCheck(c => c.CheckOnConfigChange);
}
public void HandleAsync(ProviderDeletedEvent<IDownloadClient> message)
{
PerformHealthCheck(c => c.CheckOnConfigChange);
} }
} }
} }

View file

@ -1,10 +1,9 @@
namespace NzbDrone.Core.HealthCheck namespace NzbDrone.Core.HealthCheck
{ {
public interface IProvideHealthCheck public interface IProvideHealthCheck
{ {
HealthCheck Check(); HealthCheck Check();
bool CheckOnStartup { get; } bool CheckOnStartup { get; }
bool CheckOnConfigChange { get; }
bool CheckOnSchedule { get; } bool CheckOnSchedule { get; }
} }
} }

View file

@ -0,0 +1,32 @@
using System;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download.Pending;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupDownloadClientUnavailablePendingReleases : IHousekeepingTask
{
private readonly IMainDatabase _database;
public CleanupDownloadClientUnavailablePendingReleases(IMainDatabase database)
{
_database = database;
}
public void Clean()
{
var mapper = _database.GetDataMapper();
var twoWeeksAgo = DateTime.UtcNow.AddDays(-14);
mapper.Delete<PendingRelease>(p => p.Added < twoWeeksAgo &&
(p.Reason == PendingReleaseReason.DownloadClientUnavailable ||
p.Reason == PendingReleaseReason.Fallback));
// mapper.AddParameter("twoWeeksAgo", $"{DateTime.UtcNow.AddDays(-14).ToString("s")}Z");
// mapper.ExecuteNonQuery(@"DELETE FROM PendingReleases
// WHERE Added < @twoWeeksAgo
// AND (Reason = 'DownloadClientUnavailable' OR Reason = 'Fallback')");
}
}
}

View file

@ -0,0 +1,26 @@
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupOrphanedDownloadClientStatus : IHousekeepingTask
{
private readonly IMainDatabase _database;
public CleanupOrphanedDownloadClientStatus(IMainDatabase database)
{
_database = database;
}
public void Clean()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM DownloadClientStatus
WHERE Id IN (
SELECT DownloadClientStatus.Id FROM DownloadClientStatus
LEFT OUTER JOIN DownloadClients
ON DownloadClientStatus.ProviderId = DownloadClients.Id
WHERE DownloadClients.Id IS NULL)");
}
}
}

View file

@ -95,11 +95,6 @@ namespace NzbDrone.Core.Indexers
failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message)); failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message));
} }
if (Definition.Id != 0)
{
_indexerStatusService.RecordSuccess(Definition.Id);
}
return new ValidationResult(failures); return new ValidationResult(failures);
} }

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -21,7 +22,7 @@ namespace NzbDrone.Core.Indexers
public IndexerFactory(IIndexerStatusService indexerStatusService, public IndexerFactory(IIndexerStatusService indexerStatusService,
IIndexerRepository providerRepository, IIndexerRepository providerRepository,
IEnumerable<IIndexer> providers, IEnumerable<IIndexer> providers,
IContainer container, IContainer container,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)
@ -70,7 +71,7 @@ namespace NzbDrone.Core.Indexers
private IEnumerable<IIndexer> FilterBlockedIndexers(IEnumerable<IIndexer> indexers) private IEnumerable<IIndexer> FilterBlockedIndexers(IEnumerable<IIndexer> indexers)
{ {
var blockedIndexers = _indexerStatusService.GetBlockedIndexers().ToDictionary(v => v.ProviderId, v => v); var blockedIndexers = _indexerStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v);
foreach (var indexer in indexers) foreach (var indexer in indexers)
{ {
@ -84,5 +85,17 @@ namespace NzbDrone.Core.Indexers
yield return indexer; yield return indexer;
} }
} }
public override ValidationResult Test(IndexerDefinition definition)
{
var result = base.Test(definition);
if ((result == null || result.IsValid) && definition.Id != 0)
{
_indexerStatusService.RecordSuccess(definition.Id);
}
return result;
}
} }
} }

View file

@ -1,23 +1,10 @@
using System;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
{ {
public class IndexerStatus : ModelBase public class IndexerStatus : ProviderStatusBase
{ {
public int ProviderId { get; set; }
public DateTime? InitialFailure { get; set; }
public DateTime? MostRecentFailure { get; set; }
public int EscalationLevel { get; set; }
public DateTime? DisabledTill { get; set; }
public ReleaseInfo LastRssSyncReleaseInfo { get; set; } public ReleaseInfo LastRssSyncReleaseInfo { get; set; }
public bool IsDisabled()
{
return DisabledTill.HasValue && DisabledTill.Value > DateTime.UtcNow;
}
} }
} }

View file

@ -1,26 +1,20 @@
using System.Linq;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
{ {
public interface IIndexerStatusRepository : IProviderRepository<IndexerStatus> public interface IIndexerStatusRepository : IProviderStatusRepository<IndexerStatus>
{ {
IndexerStatus FindByIndexerId(int indexerId); }
}
public class IndexerStatusRepository : ProviderStatusRepository<IndexerStatus>, IIndexerStatusRepository
public class IndexerStatusRepository : ProviderRepository<IndexerStatus>, IIndexerStatusRepository
{ {
public IndexerStatusRepository(IMainDatabase database, IEventAggregator eventAggregator) public IndexerStatusRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator) : base(database, eventAggregator)
{ {
} }
public IndexerStatus FindByIndexerId(int indexerId)
{
return Query.Where(c => c.ProviderId == indexerId).SingleOrDefault();
}
} }
} }

View file

@ -1,131 +1,31 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.ThingiProvider.Events;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
{ {
public interface IIndexerStatusService public interface IIndexerStatusService : IProviderStatusServiceBase<IndexerStatus>
{ {
List<IndexerStatus> GetBlockedIndexers();
ReleaseInfo GetLastRssSyncReleaseInfo(int indexerId); ReleaseInfo GetLastRssSyncReleaseInfo(int indexerId);
void RecordSuccess(int indexerId);
void RecordFailure(int indexerId, TimeSpan minimumBackOff = default(TimeSpan));
void RecordConnectionFailure(int indexerId);
void UpdateRssSyncStatus(int indexerId, ReleaseInfo releaseInfo); void UpdateRssSyncStatus(int indexerId, ReleaseInfo releaseInfo);
} }
public class IndexerStatusService : IIndexerStatusService, IHandleAsync<ProviderDeletedEvent<IIndexer>> public class IndexerStatusService : ProviderStatusServiceBase<IIndexer, IndexerStatus>, IIndexerStatusService
{ {
private static readonly int[] EscalationBackOffPeriods = { public IndexerStatusService(IIndexerStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
0, : base(providerStatusRepository, eventAggregator, logger)
5 * 60,
15 * 60,
30 * 60,
60 * 60,
3 * 60 * 60,
6 * 60 * 60,
12 * 60 * 60,
24 * 60 * 60
};
private static readonly int MaximumEscalationLevel = EscalationBackOffPeriods.Length - 1;
private static readonly object _syncRoot = new object();
private readonly IIndexerStatusRepository _indexerStatusRepository;
private readonly Logger _logger;
public IndexerStatusService(IIndexerStatusRepository indexerStatusRepository, Logger logger)
{ {
_indexerStatusRepository = indexerStatusRepository;
_logger = logger;
}
public List<IndexerStatus> GetBlockedIndexers()
{
return _indexerStatusRepository.All().Where(v => v.IsDisabled()).ToList();
} }
public ReleaseInfo GetLastRssSyncReleaseInfo(int indexerId) public ReleaseInfo GetLastRssSyncReleaseInfo(int indexerId)
{ {
return GetIndexerStatus(indexerId).LastRssSyncReleaseInfo; return GetProviderStatus(indexerId).LastRssSyncReleaseInfo;
}
private IndexerStatus GetIndexerStatus(int indexerId)
{
return _indexerStatusRepository.FindByIndexerId(indexerId) ?? new IndexerStatus { ProviderId = indexerId };
}
private TimeSpan CalculateBackOffPeriod(IndexerStatus status)
{
var level = Math.Min(MaximumEscalationLevel, status.EscalationLevel);
return TimeSpan.FromSeconds(EscalationBackOffPeriods[level]);
}
public void RecordSuccess(int indexerId)
{
lock (_syncRoot)
{
var status = GetIndexerStatus(indexerId);
if (status.EscalationLevel == 0)
{
return;
}
status.EscalationLevel--;
status.DisabledTill = null;
_indexerStatusRepository.Upsert(status);
}
}
protected void RecordFailure(int indexerId, TimeSpan minimumBackOff, bool escalate)
{
lock (_syncRoot)
{
var status = GetIndexerStatus(indexerId);
var now = DateTime.UtcNow;
if (status.EscalationLevel == 0)
{
status.InitialFailure = now;
}
status.MostRecentFailure = now;
if (escalate)
{
status.EscalationLevel = Math.Min(MaximumEscalationLevel, status.EscalationLevel + 1);
}
if (minimumBackOff != TimeSpan.Zero)
{
while (status.EscalationLevel < MaximumEscalationLevel && CalculateBackOffPeriod(status) < minimumBackOff)
{
status.EscalationLevel++;
}
}
status.DisabledTill = now + CalculateBackOffPeriod(status);
_indexerStatusRepository.Upsert(status);
}
}
public void RecordFailure(int indexerId, TimeSpan minimumBackOff = default(TimeSpan))
{
RecordFailure(indexerId, minimumBackOff, true);
}
public void RecordConnectionFailure(int indexerId)
{
RecordFailure(indexerId, default(TimeSpan), false);
} }
@ -133,21 +33,11 @@ namespace NzbDrone.Core.Indexers
{ {
lock (_syncRoot) lock (_syncRoot)
{ {
var status = GetIndexerStatus(indexerId); var status = GetProviderStatus(indexerId);
status.LastRssSyncReleaseInfo = releaseInfo; status.LastRssSyncReleaseInfo = releaseInfo;
_indexerStatusRepository.Upsert(status); _providerStatusRepository.Upsert(status);
}
}
public void HandleAsync(ProviderDeletedEvent<IIndexer> message)
{
var indexerStatus = _indexerStatusRepository.FindByIndexerId(message.ProviderId);
if (indexerStatus != null)
{
_indexerStatusRepository.Delete(indexerStatus);
} }
} }
} }

View file

@ -62,6 +62,17 @@ namespace NzbDrone.Core.Messaging.Events
} }
} }
foreach (var handler in _serviceFactory.BuildAll<IHandleAsync<IEvent>>())
{
var handlerLocal = handler;
_taskFactory.StartNew(() =>
{
handlerLocal.HandleAsync(@event);
}, TaskCreationOptions.PreferFairness)
.LogExceptions();
}
foreach (var handler in _serviceFactory.BuildAll<IHandleAsync<TEvent>>()) foreach (var handler in _serviceFactory.BuildAll<IHandleAsync<TEvent>>())
{ {
var handlerLocal = handler; var handlerLocal = handler;

View file

@ -170,6 +170,7 @@
<Compile Include="Datastore\MainDatabase.cs" /> <Compile Include="Datastore\MainDatabase.cs" />
<Compile Include="Datastore\LogDatabase.cs" /> <Compile Include="Datastore\LogDatabase.cs" />
<Compile Include="Datastore\Migration\001_initial_setup.cs" /> <Compile Include="Datastore\Migration\001_initial_setup.cs" />
<Compile Include="Datastore\Migration\002_add_release_to_pending_releases.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
@ -200,6 +201,7 @@
<Compile Include="DecisionEngine\SpecificationPriority.cs" /> <Compile Include="DecisionEngine\SpecificationPriority.cs" />
<Compile Include="DecisionEngine\Specifications\AcceptableSizeSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\AcceptableSizeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\BlockedIndexerSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\DiscographySpecification.cs" /> <Compile Include="DecisionEngine\Specifications\DiscographySpecification.cs" />
<Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\ProtocolSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\ProtocolSpecification.cs" />
@ -242,6 +244,7 @@
<Compile Include="Download\Clients\Deluge\DelugeUpdateUIResult.cs" /> <Compile Include="Download\Clients\Deluge\DelugeUpdateUIResult.cs" />
<Compile Include="Download\Clients\DownloadClientAuthenticationException.cs" /> <Compile Include="Download\Clients\DownloadClientAuthenticationException.cs" />
<Compile Include="Download\Clients\DownloadClientException.cs" /> <Compile Include="Download\Clients\DownloadClientException.cs" />
<Compile Include="Download\Clients\DownloadClientUnavailableException.cs" />
<Compile Include="Download\Clients\DownloadStation\Proxies\DownloadStationInfoProxy.cs" /> <Compile Include="Download\Clients\DownloadStation\Proxies\DownloadStationInfoProxy.cs" />
<Compile Include="Download\Clients\DownloadStation\TorrentDownloadStation.cs" /> <Compile Include="Download\Clients\DownloadStation\TorrentDownloadStation.cs" />
<Compile Include="Download\Clients\DownloadStation\Proxies\DownloadStationTaskProxy.cs" /> <Compile Include="Download\Clients\DownloadStation\Proxies\DownloadStationTaskProxy.cs" />
@ -373,7 +376,11 @@
<Compile Include="Download\Clients\uTorrent\UTorrentTorrentStatus.cs" /> <Compile Include="Download\Clients\uTorrent\UTorrentTorrentStatus.cs" />
<Compile Include="Download\Clients\Vuze\Vuze.cs" /> <Compile Include="Download\Clients\Vuze\Vuze.cs" />
<Compile Include="Download\CompletedDownloadService.cs" /> <Compile Include="Download\CompletedDownloadService.cs" />
<Compile Include="Download\DownloadClientStatus.cs" />
<Compile Include="Download\DownloadClientStatusRepository.cs" />
<Compile Include="Download\DownloadClientStatusService.cs" />
<Compile Include="Download\DownloadEventHub.cs" /> <Compile Include="Download\DownloadEventHub.cs" />
<Compile Include="Download\Pending\PendingReleaseReason.cs" />
<Compile Include="Download\TrackedDownloads\DownloadMonitoringService.cs" /> <Compile Include="Download\TrackedDownloads\DownloadMonitoringService.cs" />
<Compile Include="Download\TrackedDownloads\TrackedDownload.cs" /> <Compile Include="Download\TrackedDownloads\TrackedDownload.cs" />
<Compile Include="Download\TrackedDownloads\TrackedDownloadService.cs" /> <Compile Include="Download\TrackedDownloads\TrackedDownloadService.cs" />
@ -435,8 +442,10 @@
<Compile Include="Extras\Lyrics\LyricService.cs" /> <Compile Include="Extras\Lyrics\LyricService.cs" />
<Compile Include="Fluent.cs" /> <Compile Include="Fluent.cs" />
<Compile Include="HealthCheck\CheckHealthCommand.cs" /> <Compile Include="HealthCheck\CheckHealthCommand.cs" />
<Compile Include="HealthCheck\CheckOnAttribute.cs" />
<Compile Include="HealthCheck\Checks\AppDataLocationCheck.cs" /> <Compile Include="HealthCheck\Checks\AppDataLocationCheck.cs" />
<Compile Include="HealthCheck\Checks\DownloadClientCheck.cs" /> <Compile Include="HealthCheck\Checks\DownloadClientCheck.cs" />
<Compile Include="HealthCheck\Checks\DownloadClientStatusCheck.cs" />
<Compile Include="HealthCheck\Checks\MountCheck.cs" /> <Compile Include="HealthCheck\Checks\MountCheck.cs" />
<Compile Include="HealthCheck\Checks\ImportMechanismCheck.cs" /> <Compile Include="HealthCheck\Checks\ImportMechanismCheck.cs" />
<Compile Include="HealthCheck\Checks\IndexerRssCheck.cs" /> <Compile Include="HealthCheck\Checks\IndexerRssCheck.cs" />
@ -459,9 +468,11 @@
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupCommandQueue.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupCommandQueue.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAbsolutePathMetadataFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAbsolutePathMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDownloadClientUnavailablePendingReleases.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedAlbums.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedAlbums.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedDownloadClientStatus.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedTrackFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedTrackFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedTracks.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedTracks.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedIndexerStatus.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedIndexerStatus.cs" />
@ -994,6 +1005,7 @@
<Compile Include="Tags\TagsUpdatedEvent.cs" /> <Compile Include="Tags\TagsUpdatedEvent.cs" />
<Compile Include="ThingiProvider\ConfigContractNotFoundException.cs" /> <Compile Include="ThingiProvider\ConfigContractNotFoundException.cs" />
<Compile Include="ThingiProvider\Events\ProviderDeletedEvent.cs" /> <Compile Include="ThingiProvider\Events\ProviderDeletedEvent.cs" />
<Compile Include="ThingiProvider\Events\ProviderStatusChangedEvent.cs" />
<Compile Include="ThingiProvider\Events\ProviderUpdatedEvent.cs" /> <Compile Include="ThingiProvider\Events\ProviderUpdatedEvent.cs" />
<Compile Include="ThingiProvider\IProvider.cs" /> <Compile Include="ThingiProvider\IProvider.cs" />
<Compile Include="ThingiProvider\IProviderConfig.cs" /> <Compile Include="ThingiProvider\IProviderConfig.cs" />
@ -1004,6 +1016,9 @@
<Compile Include="ThingiProvider\ProviderFactory.cs" /> <Compile Include="ThingiProvider\ProviderFactory.cs" />
<Compile Include="ThingiProvider\ProviderMessage.cs" /> <Compile Include="ThingiProvider\ProviderMessage.cs" />
<Compile Include="ThingiProvider\ProviderRepository.cs" /> <Compile Include="ThingiProvider\ProviderRepository.cs" />
<Compile Include="ThingiProvider\Status\ProviderStatusBase.cs" />
<Compile Include="ThingiProvider\Status\ProviderStatusRepository.cs" />
<Compile Include="ThingiProvider\Status\ProviderStatusServiceBase.cs" />
<Compile Include="TinyTwitter.cs" /> <Compile Include="TinyTwitter.cs" />
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" /> <Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
<Compile Include="Update\InstallUpdateService.cs" /> <Compile Include="Update\InstallUpdateService.cs" />

View file

@ -0,0 +1,18 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.ThingiProvider.Events
{
public class ProviderStatusChangedEvent<TProvider> : IEvent
{
public int ProviderId { get; private set; }
public ProviderStatusBase Status { get; private set; }
public ProviderStatusChangedEvent(int id, ProviderStatusBase status)
{
ProviderId = id;
Status = status;
}
}
}

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View file

@ -1,8 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider.Events; using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.ThingiProvider.Status namespace NzbDrone.Core.ThingiProvider.Status
@ -20,13 +21,25 @@ namespace NzbDrone.Core.ThingiProvider.Status
where TProvider : IProvider where TProvider : IProvider
where TModel : ProviderStatusBase, new() where TModel : ProviderStatusBase, new()
{ {
private static readonly int[] EscalationBackOffPeriods = {
0,
5 * 60,
15 * 60,
30 * 60,
60 * 60,
3 * 60 * 60,
6 * 60 * 60,
12 * 60 * 60,
24 * 60 * 60
};
protected readonly object _syncRoot = new object(); protected readonly object _syncRoot = new object();
protected readonly IProviderStatusRepository<TModel> _providerStatusRepository; protected readonly IProviderStatusRepository<TModel> _providerStatusRepository;
protected readonly IEventAggregator _eventAggregator; protected readonly IEventAggregator _eventAggregator;
protected readonly Logger _logger; protected readonly Logger _logger;
protected int MaximumEscalationLevel { get; set; } = EscalationBackOff.Periods.Length - 1; protected int MaximumEscalationLevel { get; set; } = EscalationBackOffPeriods.Length - 1;
protected TimeSpan MinimumTimeSinceInitialFailure { get; set; } = TimeSpan.Zero; protected TimeSpan MinimumTimeSinceInitialFailure { get; set; } = TimeSpan.Zero;
public ProviderStatusServiceBase(IProviderStatusRepository<TModel> providerStatusRepository, IEventAggregator eventAggregator, Logger logger) public ProviderStatusServiceBase(IProviderStatusRepository<TModel> providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
@ -50,7 +63,7 @@ namespace NzbDrone.Core.ThingiProvider.Status
{ {
var level = Math.Min(MaximumEscalationLevel, status.EscalationLevel); var level = Math.Min(MaximumEscalationLevel, status.EscalationLevel);
return TimeSpan.FromSeconds(EscalationBackOff.Periods[level]); return TimeSpan.FromSeconds(EscalationBackOffPeriods[level]);
} }
public virtual void RecordSuccess(int providerId) public virtual void RecordSuccess(int providerId)