mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fc2866032 | |||
| eb2e7b9c79 | |||
| cab900f656 | |||
| e2b91e5dc4 | |||
| e52fcf843c | |||
| 08ba273089 | |||
| faa2d632e5 | |||
| 1b939ebf4b | |||
| aa46216117 | |||
| c3c6b3d166 | |||
| 2c95f07cb2 | |||
| 4a2277b424 | |||
| a1f02916d4 | |||
| 900dfd92d0 | |||
| d6997b0588 |
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -130,11 +130,12 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_execute_typed_get()
|
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);
|
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]
|
[Test]
|
||||||
@@ -706,6 +707,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
|
|
||||||
public class HttpBinResource
|
public class HttpBinResource
|
||||||
{
|
{
|
||||||
|
public Dictionary<string, object> Args { get; set; }
|
||||||
public Dictionary<string, object> Headers { get; set; }
|
public Dictionary<string, object> Headers { get; set; }
|
||||||
public string Origin { get; set; }
|
public string Origin { get; set; }
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
|
|||||||
@@ -138,18 +138,34 @@ namespace NzbDrone.Common.Test
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(@"C:\Test\mydir", @"C:\Test")]
|
[TestCase(@"C:\Test\mydir", @"C:\Test")]
|
||||||
[TestCase(@"C:\Test\", @"C:")]
|
[TestCase(@"C:\Test\", @"C:\")]
|
||||||
[TestCase(@"C:\", null)]
|
[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);
|
path.GetParentPath().Should().Be(parentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void path_should_return_parent_for_oversized_path()
|
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";
|
MonoOnly();
|
||||||
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";
|
|
||||||
|
// 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);
|
path.GetParentPath().Should().Be(parentPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_CLIENT_FOLDER_NAME = "NzbDrone.Update" + Path.DirectorySeparatorChar;
|
||||||
private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + 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)
|
public static string CleanFilePath(this string path)
|
||||||
{
|
{
|
||||||
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
|
||||||
@@ -67,15 +69,16 @@ namespace NzbDrone.Common.Extensions
|
|||||||
|
|
||||||
public static string GetParentPath(this string childPath)
|
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 (cleanPath.IsNullOrWhiteSpace())
|
||||||
|
|
||||||
if (index != -1)
|
|
||||||
{
|
{
|
||||||
return parentPath.Substring(0, index);
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return Directory.GetParent(cleanPath)?.FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsParentPath(this string parentPath, string childPath)
|
public static bool IsParentPath(this string parentPath, string childPath)
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ namespace NzbDrone.Common.Http
|
|||||||
FormData.Add(new HttpFormData
|
FormData.Add(new HttpFormData
|
||||||
{
|
{
|
||||||
Name = key,
|
Name = key,
|
||||||
ContentData = Encoding.UTF8.GetBytes(value.ToString())
|
ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture))
|
||||||
});
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
+182
-58
@@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles.TorrentInfo;
|
|||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.Clients.QBittorrent;
|
using NzbDrone.Core.Download.Clients.QBittorrent;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||||
{
|
{
|
||||||
@@ -20,13 +21,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
{
|
{
|
||||||
Subject.Definition = new DownloadClientDefinition();
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
Subject.Definition.Settings = new QBittorrentSettings
|
Subject.Definition.Settings = new QBittorrentSettings
|
||||||
{
|
{
|
||||||
Host = "127.0.0.1",
|
Host = "127.0.0.1",
|
||||||
Port = 2222,
|
Port = 2222,
|
||||||
Username = "admin",
|
Username = "admin",
|
||||||
Password = "pass",
|
Password = "pass",
|
||||||
TvCategory = "tv"
|
TvCategory = "tv"
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<ITorrentFileInfoReader>()
|
Mocker.GetMock<ITorrentFileInfoReader>()
|
||||||
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
|
.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]));
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||||
|
|
||||||
Mocker.GetMock<IQBittorrentProxy>()
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||||
.Returns(new QBittorrentPreferences());
|
.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()
|
protected void GivenRedirectToMagnet()
|
||||||
@@ -95,15 +100,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First;
|
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>()
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||||
.Returns(new QBittorrentPreferences
|
.Returns(new QBittorrentPreferences
|
||||||
{
|
{
|
||||||
RemoveOnMaxRatio = removeOnMaxRatio,
|
RemoveOnMaxRatio = removeOnMaxRatio,
|
||||||
MaxRatio = maxRatio
|
MaxRatio = maxRatio,
|
||||||
});
|
MaxRatioEnabled = maxRatio >= 0,
|
||||||
|
MaxSeedingTime = maxSeedingTime,
|
||||||
|
MaxSeedingTimeEnabled = maxSeedingTime >= 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
||||||
@@ -154,7 +162,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
VerifyPaused(item);
|
VerifyPaused(item);
|
||||||
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
|
item.RemainingTime.Should().NotHaveValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("pausedUP")]
|
[TestCase("pausedUP")]
|
||||||
@@ -185,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
[TestCase("queuedDL")]
|
[TestCase("queuedDL")]
|
||||||
[TestCase("checkingDL")]
|
[TestCase("checkingDL")]
|
||||||
|
[TestCase("metaDL")]
|
||||||
public void queued_item_should_have_required_properties(string state)
|
public void queued_item_should_have_required_properties(string state)
|
||||||
{
|
{
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
@@ -202,7 +211,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
VerifyQueued(item);
|
VerifyQueued(item);
|
||||||
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
|
item.RemainingTime.Should().NotHaveValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -244,7 +253,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
VerifyWarning(item);
|
VerifyWarning(item);
|
||||||
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
|
item.RemainingTime.Should().NotHaveValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -272,6 +281,35 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
id.Should().Be(expectedHash);
|
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]
|
[Test]
|
||||||
public void Download_should_set_top_priority()
|
public void Download_should_set_top_priority()
|
||||||
{
|
{
|
||||||
@@ -353,7 +391,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached()
|
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
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -374,11 +412,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
protected virtual QBittorrentTorrent GivenCompletedTorrent(
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused()
|
string state = "pausedUP",
|
||||||
|
float ratio = 0.1f, float ratioLimit = -2,
|
||||||
|
int seedingTime = 1, int seedingTimeLimit = -2)
|
||||||
{
|
{
|
||||||
GivenMaxRatio(1.0f);
|
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
Hash = "HASH",
|
Hash = "HASH",
|
||||||
@@ -386,12 +424,32 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 1.0,
|
Progress = 1.0,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "uploading",
|
State = state,
|
||||||
Label = "",
|
Label = "",
|
||||||
SavePath = "",
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -401,21 +459,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
||||||
{
|
{
|
||||||
GivenMaxRatio(1.0f, false);
|
GivenGlobalSeedLimits(-1);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||||
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 });
|
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -425,21 +470,86 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
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
|
var item = Subject.GetItems().Single();
|
||||||
{
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
Hash = "HASH",
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
Name = _title,
|
}
|
||||||
Size = 1000,
|
|
||||||
Progress = 1.0,
|
[Test]
|
||||||
Eta = 8640000,
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
||||||
State = "pausedUP",
|
{
|
||||||
Label = "",
|
GivenGlobalSeedLimits(2.0f);
|
||||||
SavePath = "",
|
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
|
||||||
Ratio = 1.0f
|
|
||||||
};
|
var item = Subject.GetItems().Single();
|
||||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
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();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
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()
|
public void should_get_category_from_the_category_if_set()
|
||||||
{
|
{
|
||||||
const string category = "tv-sonarr";
|
const string category = "tv-sonarr";
|
||||||
GivenMaxRatio(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
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()
|
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
||||||
{
|
{
|
||||||
const string category = "tv-sonarr";
|
const string category = "tv-sonarr";
|
||||||
GivenMaxRatio(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -508,5 +618,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
torrent.Eta.ToString().Should().Be("18446744073709335000");
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())));
|
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())));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestCategory());
|
failures.AddIfNotNull(TestCategory());
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,25 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
|
|
||||||
public string GetVersion(DelugeSettings settings)
|
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)
|
public Dictionary<string, object> GetConfig(DelugeSettings settings)
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestOutputPath());
|
failures.AddIfNotNull(TestOutputPath());
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestOutputPath());
|
failures.AddIfNotNull(TestOutputPath());
|
||||||
failures.AddIfNotNull(TestGetNZB());
|
failures.AddIfNotNull(TestGetNZB());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
|
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
|
||||||
{
|
{
|
||||||
private readonly IQBittorrentProxy _proxy;
|
private readonly IQBittorrentProxySelector _proxySelector;
|
||||||
|
|
||||||
public QBittorrent(IQBittorrentProxy proxy,
|
public QBittorrent(IQBittorrentProxySelector proxySelector,
|
||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
@@ -28,16 +28,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, 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)
|
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())
|
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
||||||
@@ -45,23 +52,28 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||||
{
|
{
|
||||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetInitialState(hash.ToLower());
|
SetInitialState(hash.ToLower());
|
||||||
|
|
||||||
|
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
|
||||||
|
{
|
||||||
|
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
|
||||||
|
}
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
|
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
|
||||||
{
|
{
|
||||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -76,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||||
{
|
{
|
||||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -86,6 +98,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
SetInitialState(hash.ToLower());
|
SetInitialState(hash.ToLower());
|
||||||
|
|
||||||
|
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
|
||||||
|
{
|
||||||
|
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
|
||||||
|
}
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,28 +110,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
public override IEnumerable<DownloadClientItem> GetItems()
|
public override IEnumerable<DownloadClientItem> GetItems()
|
||||||
{
|
{
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
var torrents = _proxy.GetTorrents(Settings);
|
var torrents = Proxy.GetTorrents(Settings);
|
||||||
|
|
||||||
var queueItems = new List<DownloadClientItem>();
|
var queueItems = new List<DownloadClientItem>();
|
||||||
|
|
||||||
foreach (var torrent in torrents)
|
foreach (var torrent in torrents)
|
||||||
{
|
{
|
||||||
var item = new DownloadClientItem();
|
var item = new DownloadClientItem()
|
||||||
item.DownloadId = torrent.Hash.ToUpper();
|
{
|
||||||
item.Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label;
|
DownloadId = torrent.Hash.ToUpper(),
|
||||||
item.Title = torrent.Name;
|
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
||||||
item.TotalSize = torrent.Size;
|
Title = torrent.Name,
|
||||||
item.DownloadClient = Definition.Name;
|
TotalSize = torrent.Size,
|
||||||
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
|
DownloadClient = Definition.Name,
|
||||||
item.RemainingTime = GetRemainingTime(torrent);
|
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
||||||
item.SeedRatio = torrent.Ratio;
|
RemainingTime = GetRemainingTime(torrent),
|
||||||
|
SeedRatio = torrent.Ratio,
|
||||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
|
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)),
|
||||||
|
};
|
||||||
|
|
||||||
// Avoid removing torrents that haven't reached the global max ratio.
|
// 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).
|
// 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)
|
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";
|
item.Message = "The download is stalled with no connections";
|
||||||
break;
|
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
|
case "downloading": // torrent is being downloaded and data is being transfered
|
||||||
default: // new status in API? default to downloading
|
default: // new status in API? default to downloading
|
||||||
item.Status = DownloadItemStatus.Downloading;
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
@@ -166,12 +196,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
public override void RemoveItem(string hash, bool deleteData)
|
public override void RemoveItem(string hash, bool deleteData)
|
||||||
{
|
{
|
||||||
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
Proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override DownloadClientInfo GetStatus()
|
public override DownloadClientInfo GetStatus()
|
||||||
{
|
{
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
|
|
||||||
var destDir = new OsPath(config.SavePath);
|
var destDir = new OsPath(config.SavePath);
|
||||||
|
|
||||||
@@ -185,7 +215,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestPrioritySupport());
|
failures.AddIfNotNull(TestPrioritySupport());
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
@@ -194,8 +224,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var version = _proxy.GetVersion(Settings);
|
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
|
||||||
if (version < 5)
|
if (version < Version.Parse("1.5"))
|
||||||
{
|
{
|
||||||
// API version 5 introduced the "save_path" property in /query/torrents
|
// API version 5 introduced the "save_path" property in /query/torrents
|
||||||
return new NzbDroneValidationFailure("Host", "Unsupported client version")
|
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."
|
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
|
// API version 6 introduced support for labels
|
||||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
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
|
// Complain if qBittorrent is configured to remove torrents on max ratio
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
|
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")
|
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
|
try
|
||||||
{
|
{
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
|
|
||||||
if (!config.QueueingEnabled)
|
if (!config.QueueingEnabled)
|
||||||
{
|
{
|
||||||
@@ -302,7 +332,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_proxy.GetTorrents(Settings);
|
Proxy.GetTorrents(Settings);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -320,13 +350,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
switch ((QBittorrentState)Settings.InitialState)
|
switch ((QBittorrentState)Settings.InitialState)
|
||||||
{
|
{
|
||||||
case QBittorrentState.ForceStart:
|
case QBittorrentState.ForceStart:
|
||||||
_proxy.SetForceStart(hash, true, Settings);
|
Proxy.SetForceStart(hash, true, Settings);
|
||||||
break;
|
break;
|
||||||
case QBittorrentState.Start:
|
case QBittorrentState.Start:
|
||||||
_proxy.ResumeTorrent(hash, Settings);
|
Proxy.ResumeTorrent(hash, Settings);
|
||||||
break;
|
break;
|
||||||
case QBittorrentState.Pause:
|
case QBittorrentState.Pause:
|
||||||
_proxy.PauseTorrent(hash, Settings);
|
Proxy.PauseTorrent(hash, Settings);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +373,53 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qBittorrent sends eta=8640000 if unknown such as queued
|
||||||
|
if (torrent.Eta == 8640000)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return TimeSpan.FromSeconds((int)torrent.Eta);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
{
|
{
|
||||||
@@ -14,10 +14,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
[JsonProperty(PropertyName = "max_ratio")]
|
[JsonProperty(PropertyName = "max_ratio")]
|
||||||
public float MaxRatio { get; set; } // Get the global share ratio limit
|
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")]
|
[JsonProperty(PropertyName = "max_ratio_act")]
|
||||||
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
|
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "queueing_enabled")]
|
[JsonProperty(PropertyName = "queueing_enabled")]
|
||||||
public bool QueueingEnabled { get; set; } = true;
|
public bool QueueingEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "dht")]
|
||||||
|
public bool DhtEnabled { get; set; } // DHT enabled (needed for more peers and magnet downloads)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-49
@@ -11,41 +11,68 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
|
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
|
||||||
|
|
||||||
public interface IQBittorrentProxy
|
public class QBittorrentProxyV1 : 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
|
|
||||||
{
|
{
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
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;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
_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 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;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -60,15 +87,25 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/query/torrents")
|
var request = BuildRequest(settings).Resource("/query/torrents");
|
||||||
.AddQueryParam("label", settings.TvCategory)
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
.AddQueryParam("category", settings.TvCategory);
|
{
|
||||||
|
request.AddQueryParam("label", settings.TvCategory);
|
||||||
|
request.AddQueryParam("category", settings.TvCategory);
|
||||||
|
}
|
||||||
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||||
|
|
||||||
return response;
|
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)
|
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/command/download")
|
var request = BuildRequest(settings).Resource("/command/download")
|
||||||
@@ -107,7 +144,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||||
{
|
{
|
||||||
request.AddFormParameter("paused", true);
|
request.AddFormParameter("paused", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = ProcessRequest(request, settings);
|
var result = ProcessRequest(request, settings);
|
||||||
@@ -122,8 +159,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
|
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hashes", hash);
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
@@ -138,7 +175,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
ProcessRequest(setCategoryRequest, settings);
|
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 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)
|
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)
|
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/command/topPrio")
|
var request = BuildRequest(settings).Resource("/command/topPrio")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hashes", hash);
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
@@ -166,7 +207,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
catch (DownloadClientException ex)
|
catch (DownloadClientException ex)
|
||||||
{
|
{
|
||||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
// 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)
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -180,9 +220,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/command/pause")
|
var request = BuildRequest(settings).Resource("/command/pause")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hash", hash);
|
.AddFormParameter("hash", hash);
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +230,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
var request = BuildRequest(settings).Resource("/command/resume")
|
var request = BuildRequest(settings).Resource("/command/resume")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hash", hash);
|
.AddFormParameter("hash", hash);
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,17 +238,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hashes", hash)
|
.AddFormParameter("hashes", hash)
|
||||||
.AddFormParameter("value", enabled ? "true": "false");
|
.AddFormParameter("value", enabled ? "true" : "false");
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port);
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
|
||||||
requestBuilder.LogResponseContent = true;
|
{
|
||||||
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
LogResponseContent = true,
|
||||||
|
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
|
||||||
|
};
|
||||||
return requestBuilder;
|
return requestBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,11 +312,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
_authCookieCache.Remove(authKey);
|
_authCookieCache.Remove(authKey);
|
||||||
|
|
||||||
var authLoginRequest = BuildRequest(settings).Resource("/login")
|
var authLoginRequest = BuildRequest(settings).Resource( "/login")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("username", settings.Username ?? string.Empty)
|
.AddFormParameter("username", settings.Username ?? string.Empty)
|
||||||
.AddFormParameter("password", settings.Password ?? string.Empty)
|
.AddFormParameter("password", settings.Password ?? string.Empty)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
try
|
try
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
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 string SavePath { get; set; } // Torrent save path
|
||||||
|
|
||||||
public float Ratio { get; set; } // Torrent share ratio
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
foreach (RTorrentTorrent torrent in torrents)
|
foreach (RTorrentTorrent torrent in torrents)
|
||||||
{
|
{
|
||||||
// Don't concern ourselves with categories other than specified
|
// 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("."))
|
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();
|
var item = new DownloadClientItem();
|
||||||
@@ -163,7 +163,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
failures.AddIfNotNull(TestDirectory());
|
failures.AddIfNotNull(TestDirectory());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Http;
|
|||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Newznab
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
@@ -102,7 +103,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
base.Test(failures);
|
base.Test(failures);
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestCapabilities());
|
failures.AddIfNotNull(TestCapabilities());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.Configuration;
|
|||||||
using NzbDrone.Core.Indexers.Newznab;
|
using NzbDrone.Core.Indexers.Newznab;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Torznab
|
namespace NzbDrone.Core.Indexers.Torznab
|
||||||
{
|
{
|
||||||
@@ -91,7 +92,7 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
base.Test(failures);
|
base.Test(failures);
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestCapabilities());
|
failures.AddIfNotNull(TestCapabilities());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,12 +151,18 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
private string GetOriginalFilePath(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
|
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;
|
var folderEpisodeInfo = localEpisode.FolderEpisodeInfo;
|
||||||
|
|
||||||
if (folderEpisodeInfo != null)
|
if (folderEpisodeInfo != null)
|
||||||
@@ -177,7 +183,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
return grandparentPath.GetRelativePath(path);
|
return grandparentPath.GetRelativePath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Path.Combine(Path.GetFileName(parentPath), Path.GetFileName(path));
|
return Path.GetFileName(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
|
private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
|
||||||
|
|||||||
@@ -460,7 +460,7 @@
|
|||||||
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
||||||
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
|
||||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentPriority.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\QBittorrentSettings.cs" />
|
||||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentTorrent.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentTorrent.cs" />
|
||||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
|
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
|
||||||
@@ -1234,6 +1234,8 @@
|
|||||||
<Compile Include="Validation\ProfileExistsValidator.cs" />
|
<Compile Include="Validation\ProfileExistsValidator.cs" />
|
||||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||||
<Compile Include="Validation\UrlValidator.cs" />
|
<Compile Include="Validation\UrlValidator.cs" />
|
||||||
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV2.cs" />
|
||||||
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxySelector.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
|
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Validation
|
namespace NzbDrone.Core.Validation
|
||||||
{
|
{
|
||||||
@@ -19,5 +21,21 @@ namespace NzbDrone.Core.Validation
|
|||||||
throw new ValidationException(result.Errors);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user