1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

Compare commits

...

17 Commits

Author SHA1 Message Date
Taloth Saldono e52fcf843c Handle Deluge v2 beta breaking change in their api.
closes #2412
2019-03-03 23:10:28 +01:00
Taloth Saldono 08ba273089 fixed qbittorrent tests failing due to incorrect test setup. And http tests failed due to httpbin changing their output. 2019-03-03 21:19:25 +01:00
Taloth Saldono faa2d632e5 New: Indexer Seed Limit settings applied to new downloads for qBittorrent
closes #2607
2019-03-03 20:25:31 +01:00
Taloth Saldono 1b939ebf4b Fixed: Magnet Link progress visualisation and adding magnet links if dht is disabled in qBittorrent 2019-03-03 19:29:25 +01:00
Mark Bebbington aa46216117 Fixed: qBittorrent api v2 support (qbit v4.1+)
fixes #2887
closes #2951
ref #2945
2019-03-03 19:26:50 +01:00
Mark McDowall c3c6b3d166 Fixed: Importing completed downloads from NZBGet with post processing script failing
Fixes #2919

# Conflicts:
#	src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs
2019-02-06 19:36:37 -08:00
Mark McDowall 2c95f07cb2 Another path test fix 2019-02-01 10:58:02 -08:00
Mark McDowall 4a2277b424 Fix path tests 2019-02-01 10:57:49 -08:00
Mark McDowall a1f02916d4 Fixed: Importing of completed download when not a child of the download client output path 2019-02-01 10:57:39 -08:00
Mark McDowall 900dfd92d0 Fixed: Getting parent of UNC paths 2019-02-01 10:56:48 -08:00
Mark McDowall d6997b0588 Fixed getting parent path from a path without another slash
Fixed: Manual Import failing for some paths
2019-02-01 10:55:28 -08:00
Taloth Saldono 779ab39f50 Fixed failing test 2019-01-12 13:30:08 +01:00
Taloth Saldono 00283e3d6e New: Limit indexer/download client backoff to 5 min during the first 15 min of application start.
closes #2366
2019-01-12 13:15:41 +01:00
Taloth Saldono 2b4429f8b7 Fixed: Erroneously matching Anime 10.5 special as 10.
fixes #2868
2019-01-12 13:14:47 +01:00
Taloth Saldono 2446c4185a Added 10-bit to parser cleanup.
fixes #2870
2019-01-12 13:14:47 +01:00
Taloth Saldono 04900e5f90 Tweaked reverse title detection to handle triple digit episode numbers.
fixes #2871
2019-01-12 13:14:47 +01:00
Taloth Saldono ce59db528b Fixed: Mono bug causing memory leakage when http connections use gzip compression.
The bug is registered upstream, but this commit works around the problem by doing the gzip decompression separately from the http stack.

Ref #2296
2019-01-10 20:13:48 +01:00
29 changed files with 1100 additions and 170 deletions
@@ -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)
@@ -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");
}
}
}
@@ -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)
+3 -1
View File
@@ -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
+35 -23
View File
@@ -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));