mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e52fcf843c | |||
| 08ba273089 | |||
| faa2d632e5 | |||
| 1b939ebf4b | |||
| aa46216117 | |||
| c3c6b3d166 | |||
| 2c95f07cb2 | |||
| 4a2277b424 | |||
| a1f02916d4 | |||
| 900dfd92d0 | |||
| d6997b0588 | |||
| 779ab39f50 | |||
| 00283e3d6e | |||
| 2b4429f8b7 | |||
| 2446c4185a | |||
| 04900e5f90 | |||
| ce59db528b |
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using Nancy;
|
using Nancy;
|
||||||
using Nancy.Bootstrapper;
|
using Nancy.Bootstrapper;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Extensions.Pipelines
|
namespace NzbDrone.Api.Extensions.Pipelines
|
||||||
@@ -15,9 +16,14 @@ namespace NzbDrone.Api.Extensions.Pipelines
|
|||||||
|
|
||||||
public int Order => 0;
|
public int Order => 0;
|
||||||
|
|
||||||
|
private readonly Action<Action<Stream>, Stream> _writeGZipStream;
|
||||||
|
|
||||||
public GzipCompressionPipeline(Logger logger)
|
public GzipCompressionPipeline(Logger logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
|
||||||
|
_writeGZipStream = PlatformInfo.IsMono ? WriteGZipStreamMono : (Action<Action<Stream>, Stream>)WriteGZipStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Register(IPipelines pipelines)
|
public void Register(IPipelines pipelines)
|
||||||
@@ -43,14 +49,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
|
|||||||
var contents = response.Contents;
|
var contents = response.Contents;
|
||||||
|
|
||||||
response.Headers["Content-Encoding"] = "gzip";
|
response.Headers["Content-Encoding"] = "gzip";
|
||||||
response.Contents = responseStream =>
|
response.Contents = responseStream => _writeGZipStream(contents, responseStream);
|
||||||
{
|
|
||||||
using (var gzip = new GZipStream(responseStream, CompressionMode.Compress, true))
|
|
||||||
using (var buffered = new BufferedStream(gzip, 8192))
|
|
||||||
{
|
|
||||||
contents.Invoke(buffered);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +60,25 @@ namespace NzbDrone.Api.Extensions.Pipelines
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WriteGZipStreamMono(Action<Stream> innerContent, Stream targetStream)
|
||||||
|
{
|
||||||
|
using (var membuffer = new MemoryStream())
|
||||||
|
{
|
||||||
|
WriteGZipStream(innerContent, membuffer);
|
||||||
|
membuffer.Position = 0;
|
||||||
|
membuffer.CopyTo(targetStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteGZipStream(Action<Stream> innerContent, Stream targetStream)
|
||||||
|
{
|
||||||
|
using (var gzip = new GZipStream(targetStream, CompressionMode.Compress, true))
|
||||||
|
using (var buffered = new BufferedStream(gzip, 8192))
|
||||||
|
{
|
||||||
|
innerContent.Invoke(buffered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ContentLengthIsTooSmall(Response response)
|
private static bool ContentLengthIsTooSmall(Response response)
|
||||||
{
|
{
|
||||||
var contentLength = response.Headers.GetValueOrDefault("Content-Length");
|
var contentLength = response.Headers.GetValueOrDefault("Content-Length");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
namespace NzbDrone.Common.EnvironmentInfo
|
namespace NzbDrone.Common.EnvironmentInfo
|
||||||
{
|
{
|
||||||
public interface IRuntimeInfo
|
public interface IRuntimeInfo
|
||||||
{
|
{
|
||||||
|
DateTime StartTime { get; }
|
||||||
bool IsUserInteractive { get; }
|
bool IsUserInteractive { get; }
|
||||||
bool IsAdmin { get; }
|
bool IsAdmin { get; }
|
||||||
bool IsWindowsService { get; }
|
bool IsWindowsService { get; }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
public class RuntimeInfo : IRuntimeInfo
|
public class RuntimeInfo : IRuntimeInfo
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
private readonly DateTime _startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
|
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
|
||||||
{
|
{
|
||||||
@@ -37,6 +38,14 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
IsProduction = InternalIsProduction();
|
IsProduction = InternalIsProduction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DateTime StartTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _startTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsUserInteractive => Environment.UserInteractive;
|
public static bool IsUserInteractive => Environment.UserInteractive;
|
||||||
|
|
||||||
bool IRuntimeInfo.IsUserInteractive => IsUserInteractive;
|
bool IRuntimeInfo.IsUserInteractive => IsUserInteractive;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
@@ -25,11 +26,20 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
{
|
{
|
||||||
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
|
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
|
||||||
|
|
||||||
// Deflate is not a standard and could break depending on implementation.
|
if (PlatformInfo.IsMono)
|
||||||
// we should just stick with the more compatible Gzip
|
{
|
||||||
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
|
||||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
webRequest.AutomaticDecompression = DecompressionMethods.None;
|
||||||
|
webRequest.Headers.Add("Accept-Encoding", "gzip");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Deflate is not a standard and could break depending on implementation.
|
||||||
|
// we should just stick with the more compatible Gzip
|
||||||
|
//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.Method = request.Method.ToString();
|
||||||
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
||||||
webRequest.KeepAlive = request.ConnectionKeepAlive;
|
webRequest.KeepAlive = request.ConnectionKeepAlive;
|
||||||
@@ -107,6 +117,19 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
data = responseStream.ToBytes();
|
data = responseStream.ToBytes();
|
||||||
|
|
||||||
|
if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip")
|
||||||
|
{
|
||||||
|
using (var compressedStream = new MemoryStream(data))
|
||||||
|
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
|
||||||
|
using (var decompressedStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
gzip.CopyTo(decompressedStream);
|
||||||
|
data = decompressedStream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
httpWebResponse.Headers.Remove("Content-Encoding");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
@@ -16,6 +17,10 @@ namespace NzbDrone.Core.Test.Download
|
|||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
_epoch = DateTime.UtcNow;
|
_epoch = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Mocker.GetMock<IRuntimeInfo>()
|
||||||
|
.SetupGet(v => v.StartTime)
|
||||||
|
.Returns(_epoch - TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadClientStatus WithStatus(DownloadClientStatus status)
|
private DownloadClientStatus WithStatus(DownloadClientStatus status)
|
||||||
|
|||||||
+98
-16
@@ -20,13 +20,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 +37,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()
|
||||||
@@ -100,10 +104,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
||||||
@@ -154,7 +158,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 +189,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 +207,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 +249,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 +277,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
id.Should().Be(expectedHash);
|
id.Should().Be(expectedHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Download_should_refuse_magnet_if_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";
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() => Subject.Download(remoteEpisode));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Download_should_set_top_priority()
|
public void Download_should_set_top_priority()
|
||||||
{
|
{
|
||||||
@@ -446,6 +464,56 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeTrue();
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
||||||
|
{
|
||||||
|
GivenMaxRatio(2.0f);
|
||||||
|
|
||||||
|
var torrent = new QBittorrentTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Progress = 1.0,
|
||||||
|
Eta = 8640000,
|
||||||
|
State = "pausedUP",
|
||||||
|
Label = "",
|
||||||
|
SavePath = "",
|
||||||
|
Ratio = 1.0f,
|
||||||
|
RatioLimit = 0.8f
|
||||||
|
};
|
||||||
|
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
GivenMaxRatio(0.2f);
|
||||||
|
|
||||||
|
var torrent = new QBittorrentTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Progress = 1.0,
|
||||||
|
Eta = 8640000,
|
||||||
|
State = "pausedUP",
|
||||||
|
Label = "",
|
||||||
|
SavePath = "",
|
||||||
|
Ratio = 0.5f,
|
||||||
|
RatioLimit = 0.8f
|
||||||
|
};
|
||||||
|
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_get_category_from_the_category_if_set()
|
public void should_get_category_from_the_category_if_set()
|
||||||
{
|
{
|
||||||
@@ -508,5 +576,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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
@@ -16,6 +17,10 @@ namespace NzbDrone.Core.Test.IndexerTests
|
|||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
_epoch = DateTime.UtcNow;
|
_epoch = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Mocker.GetMock<IRuntimeInfo>()
|
||||||
|
.SetupGet(v => v.StartTime)
|
||||||
|
.Returns(_epoch - TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WithStatus(IndexerStatus status)
|
private void WithStatus(IndexerStatus status)
|
||||||
|
|||||||
@@ -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())));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,5 +136,29 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
result.SeriesTitle.Should().Be(title);
|
result.SeriesTitle.Should().Be(title);
|
||||||
result.FullSeason.Should().BeFalse();
|
result.FullSeason.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("[Vivid] Living Sky Saga S01 [Web][MKV][h264 10-bit][1080p][AAC 2.0]", "Living Sky Saga", 1)]
|
||||||
|
public void should_parse_anime_season_packs(string postTitle, string title, int seasonNumber)
|
||||||
|
{
|
||||||
|
var result = Parser.Parser.ParseTitle(postTitle);
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
|
||||||
|
result.SeriesTitle.Should().Be(title);
|
||||||
|
result.FullSeason.Should().BeTrue();
|
||||||
|
result.SeasonNumber.Should().Be(seasonNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("[HorribleSubs] Goblin Slayer - 10.5 [1080p].mkv", "Goblin Slayer", 10.5)]
|
||||||
|
public void should_handle_anime_recap_numbering(string postTitle, string title, double specialEpisodeNumber)
|
||||||
|
{
|
||||||
|
var result = Parser.Parser.ParseTitle(postTitle);
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.SeriesTitle.Should().Be(title);
|
||||||
|
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
|
||||||
|
result.SpecialAbsoluteEpisodeNumbers.Should().NotBeEmpty();
|
||||||
|
result.SpecialAbsoluteEpisodeNumbers.Should().BeEquivalentTo(new[] { (decimal)specialEpisodeNumber });
|
||||||
|
result.FullSeason.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
"Fargo",
|
"Fargo",
|
||||||
Quality.WEBDL1080p,
|
Quality.WEBDL1080p,
|
||||||
"RARBG"
|
"RARBG"
|
||||||
|
},
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
@"C:\Test\XxQVHK4GJMP3n2dLpmhW\XxQVHK4GJMP3n2dLpmhW\MKV\010E70S.yhcranA.fo.snoS.mkv".AsOsAgnostic(),
|
||||||
|
"Sons of Anarchy",
|
||||||
|
Quality.HDTV720p,
|
||||||
|
null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@@ -25,8 +26,8 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
|
|
||||||
public class MockProviderStatusService : ProviderStatusServiceBase<IMockProvider, MockProviderStatus>
|
public class MockProviderStatusService : ProviderStatusServiceBase<IMockProvider, MockProviderStatus>
|
||||||
{
|
{
|
||||||
public MockProviderStatusService(IMockProviderStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
|
public MockProviderStatusService(IMockProviderStatusRepository providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
|
||||||
: base(providerStatusRepository, eventAggregator, logger)
|
: base(providerStatusRepository, eventAggregator, runtimeInfo, logger)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -40,9 +41,20 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
_epoch = DateTime.UtcNow;
|
_epoch = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Mocker.GetMock<IRuntimeInfo>()
|
||||||
|
.SetupGet(v => v.StartTime)
|
||||||
|
.Returns(_epoch - TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WithStatus(MockProviderStatus status)
|
private void GivenRecentStartup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IRuntimeInfo>()
|
||||||
|
.SetupGet(v => v.StartTime)
|
||||||
|
.Returns(_epoch - TimeSpan.FromMinutes(12));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockProviderStatus WithStatus(MockProviderStatus status)
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IMockProviderStatusRepository>()
|
Mocker.GetMock<IMockProviderStatusRepository>()
|
||||||
.Setup(v => v.FindByProviderId(1))
|
.Setup(v => v.FindByProviderId(1))
|
||||||
@@ -51,6 +63,8 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
Mocker.GetMock<IMockProviderStatusRepository>()
|
Mocker.GetMock<IMockProviderStatusRepository>()
|
||||||
.Setup(v => v.All())
|
.Setup(v => v.All())
|
||||||
.Returns(new[] { status });
|
.Returns(new[] { status });
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyUpdate()
|
private void VerifyUpdate()
|
||||||
@@ -122,5 +136,32 @@ namespace NzbDrone.Core.Test.ThingiProviderTests
|
|||||||
status.DisabledTill.Should().HaveValue();
|
status.DisabledTill.Should().HaveValue();
|
||||||
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
|
status.DisabledTill.Value.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(15), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_escalate_further_till_after_5_minutes_since_startup()
|
||||||
|
{
|
||||||
|
GivenRecentStartup();
|
||||||
|
|
||||||
|
var origStatus = WithStatus(new MockProviderStatus
|
||||||
|
{
|
||||||
|
InitialFailure = _epoch - TimeSpan.FromMinutes(6),
|
||||||
|
MostRecentFailure = _epoch - TimeSpan.FromSeconds(120),
|
||||||
|
EscalationLevel = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
Subject.RecordFailure(1);
|
||||||
|
|
||||||
|
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||||
|
status.Should().NotBeNull();
|
||||||
|
|
||||||
|
origStatus.EscalationLevel.Should().Be(3);
|
||||||
|
status.DisabledTill.Should().BeCloseTo(_epoch + TimeSpan.FromMinutes(5), 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Magnet Links 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,30 @@ 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 +171,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 +197,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);
|
||||||
|
|
||||||
@@ -194,8 +225,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 +234,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 +256,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 +306,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 +333,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_proxy.GetTorrents(Settings);
|
Proxy.GetTorrents(Settings);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -320,13 +351,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 +374,36 @@ 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 false;
|
||||||
|
}
|
||||||
|
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
||||||
|
{
|
||||||
|
if (torrent.Ratio < config.MaxRatio) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.SeedingTimeLimit >= 0)
|
||||||
|
{
|
||||||
|
if (torrent.SeedingTime < torrent.SeedingTimeLimit) return false;
|
||||||
|
}
|
||||||
|
else if (torrent.RatioLimit == -2 && config.MaxSeedingTimeEnabled)
|
||||||
|
{
|
||||||
|
if (torrent.SeedingTime < config.MaxSeedingTime) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,89 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-46
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -63,7 +90,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
var request = BuildRequest(settings).Resource("/query/torrents")
|
var request = BuildRequest(settings).Resource("/query/torrents")
|
||||||
.AddQueryParam("label", settings.TvCategory)
|
.AddQueryParam("label", settings.TvCategory)
|
||||||
.AddQueryParam("category", settings.TvCategory);
|
.AddQueryParam("category", settings.TvCategory);
|
||||||
|
|
||||||
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -107,7 +133,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 +148,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 +164,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 +179,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 +196,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 +209,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 +219,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 +227,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 +301,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,359 @@
|
|||||||
|
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")
|
||||||
|
.AddQueryParam("category", settings.TvCategory);
|
||||||
|
var response = ProcessRequest<List<QBittorrentTorrent>>(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,15 @@ 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
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited)
|
||||||
|
public long SeedingTimeLimit { get; set; } = -2;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.ThingiProvider.Status;
|
using NzbDrone.Core.ThingiProvider.Status;
|
||||||
|
|
||||||
@@ -12,8 +13,8 @@ namespace NzbDrone.Core.Download
|
|||||||
|
|
||||||
public class DownloadClientStatusService : ProviderStatusServiceBase<IDownloadClient, DownloadClientStatus>, IDownloadClientStatusService
|
public class DownloadClientStatusService : ProviderStatusServiceBase<IDownloadClient, DownloadClientStatus>, IDownloadClientStatusService
|
||||||
{
|
{
|
||||||
public DownloadClientStatusService(IDownloadClientStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
|
public DownloadClientStatusService(IDownloadClientStatusRepository providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
|
||||||
: base(providerStatusRepository, eventAggregator, logger)
|
: base(providerStatusRepository, eventAggregator, runtimeInfo, logger)
|
||||||
{
|
{
|
||||||
MinimumTimeSinceInitialFailure = TimeSpan.FromMinutes(5);
|
MinimumTimeSinceInitialFailure = TimeSpan.FromMinutes(5);
|
||||||
MaximumEscalationLevel = 5;
|
MaximumEscalationLevel = 5;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.ThingiProvider.Status;
|
using NzbDrone.Core.ThingiProvider.Status;
|
||||||
@@ -14,8 +15,8 @@ namespace NzbDrone.Core.Indexers
|
|||||||
|
|
||||||
public class IndexerStatusService : ProviderStatusServiceBase<IIndexer, IndexerStatus>, IIndexerStatusService
|
public class IndexerStatusService : ProviderStatusServiceBase<IIndexer, IndexerStatus>, IIndexerStatusService
|
||||||
{
|
{
|
||||||
public IndexerStatusService(IIndexerStatusRepository providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
|
public IndexerStatusService(IIndexerStatusRepository providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
|
||||||
: base(providerStatusRepository, eventAggregator, logger)
|
: base(providerStatusRepository, eventAggregator, runtimeInfo, logger)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace NzbDrone.Core.Parser.Model
|
|||||||
public int SeasonNumber { get; set; }
|
public int SeasonNumber { get; set; }
|
||||||
public int[] EpisodeNumbers { get; set; }
|
public int[] EpisodeNumbers { get; set; }
|
||||||
public int[] AbsoluteEpisodeNumbers { get; set; }
|
public int[] AbsoluteEpisodeNumbers { get; set; }
|
||||||
|
public decimal[] SpecialAbsoluteEpisodeNumbers { get; set; }
|
||||||
public string AirDate { get; set; }
|
public string AirDate { get; set; }
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
public bool FullSeason { get; set; }
|
public bool FullSeason { get; set; }
|
||||||
@@ -27,6 +28,7 @@ namespace NzbDrone.Core.Parser.Model
|
|||||||
{
|
{
|
||||||
EpisodeNumbers = new int[0];
|
EpisodeNumbers = new int[0];
|
||||||
AbsoluteEpisodeNumbers = new int[0];
|
AbsoluteEpisodeNumbers = new int[0];
|
||||||
|
SpecialAbsoluteEpisodeNumbers = new decimal[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDaily
|
public bool IsDaily
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -39,15 +40,15 @@ namespace NzbDrone.Core.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01)
|
//Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01)
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
|
//Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.)",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title Season+Episode + Absolute Episode Number
|
//Anime - [SubGroup] Title Season+Episode + Absolute Episode Number
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title Season+Episode
|
//Anime - [SubGroup] Title Season+Episode
|
||||||
@@ -55,15 +56,15 @@ namespace NzbDrone.Core.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title with trailing number Absolute Episode Number
|
//Anime - [SubGroup] Title with trailing number Absolute Episode Number
|
||||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?\d+?)[-_. ]+(?:[-_. ]?(?<absoluteepisode>\d{3}(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
|
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?\d+?)[-_. ]+(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title - Absolute Episode Number
|
//Anime - [SubGroup] Title - Absolute Episode Number
|
||||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
|
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - [SubGroup] Title Absolute Episode Number
|
//Anime - [SubGroup] Title Absolute Episode Number
|
||||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
|
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc)
|
//Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc)
|
||||||
@@ -75,7 +76,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - Title Season EpisodeNumber + Absolute Episode Number [SubGroup]
|
//Anime - Title Season EpisodeNumber + Absolute Episode Number [SubGroup]
|
||||||
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))).+?(?:[-_. ]?(?<absoluteepisode>(?<!\d+)\d{3}(?!\d+)))+.+?\[(?<subgroup>.+?)\](?:$|\.mkv)",
|
new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))).+?(?:[-_. ]?(?<absoluteepisode>(?<!\d+)\d{3}(\.\d{1,2})?(?!\d+)))+.+?\[(?<subgroup>.+?)\](?:$|\.mkv)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Multi-Episode with a title (S01E05E06, S01E05-06, S01E05 E06, etc) and trailing info in slashes
|
//Multi-Episode with a title (S01E05E06, S01E05-06, S01E05 E06, etc) and trailing info in slashes
|
||||||
@@ -83,11 +84,11 @@ namespace NzbDrone.Core.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - Title Absolute Episode Number [SubGroup]
|
//Anime - Title Absolute Episode Number [SubGroup]
|
||||||
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)",
|
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - Title Absolute Episode Number [Hash]
|
//Anime - Title Absolute Episode Number [Hash]
|
||||||
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)",
|
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Episodes with airdate AND season/episode number, capture season/epsiode only
|
//Episodes with airdate AND season/episode number, capture season/epsiode only
|
||||||
@@ -171,11 +172,11 @@ namespace NzbDrone.Core.Parser
|
|||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// Anime - Title with season number - Absolute Episode Number (Title S01 - EP14)
|
// Anime - Title with season number - Absolute Episode Number (Title S01 - EP14)
|
||||||
new Regex(@"^(?<title>.+?S\d{1,2})[-_. ]{3,}(?:EP)?(?<absoluteepisode>\d{2,3}(?!\d+|[-]))",
|
new Regex(@"^(?<title>.+?S\d{1,2})[-_. ]{3,}(?:EP)?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
// Anime - French titles with single episode numbers, with or without leading sub group ([RlsGroup] Title - Episode 1)
|
// Anime - French titles with single episode numbers, with or without leading sub group ([RlsGroup] Title - Episode 1)
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(?!\d+))",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(\.\d{1,2})?(?!\d+))",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Season only releases
|
//Season only releases
|
||||||
@@ -230,19 +231,19 @@ namespace NzbDrone.Core.Parser
|
|||||||
|
|
||||||
// TODO: THIS ONE
|
// TODO: THIS ONE
|
||||||
//Anime - Title Absolute Episode Number (e66)
|
//Anime - Title Absolute Episode Number (e66)
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - Title Episode Absolute Episode Number (Series Title Episode 01)
|
//Anime - Title Episode Absolute Episode Number (Series Title Episode 01)
|
||||||
new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - Title Absolute Episode Number
|
//Anime - Title Absolute Episode Number
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Anime - Title {Absolute Episode Number}
|
//Anime - Title {Absolute Episode Number}
|
||||||
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Extant, terrible multi-episode naming (extant.10708.hdtv-lol.mp4)
|
//Extant, terrible multi-episode naming (extant.10708.hdtv-lol.mp4)
|
||||||
@@ -290,7 +291,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
};
|
};
|
||||||
|
|
||||||
//Regex to detect whether the title was reversed.
|
//Regex to detect whether the title was reversed.
|
||||||
private static readonly Regex ReversedTitleRegex = new Regex(@"[-._ ](p027|p0801|\d{2}E\d{2}S)[-._ ]", RegexOptions.Compiled);
|
private static readonly Regex ReversedTitleRegex = new Regex(@"[-._ ](p027|p0801|\d{2,3}E\d{2}S)[-._ ]", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a(?!$)|an|the|and|or|of)(?:\b|_))|\W|_",
|
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a(?!$)|an|the|and|or|of)(?:\b|_))|\W|_",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
@@ -298,7 +299,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$",
|
private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex SimpleTitleRegex = new Regex(@"(?:(480|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?)\s*?",
|
private static readonly Regex SimpleTitleRegex = new Regex(@"(?:(480|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?|10-bit)\s*?",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex WebsitePrefixRegex = new Regex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net)[ -]*",
|
private static readonly Regex WebsitePrefixRegex = new Regex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net)[ -]*",
|
||||||
@@ -653,21 +654,32 @@ namespace NzbDrone.Core.Parser
|
|||||||
|
|
||||||
if (absoluteEpisodeCaptures.Any())
|
if (absoluteEpisodeCaptures.Any())
|
||||||
{
|
{
|
||||||
var first = Convert.ToInt32(absoluteEpisodeCaptures.First().Value);
|
var first = Convert.ToDecimal(absoluteEpisodeCaptures.First().Value, CultureInfo.InvariantCulture);
|
||||||
var last = Convert.ToInt32(absoluteEpisodeCaptures.Last().Value);
|
var last = Convert.ToDecimal(absoluteEpisodeCaptures.Last().Value, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
if (first > last)
|
if (first > last)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = last - first + 1;
|
if ((first % 1) != 0 || (last % 1) != 0)
|
||||||
result.AbsoluteEpisodeNumbers = Enumerable.Range(first, count).ToArray();
|
|
||||||
|
|
||||||
if (matchGroup.Groups["special"].Success)
|
|
||||||
{
|
{
|
||||||
|
if (absoluteEpisodeCaptures.Count != 1)
|
||||||
|
return null; // Multiple matches not allowed for specials
|
||||||
|
|
||||||
|
result.SpecialAbsoluteEpisodeNumbers = new decimal[] { first };
|
||||||
result.Special = true;
|
result.Special = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var count = last - first + 1;
|
||||||
|
result.AbsoluteEpisodeNumbers = Enumerable.Range((int)first, (int)count).ToArray();
|
||||||
|
|
||||||
|
if (matchGroup.Groups["special"].Success)
|
||||||
|
{
|
||||||
|
result.Special = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!episodeCaptures.Any() && !absoluteEpisodeCaptures.Any())
|
if (!episodeCaptures.Any() && !absoluteEpisodeCaptures.Any())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.ThingiProvider.Events;
|
using NzbDrone.Core.ThingiProvider.Events;
|
||||||
|
|
||||||
@@ -24,15 +25,18 @@ namespace NzbDrone.Core.ThingiProvider.Status
|
|||||||
|
|
||||||
protected readonly IProviderStatusRepository<TModel> _providerStatusRepository;
|
protected readonly IProviderStatusRepository<TModel> _providerStatusRepository;
|
||||||
protected readonly IEventAggregator _eventAggregator;
|
protected readonly IEventAggregator _eventAggregator;
|
||||||
|
protected readonly IRuntimeInfo _runtimeInfo;
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
protected int MaximumEscalationLevel { get; set; } = EscalationBackOff.Periods.Length - 1;
|
protected int MaximumEscalationLevel { get; set; } = EscalationBackOff.Periods.Length - 1;
|
||||||
protected TimeSpan MinimumTimeSinceInitialFailure { get; set; } = TimeSpan.Zero;
|
protected TimeSpan MinimumTimeSinceInitialFailure { get; set; } = TimeSpan.Zero;
|
||||||
|
protected TimeSpan MinimumTimeSinceStartup { get; set; } = TimeSpan.FromMinutes(15);
|
||||||
|
|
||||||
public ProviderStatusServiceBase(IProviderStatusRepository<TModel> providerStatusRepository, IEventAggregator eventAggregator, Logger logger)
|
public ProviderStatusServiceBase(IProviderStatusRepository<TModel> providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
|
||||||
{
|
{
|
||||||
_providerStatusRepository = providerStatusRepository;
|
_providerStatusRepository = providerStatusRepository;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
|
_runtimeInfo = runtimeInfo;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,9 +93,10 @@ namespace NzbDrone.Core.ThingiProvider.Status
|
|||||||
escalate = false;
|
escalate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inStartupGracePeriod = (_runtimeInfo.StartTime + MinimumTimeSinceStartup) > now;
|
||||||
var inGracePeriod = (status.InitialFailure.Value + MinimumTimeSinceInitialFailure) > now;
|
var inGracePeriod = (status.InitialFailure.Value + MinimumTimeSinceInitialFailure) > now;
|
||||||
|
|
||||||
if (escalate && !inGracePeriod)
|
if (escalate && !inGracePeriod && !inStartupGracePeriod)
|
||||||
{
|
{
|
||||||
status.EscalationLevel = Math.Min(MaximumEscalationLevel, status.EscalationLevel + 1);
|
status.EscalationLevel = Math.Min(MaximumEscalationLevel, status.EscalationLevel + 1);
|
||||||
}
|
}
|
||||||
@@ -109,6 +114,15 @@ namespace NzbDrone.Core.ThingiProvider.Status
|
|||||||
status.DisabledTill = now + CalculateBackOffPeriod(status);
|
status.DisabledTill = now + CalculateBackOffPeriod(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inStartupGracePeriod && minimumBackOff == TimeSpan.Zero && status.DisabledTill.HasValue)
|
||||||
|
{
|
||||||
|
var maximumDisabledTill = now + TimeSpan.FromSeconds(EscalationBackOff.Periods[1]);
|
||||||
|
if (maximumDisabledTill < status.DisabledTill)
|
||||||
|
{
|
||||||
|
status.DisabledTill = maximumDisabledTill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_providerStatusRepository.Upsert(status);
|
_providerStatusRepository.Upsert(status);
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(new ProviderStatusChangedEvent<TProvider>(providerId, status));
|
_eventAggregator.PublishEvent(new ProviderStatusChangedEvent<TProvider>(providerId, status));
|
||||||
|
|||||||
Reference in New Issue
Block a user