1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-06 13:30:00 -05:00

Compare commits

..

18 Commits

Author SHA1 Message Date
Taloth Saldono
63853494f8 Added active detection for updatecheck so we know which os/runtime versions don't need to be supported anymore. 2020-03-13 14:53:35 +01:00
Taloth Saldono
6593575850 Fixed: Don't auto-search newly added episodes on tvdb that aired more than 2 weeks ago
Fixed: Don't monitor newly added old episodes on tvdb if series was previously empty
2020-03-13 13:55:48 +01:00
Taloth Saldono
bbf74a8835 Fixed: Workaround for mono 5.16+ bug preventing the closure of sockets on timeouts (Jackett connections)
ref #2802
2019-07-02 21:00:02 +02:00
Taloth Saldono
1fc2866032 Fixed: Include all download items if no category is specified in rtorrent.
closes #3002
2019-03-24 15:27:41 +01:00
Taloth Saldono
eb2e7b9c79 Continue Test in case of validation warnings. 2019-03-24 15:22:50 +01:00
Taloth Saldono
cab900f656 Don't skip magnet links with included trackers if dht is disabled. 2019-03-24 14:58:13 +01:00
Taloth Saldono
e2b91e5dc4 Fixed: Detecting if qbittorrent seeding time limit has been reached 2019-03-23 22:58:43 +01:00
Taloth Saldono
e52fcf843c Handle Deluge v2 beta breaking change in their api.
closes #2412
2019-03-03 23:10:28 +01:00
Taloth Saldono
08ba273089 fixed qbittorrent tests failing due to incorrect test setup. And http tests failed due to httpbin changing their output. 2019-03-03 21:19:25 +01:00
Taloth Saldono
faa2d632e5 New: Indexer Seed Limit settings applied to new downloads for qBittorrent
closes #2607
2019-03-03 20:25:31 +01:00
Taloth Saldono
1b939ebf4b Fixed: Magnet Link progress visualisation and adding magnet links if dht is disabled in qBittorrent 2019-03-03 19:29:25 +01:00
Mark Bebbington
aa46216117 Fixed: qBittorrent api v2 support (qbit v4.1+)
fixes #2887
closes #2951
ref #2945
2019-03-03 19:26:50 +01:00
Mark McDowall
c3c6b3d166 Fixed: Importing completed downloads from NZBGet with post processing script failing
Fixes #2919

# Conflicts:
#	src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs
2019-02-06 19:36:37 -08:00
Mark McDowall
2c95f07cb2 Another path test fix 2019-02-01 10:58:02 -08:00
Mark McDowall
4a2277b424 Fix path tests 2019-02-01 10:57:49 -08:00
Mark McDowall
a1f02916d4 Fixed: Importing of completed download when not a child of the download client output path 2019-02-01 10:57:39 -08:00
Mark McDowall
900dfd92d0 Fixed: Getting parent of UNC paths 2019-02-01 10:56:48 -08:00
Mark McDowall
d6997b0588 Fixed getting parent path from a path without another slash
Fixed: Manual Import failing for some paths
2019-02-01 10:55:28 -08:00
32 changed files with 1242 additions and 195 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -130,11 +130,12 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_execute_typed_get()
{
var request = new HttpRequest($"http://{_httpBinHost}/get");
var request = new HttpRequest($"http://{_httpBinHost}/get?test=1");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Url.Should().Be(request.Url.FullUri);
response.Resource.Url.EndsWith("/get?test=1");
response.Resource.Args.Should().Contain("test", "1");
}
[Test]
@@ -706,6 +707,7 @@ namespace NzbDrone.Common.Test.Http
public class HttpBinResource
{
public Dictionary<string, object> Args { get; set; }
public Dictionary<string, object> Headers { get; set; }
public string Origin { get; set; }
public string Url { get; set; }

View File

@@ -138,18 +138,34 @@ namespace NzbDrone.Common.Test
}
[TestCase(@"C:\Test\mydir", @"C:\Test")]
[TestCase(@"C:\Test\", @"C:")]
[TestCase(@"C:\Test\", @"C:\")]
[TestCase(@"C:\", null)]
public void path_should_return_parent(string path, string parentPath)
[TestCase(@"\\server\share", null)]
[TestCase(@"\\server\share\test", @"\\server\share")]
public void path_should_return_parent_windows(string path, string parentPath)
{
WindowsOnly();
path.GetParentPath().Should().Be(parentPath);
}
[TestCase(@"/", null)]
[TestCase(@"/test", "/")]
public void path_should_return_parent_mono(string path, string parentPath)
{
MonoOnly();
path.GetParentPath().Should().Be(parentPath);
}
[Test]
public void path_should_return_parent_for_oversized_path()
{
var path = @"/media/2e168617-f2ae-43fb-b88c-3663af1c8eea/downloads/sabnzbd/nzbdrone/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories";
var parentPath = @"/media/2e168617-f2ae-43fb-b88c-3663af1c8eea/downloads/sabnzbd/nzbdrone/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing/With.Alot.Of.Nested.Directories/Some.Real.Big.Thing";
MonoOnly();
// This test will fail on Windows if long path support is not enabled: https://www.howtogeek.com/266621/how-to-make-windows-10-accept-file-paths-over-260-characters/
// It will also fail if the app isn't configured to use long path (such as resharper): https://blogs.msdn.microsoft.com/jeremykuhne/2016/07/30/net-4-6-2-and-long-paths-on-windows-10/
var path = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories".AsOsAgnostic();
var parentPath = @"C:\media\2e168617-f2ae-43fb-b88c-3663af1c8eea\downloads\sabnzbd\nzbdrone\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing\With.Alot.Of.Nested.Directories\Some.Real.Big.Thing".AsOsAgnostic();
path.GetParentPath().Should().Be(parentPath);
}

View File

@@ -24,6 +24,8 @@ namespace NzbDrone.Common.Extensions
private static readonly string UPDATE_CLIENT_FOLDER_NAME = "NzbDrone.Update" + Path.DirectorySeparatorChar;
private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar;
private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(?<!:)\\$", RegexOptions.Compiled);
public static string CleanFilePath(this string path)
{
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
@@ -67,15 +69,16 @@ namespace NzbDrone.Common.Extensions
public static string GetParentPath(this string childPath)
{
var parentPath = childPath.TrimEnd('\\', '/');
var cleanPath = OsInfo.IsWindows
? PARENT_PATH_END_SLASH_REGEX.Replace(childPath, "")
: childPath.TrimEnd(Path.DirectorySeparatorChar);
var index = parentPath.LastIndexOfAny(new[] { '\\', '/' });
if (index != -1)
if (cleanPath.IsNullOrWhiteSpace())
{
return parentPath.Substring(0, index);
return null;
}
return null;
return Directory.GetParent(cleanPath)?.FullName;
}
public static bool IsParentPath(this string parentPath, string childPath)

View File

@@ -2,9 +2,13 @@ using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Reflection;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Security;
namespace NzbDrone.Common.Http.Dispatchers
@@ -14,12 +18,16 @@ namespace NzbDrone.Common.Http.Dispatchers
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder)
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
_userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger;
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
@@ -39,7 +47,7 @@ namespace NzbDrone.Common.Http.Dispatchers
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
}
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
@@ -84,6 +92,9 @@ namespace NzbDrone.Common.Http.Dispatchers
if (httpWebResponse == null)
{
// Workaround for mono not closing connections properly in certain situations.
AbortWebRequest(webRequest);
// The default messages for WebException on mono are pretty horrible.
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{
@@ -198,5 +209,36 @@ namespace NzbDrone.Common.Http.Dispatchers
}
}
}
// Workaround for mono not closing connections properly on timeouts
private void AbortWebRequest(HttpWebRequest webRequest)
{
// First affected version was mono 5.16
if (OsInfo.IsNotWindows && _platformInfo.Version >= new Version(5, 16))
{
try
{
var currentOperationInfo = webRequest.GetType().GetField("currentOperation", BindingFlags.NonPublic | BindingFlags.Instance);
var currentOperation = currentOperationInfo.GetValue(webRequest);
if (currentOperation != null)
{
var responseStreamInfo = currentOperation.GetType().GetField("responseStream", BindingFlags.NonPublic | BindingFlags.Instance);
var responseStream = responseStreamInfo.GetValue(currentOperation) as Stream;
// Note that responseStream will likely be null once mono fixes it.
responseStream?.Dispose();
}
}
catch (Exception ex)
{
// This can fail randomly on future mono versions that have been changed/fixed. Log to sentry and ignore.
_logger.Trace()
.Exception(ex)
.Message("Unable to dispose responseStream on mono {0}", _platformInfo.Version)
.WriteSentryWarn("MonoCloseWaitPatchFailed", ex.Message)
.Write();
}
}
}
}
}

View File

@@ -355,7 +355,7 @@ namespace NzbDrone.Common.Http
FormData.Add(new HttpFormData
{
Name = key,
ContentData = Encoding.UTF8.GetBytes(value.ToString())
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
});
return this;

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
@@ -20,13 +21,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new QBittorrentSettings
{
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass",
TvCategory = "tv"
};
{
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass",
TvCategory = "tv"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
@@ -37,8 +38,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences());
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences() { DhtEnabled = true });
Mocker.GetMock<IQBittorrentProxySelector>()
.Setup(s => s.GetProxy(It.IsAny<QBittorrentSettings>(), It.IsAny<bool>()))
.Returns(Mocker.GetMock<IQBittorrentProxy>().Object);
}
protected void GivenRedirectToMagnet()
@@ -95,15 +100,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First;
}
protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true)
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, bool removeOnMaxRatio = false)
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences
{
RemoveOnMaxRatio = removeOnMaxRatio,
MaxRatio = maxRatio
});
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences
{
RemoveOnMaxRatio = removeOnMaxRatio,
MaxRatio = maxRatio,
MaxRatioEnabled = maxRatio >= 0,
MaxSeedingTime = maxSeedingTime,
MaxSeedingTimeEnabled = maxSeedingTime >= 0
});
}
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
@@ -154,7 +162,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
VerifyPaused(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
item.RemainingTime.Should().NotHaveValue();
}
[TestCase("pausedUP")]
@@ -185,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("queuedDL")]
[TestCase("checkingDL")]
[TestCase("metaDL")]
public void queued_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
@@ -202,7 +211,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
VerifyQueued(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
item.RemainingTime.Should().NotHaveValue();
}
[Test]
@@ -244,7 +253,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
VerifyWarning(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
item.RemainingTime.Should().NotHaveValue();
}
[Test]
@@ -272,6 +281,35 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
id.Should().Be(expectedHash);
}
[Test]
public void Download_should_refuse_magnet_if_no_trackers_provided_and_dht_is_disabled()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences() { DhtEnabled = false });
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR";
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteEpisode));
}
[Test]
public void Download_should_accept_magnet_if_trackers_provided_and_dht_is_disabled()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences() { DhtEnabled = false });
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp://abc";
Assert.DoesNotThrow(() => Subject.Download(remoteEpisode));
Mocker.GetMock<IQBittorrentProxy>()
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]
public void Download_should_set_top_priority()
{
@@ -353,7 +391,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached()
{
GivenMaxRatio(1.0f);
GivenGlobalSeedLimits(1.0f);
var torrent = new QBittorrentTorrent
{
@@ -374,11 +412,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused()
protected virtual QBittorrentTorrent GivenCompletedTorrent(
string state = "pausedUP",
float ratio = 0.1f, float ratioLimit = -2,
int seedingTime = 1, int seedingTimeLimit = -2)
{
GivenMaxRatio(1.0f);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
@@ -386,12 +424,32 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "uploading",
State = state,
Label = "",
SavePath = "",
Ratio = 1.0f
Ratio = ratio,
RatioLimit = ratioLimit,
SeedingTimeLimit = seedingTimeLimit
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
GivenTorrents(new List<QBittorrentTorrent>() { torrent });
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetTorrentProperties("HASH", It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentTorrentProperties
{
Hash = "HASH",
SeedingTime = seedingTime
});
return torrent;
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused()
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent("uploading", ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -401,21 +459,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
{
GivenMaxRatio(1.0f, false);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "uploading",
Label = "",
SavePath = "",
Ratio = 1.0f
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
GivenGlobalSeedLimits(-1);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -425,21 +470,86 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
{
GivenMaxRatio(1.0f);
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
Label = "",
SavePath = "",
Ratio = 1.0f
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
{
GivenGlobalSeedLimits(0.2f);
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_seedingtime_reached_and_not_paused()
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("uploading", ratio: 2.0f, seedingTime: 30);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
{
GivenGlobalSeedLimits(-1, 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
{
GivenGlobalSeedLimits(2.0f, 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
@@ -450,7 +560,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
public void should_get_category_from_the_category_if_set()
{
const string category = "tv-sonarr";
GivenMaxRatio(1.0f);
GivenGlobalSeedLimits(1.0f);
var torrent = new QBittorrentTorrent
{
@@ -475,7 +585,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
public void should_get_category_from_the_label_if_the_category_is_not_available()
{
const string category = "tv-sonarr";
GivenMaxRatio(1.0f);
GivenGlobalSeedLimits(1.0f);
var torrent = new QBittorrentTorrent
{
@@ -508,5 +618,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
torrent.Eta.ToString().Should().Be("18446744073709335000");
}
[Test]
public void Test_should_force_api_version_check()
{
// Set TestConnection up to fail quick
Mocker.GetMock<IQBittorrentProxy>()
.Setup(v => v.GetApiVersion(It.IsAny<QBittorrentSettings>()))
.Returns(new Version(1, 0));
Subject.Test();
Mocker.GetMock<IQBittorrentProxySelector>()
.Verify(v => v.GetProxy(It.IsAny<QBittorrentSettings>(), true), Times.Once());
}
}
}

View File

@@ -23,8 +23,8 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>()));
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<NLog.Logger>()));
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger));
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());

View File

@@ -317,5 +317,89 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic())));
}
[Test]
public void should_get_relative_path_when_there_is_no_grandparent_windows()
{
WindowsOnly();
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
var outputPath = @"C:\";
var localEpisode = _approvedDecisions.First().LocalEpisode;
localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name };
localEpisode.Path = Path.Combine(outputPath, name + ".mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, null);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}.mkv".AsOsAgnostic())));
}
[Test]
public void should_get_relative_path_when_there_is_no_grandparent_mono()
{
MonoOnly();
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
var outputPath = "/";
var localEpisode = _approvedDecisions.First().LocalEpisode;
localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name };
localEpisode.Path = Path.Combine(outputPath, name + ".mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, null);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}.mkv".AsOsAgnostic())));
}
[Test]
public void should_get_relative_path_when_there_is_no_grandparent_for_UNC_path()
{
WindowsOnly();
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
var outputPath = @"\\server\share";
var localEpisode = _approvedDecisions.First().LocalEpisode;
localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name };
localEpisode.Path = Path.Combine(outputPath, name + ".mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, null);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}.mkv")));
}
[Test]
public void should_use_folder_info_release_title_to_find_relative_path_when_file_is_not_in_download_client_item_output_directory()
{
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name);
var localEpisode = _approvedDecisions.First().LocalEpisode;
_downloadClientItem.OutputPath = new OsPath(Path.Combine(@"C:\Test\Unsorted\TV-Other\".AsOsAgnostic(), name));
localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name };
localEpisode.Path = Path.Combine(outputPath, "subfolder", name + ".mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic())));
}
[Test]
public void should_use_folder_info_release_title_to_find_relative_path_when_download_client_item_has_an_empty_output_path()
{
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name);
var localEpisode = _approvedDecisions.First().LocalEpisode;
_downloadClientItem.OutputPath = new OsPath();
localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name };
localEpisode.Path = Path.Combine(outputPath, "subfolder", name + ".mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic())));
}
}
}

View File

@@ -9,6 +9,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.TvTests
{
@@ -77,12 +78,14 @@ namespace NzbDrone.Core.Test.TvTests
{
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
.Returns(new List<Episode>());
Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes());
_insertedEpisodes.Should().HaveSameCount(GetEpisodes());
_updatedEpisodes.Should().BeEmpty();
_deletedEpisodes.Should().BeEmpty();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
@@ -146,6 +149,63 @@ namespace NzbDrone.Core.Test.TvTests
_updatedEpisodes.Should().OnlyContain(e => e.Monitored == true);
}
[Test]
public void should_not_set_monitored_status_for_old_episodes_to_false_if_recent_enough()
{
var series = GetSeries();
series.Seasons = new List<Season>();
series.Seasons.Add(new Season { SeasonNumber = 1, Monitored = true });
var episodes = GetEpisodes().OrderBy(v => v.SeasonNumber).ThenBy(v => v.EpisodeNumber).Take(5).ToList();
episodes[1].AirDateUtc = DateTime.UtcNow.AddDays(-15);
episodes[2].AirDateUtc = DateTime.UtcNow.AddDays(-10);
episodes[3].AirDateUtc = DateTime.UtcNow.AddDays(1);
var existingEpisodes = episodes.Skip(4).ToList();
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
.Returns(existingEpisodes);
Subject.RefreshEpisodeInfo(series, episodes);
_insertedEpisodes = _insertedEpisodes.OrderBy(v => v.EpisodeNumber).ToList();
_insertedEpisodes.Should().HaveCount(4);
_insertedEpisodes[0].Monitored.Should().Be(true);
_insertedEpisodes[1].Monitored.Should().Be(true);
_insertedEpisodes[2].Monitored.Should().Be(true);
_insertedEpisodes[3].Monitored.Should().Be(true);
}
[Test]
public void should_set_monitored_status_for_old_episodes_to_false_if_no_episodes_existed()
{
var series = GetSeries();
series.Seasons = new List<Season>();
var episodes = GetEpisodes().OrderBy(v => v.SeasonNumber).ThenBy(v => v.EpisodeNumber).Take(4).ToList();
episodes[1].AirDateUtc = DateTime.UtcNow.AddDays(-15);
episodes[2].AirDateUtc = DateTime.UtcNow.AddDays(-10);
episodes[3].AirDateUtc = DateTime.UtcNow.AddDays(1);
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
.Returns(new List<Episode>());
Subject.RefreshEpisodeInfo(series, episodes);
_insertedEpisodes = _insertedEpisodes.OrderBy(v => v.EpisodeNumber).ToList();
_insertedEpisodes.Should().HaveSameCount(episodes);
_insertedEpisodes[0].Monitored.Should().Be(false);
_insertedEpisodes[1].Monitored.Should().Be(false);
_insertedEpisodes[2].Monitored.Should().Be(false);
_insertedEpisodes[3].Monitored.Should().Be(true);
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_remove_duplicate_remote_episodes_before_processing()
{

View File

@@ -1,22 +1,40 @@
using NzbDrone.Common.EnvironmentInfo;
using System;
using System.Linq;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.History;
namespace NzbDrone.Core.Analytics
{
public interface IAnalyticsService
{
bool IsEnabled { get; }
bool InstallIsActive { get; }
}
public class AnalyticsService : IAnalyticsService
{
private readonly IConfigFileProvider _configFileProvider;
private readonly IHistoryService _historyService;
public AnalyticsService(IConfigFileProvider configFileProvider)
public AnalyticsService(IHistoryService historyService, IConfigFileProvider configFileProvider)
{
_configFileProvider = configFileProvider;
_historyService = historyService;
}
public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfo.IsProduction;
public bool InstallIsActive
{
get
{
var lastRecord = _historyService.Paged(new PagingSpec<History.History>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
var monthAgo = DateTime.UtcNow.AddMonths(-1);
return lastRecord.Records.Any(v => v.Date > monthAgo);
}
}
}
}
}

View File

@@ -198,7 +198,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestCategory());
failures.AddIfNotNull(TestGetTorrents());
}

View File

@@ -48,9 +48,25 @@ namespace NzbDrone.Core.Download.Clients.Deluge
public string GetVersion(DelugeSettings settings)
{
var response = ProcessRequest<string>(settings, "daemon.info");
try
{
var response = ProcessRequest<string>(settings, "daemon.info");
return response;
return response;
}
catch (DownloadClientException ex)
{
if (ex.Message.Contains("Unknown method"))
{
// Deluge v2 beta replaced 'daemon.info' with 'daemon.get_version'.
// It may return or become official, for now we just retry with the get_version api.
var response = ProcessRequest<string>(settings, "daemon.get_version");
return response;
}
throw;
}
}
public Dictionary<string, object> GetConfig(DelugeSettings settings)

View File

@@ -194,7 +194,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestOutputPath());
failures.AddIfNotNull(TestGetTorrents());
}

View File

@@ -189,7 +189,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestOutputPath());
failures.AddIfNotNull(TestGetNZB());
}

View File

@@ -130,7 +130,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestGetTorrents());
}

View File

@@ -17,9 +17,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
{
private readonly IQBittorrentProxy _proxy;
private readonly IQBittorrentProxySelector _proxySelector;
public QBittorrent(IQBittorrentProxy proxy,
public QBittorrent(IQBittorrentProxySelector proxySelector,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
@@ -28,16 +28,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
_proxySelector = proxySelector;
}
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr="))
{
throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled");
}
Proxy.AddTorrentFromUrl(magnetLink, Settings);
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
}
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
@@ -45,23 +52,28 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
SetInitialState(hash.ToLower());
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
}
return hash;
}
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
try
{
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
}
}
catch (Exception ex)
@@ -76,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
}
catch (Exception ex)
@@ -86,6 +98,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
SetInitialState(hash.ToLower());
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
{
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
}
return hash;
}
@@ -93,28 +110,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public override IEnumerable<DownloadClientItem> GetItems()
{
var config = _proxy.GetConfig(Settings);
var torrents = _proxy.GetTorrents(Settings);
var config = Proxy.GetConfig(Settings);
var torrents = Proxy.GetTorrents(Settings);
var queueItems = new List<DownloadClientItem>();
foreach (var torrent in torrents)
{
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper();
item.Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label;
item.Title = torrent.Name;
item.TotalSize = torrent.Size;
item.DownloadClient = Definition.Name;
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
item.RemainingTime = GetRemainingTime(torrent);
item.SeedRatio = torrent.Ratio;
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
var item = new DownloadClientItem()
{
DownloadId = torrent.Hash.ToUpper(),
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
Title = torrent.Name,
TotalSize = torrent.Size,
DownloadClient = Definition.Name,
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
RemainingTime = GetRemainingTime(torrent),
SeedRatio = torrent.Ratio,
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)),
};
// Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.CanMoveFiles = item.CanBeRemoved = (!config.MaxRatioEnabled || config.MaxRatio <= torrent.Ratio) && torrent.State == "pausedUP";
item.CanMoveFiles = item.CanBeRemoved = (torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config));
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
{
@@ -152,6 +170,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = "The download is stalled with no connections";
break;
case "metaDL": // torrent magnet is being downloaded
if (config.DhtEnabled)
{
item.Status = DownloadItemStatus.Queued;
}
else
{
item.Status = DownloadItemStatus.Warning;
item.Message = "qBittorrent cannot resolve magnet link with DHT disabled";
}
break;
case "downloading": // torrent is being downloaded and data is being transfered
default: // new status in API? default to downloading
item.Status = DownloadItemStatus.Downloading;
@@ -166,12 +196,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public override void RemoveItem(string hash, bool deleteData)
{
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
Proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
}
public override DownloadClientInfo GetStatus()
{
var config = _proxy.GetConfig(Settings);
var config = Proxy.GetConfig(Settings);
var destDir = new OsPath(config.SavePath);
@@ -185,7 +215,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestPrioritySupport());
failures.AddIfNotNull(TestGetTorrents());
}
@@ -194,8 +224,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
try
{
var version = _proxy.GetVersion(Settings);
if (version < 5)
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
if (version < Version.Parse("1.5"))
{
// API version 5 introduced the "save_path" property in /query/torrents
return new NzbDroneValidationFailure("Host", "Unsupported client version")
@@ -203,7 +233,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
};
}
else if (version < 6)
else if (version < Version.Parse("1.6"))
{
// API version 6 introduced support for labels
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
@@ -225,8 +255,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
// Complain if qBittorrent is configured to remove torrents on max ratio
var config = _proxy.GetConfig(Settings);
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
var config = Proxy.GetConfig(Settings);
if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && config.RemoveOnMaxRatio)
{
return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
{
@@ -275,7 +305,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
try
{
var config = _proxy.GetConfig(Settings);
var config = Proxy.GetConfig(Settings);
if (!config.QueueingEnabled)
{
@@ -302,7 +332,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
try
{
_proxy.GetTorrents(Settings);
Proxy.GetTorrents(Settings);
}
catch (Exception ex)
{
@@ -320,13 +350,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
switch ((QBittorrentState)Settings.InitialState)
{
case QBittorrentState.ForceStart:
_proxy.SetForceStart(hash, true, Settings);
Proxy.SetForceStart(hash, true, Settings);
break;
case QBittorrentState.Start:
_proxy.ResumeTorrent(hash, Settings);
Proxy.ResumeTorrent(hash, Settings);
break;
case QBittorrentState.Pause:
_proxy.PauseTorrent(hash, Settings);
Proxy.PauseTorrent(hash, Settings);
break;
}
}
@@ -343,7 +373,53 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return null;
}
// qBittorrent sends eta=8640000 if unknown such as queued
if (torrent.Eta == 8640000)
{
return null;
}
return TimeSpan.FromSeconds((int)torrent.Eta);
}
protected bool HasReachedSeedLimit(QBittorrentTorrent torrent, QBittorrentPreferences config)
{
if (torrent.RatioLimit >= 0)
{
if (torrent.Ratio >= torrent.RatioLimit) return true;
}
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
{
if (torrent.Ratio >= config.MaxRatio) return true;
}
if (torrent.SeedingTimeLimit >= 0)
{
if (!torrent.SeedingTime.HasValue)
{
FetchTorrentDetails(torrent);
}
if (torrent.SeedingTime >= torrent.SeedingTimeLimit) return true;
}
else if (torrent.SeedingTimeLimit == -2 && config.MaxSeedingTimeEnabled)
{
if (!torrent.SeedingTime.HasValue)
{
FetchTorrentDetails(torrent);
}
if (torrent.SeedingTime >= config.MaxSeedingTime) return true;
}
return false;
}
protected void FetchTorrentDetails(QBittorrentTorrent torrent)
{
var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings);
torrent.SeedingTime = torrentProperties.SeedingTime;
}
}
}

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
@@ -14,10 +14,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[JsonProperty(PropertyName = "max_ratio")]
public float MaxRatio { get; set; } // Get the global share ratio limit
[JsonProperty(PropertyName = "max_seeding_time_enabled")]
public bool MaxSeedingTimeEnabled { get; set; } // True if share time limit is enabled
[JsonProperty(PropertyName = "max_seeding_time")]
public long MaxSeedingTime { get; set; } // Get the global share time limit in minutes
[JsonProperty(PropertyName = "max_ratio_act")]
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
[JsonProperty(PropertyName = "queueing_enabled")]
public bool QueueingEnabled { get; set; } = true;
[JsonProperty(PropertyName = "dht")]
public bool DhtEnabled { get; set; } // DHT enabled (needed for more peers and magnet downloads)
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public interface IQBittorrentProxy
{
bool IsApiSupported(QBittorrentSettings settings);
Version GetApiVersion(QBittorrentSettings settings);
string GetVersion(QBittorrentSettings settings);
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void PauseTorrent(string hash, QBittorrentSettings settings);
void ResumeTorrent(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
}
public interface IQBittorrentProxySelector
{
IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false);
}
public class QBittorrentProxySelector : IQBittorrentProxySelector
{
private readonly IHttpClient _httpClient;
private readonly ICached<IQBittorrentProxy> _proxyCache;
private readonly Logger _logger;
private readonly IQBittorrentProxy _proxyV1;
private readonly IQBittorrentProxy _proxyV2;
public QBittorrentProxySelector(QBittorrentProxyV1 proxyV1,
QBittorrentProxyV2 proxyV2,
IHttpClient httpClient,
ICacheManager cacheManager,
Logger logger)
{
_httpClient = httpClient;
_proxyCache = cacheManager.GetCache<IQBittorrentProxy>(GetType());
_logger = logger;
_proxyV1 = proxyV1;
_proxyV2 = proxyV2;
}
public IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force)
{
var proxyKey = $"{settings.Host}_{settings.Port}";
if (force)
{
_proxyCache.Remove(proxyKey);
}
return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0));
}
private IQBittorrentProxy FetchProxy(QBittorrentSettings settings)
{
if (_proxyV2.IsApiSupported(settings))
{
_logger.Trace("Using qbitTorrent API v2");
return _proxyV2;
}
if (_proxyV1.IsApiSupported(settings))
{
_logger.Trace("Using qbitTorrent API v1");
return _proxyV1;
}
throw new DownloadClientException("Unable to determine qBittorrent API version");
}
}
}

View File

@@ -11,41 +11,68 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
public interface IQBittorrentProxy
{
int GetVersion(QBittorrentSettings settings);
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void PauseTorrent(string hash, QBittorrentSettings settings);
void ResumeTorrent(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
}
public class QBittorrentProxy : IQBittorrentProxy
public class QBittorrentProxyV1 : IQBittorrentProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private readonly ICached<Dictionary<string, string>> _authCookieCache;
public QBittorrentProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
public QBittorrentProxyV1(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
}
public int GetVersion(QBittorrentSettings settings)
public bool IsApiSupported(QBittorrentSettings settings)
{
// We can do the api test without having to authenticate since v4.1 will return 404 on the request.
var request = BuildRequest(settings).Resource("/version/api");
var response = ProcessRequest<int>(request, settings);
request.SuppressHttpError = true;
try
{
var response = _httpClient.Execute(request.Build());
// Version request will return 404 if it doesn't exist.
if (response.StatusCode == HttpStatusCode.NotFound)
{
return false;
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
return true;
}
if (response.HasHttpError)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
}
return true;
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
public Version GetApiVersion(QBittorrentSettings settings)
{
// Version request does not require authentication and will return 404 if it doesn't exist.
var request = BuildRequest(settings).Resource("/version/api");
var response = Version.Parse("1." + ProcessRequest(request, settings));
return response;
}
public string GetVersion(QBittorrentSettings settings)
{
// Version request does not require authentication.
var request = BuildRequest(settings).Resource("/version/qbittorrent");
var response = ProcessRequest(request, settings).TrimStart('v');
return response;
}
@@ -60,15 +87,25 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/query/torrents")
.AddQueryParam("label", settings.TvCategory)
.AddQueryParam("category", settings.TvCategory);
var request = BuildRequest(settings).Resource("/query/torrents");
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddQueryParam("label", settings.TvCategory);
request.AddQueryParam("category", settings.TvCategory);
}
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
return response;
}
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
var response = ProcessRequest<QBittorrentTorrentProperties>(request, settings);
return response;
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/download")
@@ -107,7 +144,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
request.AddFormParameter("paused", "true");
}
var result = ProcessRequest(request, settings);
@@ -122,8 +159,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
.Post()
.AddFormParameter("hashes", hash);
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
@@ -138,7 +175,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
ProcessRequest(setCategoryRequest, settings);
}
catch(DownloadClientException ex)
catch (DownloadClientException ex)
{
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
@@ -153,12 +190,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
// Not supported on api v1
}
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/topPrio")
.Post()
.AddFormParameter("hashes", hash);
.Post()
.AddFormParameter("hashes", hash);
try
{
ProcessRequest(request, settings);
@@ -166,7 +207,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden?
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
{
return;
@@ -180,9 +220,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/pause")
.Post()
.AddFormParameter("hash", hash);
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
@@ -191,7 +230,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = BuildRequest(settings).Resource("/command/resume")
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
@@ -200,17 +238,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = BuildRequest(settings).Resource("/command/setForceStart")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("value", enabled ? "true": "false");
.AddFormParameter("value", enabled ? "true" : "false");
ProcessRequest(request, settings);
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}
@@ -274,11 +312,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
_authCookieCache.Remove(authKey);
var authLoginRequest = BuildRequest(settings).Resource("/login")
.Post()
.AddFormParameter("username", settings.Username ?? string.Empty)
.AddFormParameter("password", settings.Password ?? string.Empty)
.Build();
var authLoginRequest = BuildRequest(settings).Resource( "/login")
.Post()
.AddFormParameter("username", settings.Username ?? string.Empty)
.AddFormParameter("password", settings.Password ?? string.Empty)
.Build();
HttpResponse response;
try

View File

@@ -0,0 +1,371 @@
using System;
using System.Collections.Generic;
using System.Net;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// API https://github.com/qbittorrent/qBittorrent/wiki/Web-API-Documentation
public class QBittorrentProxyV2 : IQBittorrentProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private readonly ICached<Dictionary<string, string>> _authCookieCache;
public QBittorrentProxyV2(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
}
public bool IsApiSupported(QBittorrentSettings settings)
{
// We can do the api test without having to authenticate since v3.2.0-v4.0.4 will return 404 on the request.
var request = BuildRequest(settings).Resource("/api/v2/app/webapiVersion");
request.SuppressHttpError = true;
try
{
var response = _httpClient.Execute(request.Build());
// Version request will return 404 if it doesn't exist.
if (response.StatusCode == HttpStatusCode.NotFound)
{
return false;
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
return true;
}
if (response.HasHttpError)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
}
return true;
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
public Version GetApiVersion(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/app/webapiVersion");
var response = Version.Parse(ProcessRequest(request, settings));
return response;
}
public string GetVersion(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/app/version");
var response = ProcessRequest(request, settings).TrimStart('v');
// eg "4.2alpha"
return response;
}
public QBittorrentPreferences GetConfig(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/app/preferences");
var response = ProcessRequest<QBittorrentPreferences>(request, settings);
return response;
}
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/info");
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddQueryParam("category", settings.TvCategory);
}
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
return response;
}
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/properties")
.AddQueryParam("hash", hash);
var response = ProcessRequest<QBittorrentTorrentProperties>(request, settings);
return response;
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
.AddFormParameter("urls", torrentUrl);
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
var result = ProcessRequest(request, settings);
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here.
if (result == "Fails.")
{
throw new DownloadClientException("Download client failed to add torrent by url");
}
}
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
.AddFormUpload("torrents", fileName, fileContent);
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", "true");
}
var result = ProcessRequest(request, settings);
// Note: Current qbit versions return nothing, so we can't do != "Ok." here.
if (result == "Fails.")
{
throw new DownloadClientException("Download client failed to add torrent");
}
}
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/delete")
.Post()
.AddFormParameter("hashes", hash);
if (removeData)
{
request.AddFormParameter("deleteFiles", "true");
}
ProcessRequest(request, settings);
}
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setCategory")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("category", label);
ProcessRequest(request, settings);
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
{
var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2;
var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2;
var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("ratioLimit", ratioLimit)
.AddFormParameter("seedingTimeLimit", seedingTimeLimit);
try
{
ProcessRequest(request, settings);
}
catch (DownloadClientException ex)
{
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
{
return;
}
throw;
}
}
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/topPrio")
.Post()
.AddFormParameter("hashes", hash);
try
{
ProcessRequest(request, settings);
}
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
{
return;
}
throw;
}
}
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void ResumeTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("value", enabled ? "true" : "false");
ProcessRequest(request, settings);
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
where TResult : new()
{
var responseContent = ProcessRequest(requestBuilder, settings);
return Json.Deserialize<TResult>(responseContent);
}
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
{
AuthenticateClient(requestBuilder, settings);
var request = requestBuilder.Build();
request.LogResponseContent = true;
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Debug("Authentication required, logging in.");
AuthenticateClient(requestBuilder, settings, true);
request = requestBuilder.Build();
response = _httpClient.Execute(request);
}
else
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
return response.Content;
}
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
{
if (settings.Username.IsNullOrWhiteSpace() || settings.Password.IsNullOrWhiteSpace())
{
if (reauthenticate)
{
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}
return;
}
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
var cookies = _authCookieCache.Find(authKey);
if (cookies == null || reauthenticate)
{
_authCookieCache.Remove(authKey);
var authLoginRequest = BuildRequest(settings).Resource("/api/v2/auth/login")
.Post()
.AddFormParameter("username", settings.Username ?? string.Empty)
.AddFormParameter("password", settings.Password ?? string.Empty)
.Build();
HttpResponse response;
try
{
response = _httpClient.Execute(authLoginRequest);
}
catch (HttpException ex)
{
_logger.Debug("qbitTorrent authentication failed.");
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
}
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
}
if (response.Content != "Ok.") // returns "Fails." on bad login
{
_logger.Debug("qbitTorrent authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}
_logger.Debug("qBittorrent authentication succeeded.");
cookies = response.GetCookies();
_authCookieCache.Set(authKey, cookies);
}
requestBuilder.SetCookies(cookies);
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.QBittorrent
@@ -25,5 +25,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public string SavePath { get; set; } // Torrent save path
public float Ratio { get; set; } // Torrent share ratio
[JsonProperty(PropertyName = "ratio_limit")] // Per torrent seeding ratio limit (-2 = use global, -1 = unlimited)
public float RatioLimit { get; set; } = -2;
[JsonProperty(PropertyName = "seeding_time")]
public long? SeedingTime { get; set; } // Torrent seeding time (not provided by the list api)
[JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited)
public long SeedingTimeLimit { get; set; } = -2;
}
public class QBittorrentTorrentProperties
{
public string Hash { get; set; } // Torrent hash
[JsonProperty(PropertyName = "seeding_time")]
public long SeedingTime { get; set; } // Torrent seeding time
}
}

View File

@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestGetTorrents());
}

View File

@@ -89,11 +89,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
foreach (RTorrentTorrent torrent in torrents)
{
// Don't concern ourselves with categories other than specified
if (torrent.Category != Settings.TvCategory) continue;
if (Settings.TvCategory.IsNotNullOrWhiteSpace() && torrent.Category != Settings.TvCategory) continue;
if (torrent.Path.StartsWith("."))
{
throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent.");
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
}
var item = new DownloadClientItem();
@@ -163,7 +163,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestGetTorrents());
failures.AddIfNotNull(TestDirectory());
}

View File

@@ -224,7 +224,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestGetTorrents());
}

View File

@@ -8,6 +8,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Newznab
{
@@ -102,7 +103,7 @@ namespace NzbDrone.Core.Indexers.Newznab
protected override void Test(List<ValidationFailure> failures)
{
base.Test(failures);
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestCapabilities());
}

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Torznab
{
@@ -91,7 +92,7 @@ namespace NzbDrone.Core.Indexers.Torznab
protected override void Test(List<ValidationFailure> failures)
{
base.Test(failures);
if (failures.Any()) return;
if (failures.HasErrors()) return;
failures.AddIfNotNull(TestCapabilities());
}

View File

@@ -151,12 +151,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
private string GetOriginalFilePath(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
{
if (downloadClientItem != null)
var path = localEpisode.Path;
if (downloadClientItem != null && !downloadClientItem.OutputPath.IsEmpty)
{
return downloadClientItem.OutputPath.Directory.ToString().GetRelativePath(localEpisode.Path);
var outputDirectory = downloadClientItem.OutputPath.Directory.ToString();
if (outputDirectory.IsParentPath(path))
{
return outputDirectory.GetRelativePath(path);
}
}
var path = localEpisode.Path;
var folderEpisodeInfo = localEpisode.FolderEpisodeInfo;
if (folderEpisodeInfo != null)
@@ -177,7 +183,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return grandparentPath.GetRelativePath(path);
}
return Path.Combine(Path.GetFileName(parentPath), Path.GetFileName(path));
return Path.GetFileName(path);
}
private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)

View File

@@ -460,7 +460,7 @@
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentPriority.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxy.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV1.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentSettings.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentTorrent.cs" />
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
@@ -1234,6 +1234,8 @@
<Compile Include="Validation\ProfileExistsValidator.cs" />
<Compile Include="Validation\RuleBuilderExtensions.cs" />
<Compile Include="Validation\UrlValidator.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV2.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxySelector.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">

View File

@@ -73,7 +73,9 @@ namespace NzbDrone.Core.Tv
return;
}
var previouslyAired = message.Added.Where(a => a.AirDateUtc.HasValue && a.AirDateUtc.Value.Before(DateTime.UtcNow.AddDays(1)) && a.Monitored).ToList();
var previouslyAired = message.Added.Where(a => a.AirDateUtc.HasValue
&& a.AirDateUtc.Value.Between(DateTime.UtcNow.AddDays(-14), DateTime.UtcNow.AddDays(1))
&& a.Monitored).ToList();
if (previouslyAired.Empty())
{

View File

@@ -34,6 +34,7 @@ namespace NzbDrone.Core.Tv
var existingEpisodes = _episodeService.GetEpisodeBySeries(series.Id);
var seasons = series.Seasons;
var hasExisting = existingEpisodes.Any();
var updateList = new List<Episode>();
var newList = new List<Episode>();
@@ -82,6 +83,8 @@ namespace NzbDrone.Core.Tv
}
}
UnmonitorReaddedEpisodes(series, newList, hasExisting);
var allEpisodes = new List<Episode>();
allEpisodes.AddRange(newList);
allEpisodes.AddRange(updateList);
@@ -117,6 +120,41 @@ namespace NzbDrone.Core.Tv
return season == null || season.Monitored;
}
private void UnmonitorReaddedEpisodes(Series series, List<Episode> episodes, bool hasExisting)
{
if (series.AddOptions != null)
{
return;
}
var threshold = DateTime.UtcNow.AddDays(-14);
var oldEpisodes = episodes.Where(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(threshold)).ToList();
if (oldEpisodes.Any())
{
if (hasExisting)
{
_logger.Warn("Show {0} ({1}) had {2} old episodes appear, please check monitored status.", series.TvdbId, series.Title, oldEpisodes.Count);
}
else
{
threshold = DateTime.UtcNow.AddDays(-1);
foreach (var episode in episodes)
{
if (episode.AirDateUtc.HasValue && episode.AirDateUtc.Value.Before(threshold))
{
episode.Monitored = false;
}
}
_logger.Warn("Show {0} ({1}) had {2} old episodes appear, unmonitored aired episodes to prevent unexpected downloads.", series.TvdbId, series.Title, oldEpisodes.Count);
}
}
}
private void AdjustMultiEpisodeAirTime(Series series, IEnumerable<Episode> allEpisodes)
{
if (series.Network == "Netflix")

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Core.Analytics;
namespace NzbDrone.Core.Update
{
@@ -15,14 +16,16 @@ namespace NzbDrone.Core.Update
public class UpdatePackageProvider : IUpdatePackageProvider
{
private readonly IHttpClient _httpClient;
private readonly IPlatformInfo _platformInfo;
private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IPlatformInfo _platformInfo;
private readonly IAnalyticsService _analyticsService;
public UpdatePackageProvider(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, IPlatformInfo platformInfo)
public UpdatePackageProvider(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, IPlatformInfo platformInfo)
{
_httpClient = httpClient;
_platformInfo = platformInfo;
_analyticsService = analyticsService;
_requestBuilder = requestBuilder.Services;
_httpClient = httpClient;
}
public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
@@ -32,10 +35,15 @@ namespace NzbDrone.Core.Update
.AddQueryParam("version", currentVersion)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("runtimeVer", _platformInfo.Version)
.SetSegment("branch", branch)
.Build();
.SetSegment("branch", branch);
var update = _httpClient.Get<UpdatePackageAvailable>(request).Resource;
if (_analyticsService.IsEnabled)
{
// Send if the system is active so we know which versions to deprecate/ignore
request.AddQueryParam("active", _analyticsService.InstallIsActive.ToString().ToLower());
}
var update = _httpClient.Get<UpdatePackageAvailable>(request.Build()).Resource;
if (!update.Available) return null;
@@ -49,10 +57,15 @@ namespace NzbDrone.Core.Update
.AddQueryParam("version", currentVersion)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("runtimeVer", _platformInfo.Version)
.SetSegment("branch", branch)
.Build();
.SetSegment("branch", branch);
var updates = _httpClient.Get<List<UpdatePackage>>(request);
if (_analyticsService.IsEnabled)
{
// Send if the system is active so we know which versions to deprecate/ignore
request.AddQueryParam("active", _analyticsService.InstallIsActive.ToString().ToLower());
}
var updates = _httpClient.Get<List<UpdatePackage>>(request.Build());
return updates.Resource;
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
namespace NzbDrone.Core.Validation
{
@@ -19,5 +21,21 @@ namespace NzbDrone.Core.Validation
throw new ValidationException(result.Errors);
}
}
public static bool HasErrors(this List<ValidationFailure> list)
{
foreach (var item in list)
{
var extended = item as NzbDroneValidationFailure;
if (extended != null && extended.IsWarning)
{
continue;
}
return true;
}
return false;
}
}
}