mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
78 Commits
v2.0.0.5228
..
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 63853494f8 | |||
| 6593575850 | |||
| bbf74a8835 | |||
| 1fc2866032 | |||
| eb2e7b9c79 | |||
| cab900f656 | |||
| e2b91e5dc4 | |||
| e52fcf843c | |||
| 08ba273089 | |||
| faa2d632e5 | |||
| 1b939ebf4b | |||
| aa46216117 | |||
| c3c6b3d166 | |||
| 2c95f07cb2 | |||
| 4a2277b424 | |||
| a1f02916d4 | |||
| 900dfd92d0 | |||
| d6997b0588 | |||
| 779ab39f50 | |||
| 00283e3d6e | |||
| 2b4429f8b7 | |||
| 2446c4185a | |||
| 04900e5f90 | |||
| ce59db528b | |||
| 31b266659e | |||
| e071b0c2e0 | |||
| 270f04d2d2 | |||
| 9af57c6786 | |||
| ff4a550cbb | |||
| 537e4d7c39 | |||
| 9f16d9b2fc | |||
| ae6d920e2a | |||
| 0d22f9ec29 | |||
| 699076a405 | |||
| df593f486f | |||
| 0d95873a05 | |||
| b20acc9063 | |||
| 70d6d25178 | |||
| 196d165432 | |||
| bb3ca998fc | |||
| da73221cef | |||
| 36f66eed21 | |||
| 8e916d60f5 | |||
| 44048207f2 | |||
| b73b99df8d | |||
| ad69ecc5eb | |||
| 1304bc8fb9 | |||
| a4f63e728c | |||
| 307b3536b7 | |||
| 24c6d3f4b3 | |||
| 4a052708c8 | |||
| 39a8d4f0d8 | |||
| ca22a25842 | |||
| ff9a9a5e4d | |||
| 3d7c59bc3b | |||
| 63ea1f1afd | |||
| baf8f6cca6 | |||
| c67c7e1b5a | |||
| 46d8e5830a | |||
| 37054673b7 | |||
| 86bc5c5547 | |||
| fc44607c73 | |||
| 2a1421f488 | |||
| d7a054f637 | |||
| 9c9ad9aec3 | |||
| 1467c52e03 | |||
| e407145d10 | |||
| 476110b1de | |||
| 45f9f45f50 | |||
| d581d997c2 | |||
| 633344e5bb | |||
| 0cce6b74f9 | |||
| 8b8bfb9bf0 | |||
| 7241ca4ae9 | |||
| e9b11e55e9 | |||
| 48126f55ed | |||
| cb549507ee | |||
| a0b6cdb08e |
@@ -52,15 +52,6 @@ CleanFolder()
|
|||||||
find $path -depth -empty -type d -exec rm -r "{}" \;
|
find $path -depth -empty -type d -exec rm -r "{}" \;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AddJsonNet()
|
|
||||||
{
|
|
||||||
rm $outputFolder/Newtonsoft.Json.*
|
|
||||||
cp $sourceFolder/packages/Newtonsoft.Json.*/lib/net35/*.dll $outputFolder
|
|
||||||
cp $sourceFolder/packages/Newtonsoft.Json.*/lib/net35/*.dll $outputFolder/NzbDrone.Update
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildWithMSBuild()
|
BuildWithMSBuild()
|
||||||
{
|
{
|
||||||
export PATH=$msBuild:$PATH
|
export PATH=$msBuild:$PATH
|
||||||
@@ -91,8 +82,6 @@ Build()
|
|||||||
|
|
||||||
CleanFolder $outputFolder false
|
CleanFolder $outputFolder false
|
||||||
|
|
||||||
AddJsonNet
|
|
||||||
|
|
||||||
echo "Removing Mono.Posix.dll"
|
echo "Removing Mono.Posix.dll"
|
||||||
rm $outputFolder/Mono.Posix.dll
|
rm $outputFolder/Mono.Posix.dll
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ APPNAME="Sonarr"
|
|||||||
|
|
||||||
#set up environment
|
#set up environment
|
||||||
if [[ -x '/opt/local/bin/mono' ]]; then
|
if [[ -x '/opt/local/bin/mono' ]]; then
|
||||||
|
# Macports and mono-supplied installer path
|
||||||
export PATH="/opt/local/bin:$PATH"
|
export PATH="/opt/local/bin:$PATH"
|
||||||
|
elif [[ -x '/usr/local/bin/mono' ]]; then
|
||||||
|
# Homebrew-supplied path to mono
|
||||||
|
export PATH="/usr/local/bin:$PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export DYLD_FALLBACK_LIBRARY_PATH="$DIR"
|
export DYLD_FALLBACK_LIBRARY_PATH="$DIR"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
@@ -91,9 +91,11 @@ namespace Marr.Data.Mapping
|
|||||||
Type entType = ent.GetType();
|
Type entType = ent.GetType();
|
||||||
if (_repos.Relationships.ContainsKey(entType))
|
if (_repos.Relationships.ContainsKey(entType))
|
||||||
{
|
{
|
||||||
|
var provider = _db.ProviderFactory;
|
||||||
|
var connectionString = _db.ConnectionString;
|
||||||
Func<IDataMapper> dbCreate = () =>
|
Func<IDataMapper> dbCreate = () =>
|
||||||
{
|
{
|
||||||
var db = new DataMapper(_db.ProviderFactory, _db.ConnectionString);
|
var db = new DataMapper(provider, connectionString);
|
||||||
db.SqlMode = SqlModes.Text;
|
db.SqlMode = SqlModes.Text;
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace NzbDrone.Api.EpisodeFiles
|
|||||||
public string SceneName { get; set; }
|
public string SceneName { get; set; }
|
||||||
public QualityModel Quality { get; set; }
|
public QualityModel Quality { get; set; }
|
||||||
public MediaInfoResource MediaInfo { get; set; }
|
public MediaInfoResource MediaInfo { get; set; }
|
||||||
|
public string OriginalFilePath { get; set; }
|
||||||
|
|
||||||
public bool QualityCutoffNotMet { get; set; }
|
public bool QualityCutoffNotMet { get; set; }
|
||||||
}
|
}
|
||||||
@@ -38,8 +39,8 @@ namespace NzbDrone.Api.EpisodeFiles
|
|||||||
DateAdded = model.DateAdded,
|
DateAdded = model.DateAdded,
|
||||||
SceneName = model.SceneName,
|
SceneName = model.SceneName,
|
||||||
Quality = model.Quality,
|
Quality = model.Quality,
|
||||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName)
|
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||||
//QualityCutoffNotMet
|
OriginalFilePath = model.OriginalFilePath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ namespace NzbDrone.Api.EpisodeFiles
|
|||||||
Quality = model.Quality,
|
Quality = model.Quality,
|
||||||
QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality),
|
QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality),
|
||||||
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
MediaInfo = model.MediaInfo.ToResource(model.SceneName),
|
||||||
|
OriginalFilePath = model.OriginalFilePath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -30,6 +30,7 @@ namespace NzbDrone.Api.Episodes
|
|||||||
public bool UnverifiedSceneNumbering { get; set; }
|
public bool UnverifiedSceneNumbering { get; set; }
|
||||||
public string SeriesTitle { get; set; }
|
public string SeriesTitle { get; set; }
|
||||||
public SeriesResource Series { get; set; }
|
public SeriesResource Series { get; set; }
|
||||||
|
public DateTime? LastSearchTime { get; set; }
|
||||||
|
|
||||||
//Hiding this so people don't think its usable (only used to set the initial state)
|
//Hiding this so people don't think its usable (only used to set the initial state)
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
@@ -65,6 +66,7 @@ namespace NzbDrone.Api.Episodes
|
|||||||
UnverifiedSceneNumbering = model.UnverifiedSceneNumbering,
|
UnverifiedSceneNumbering = model.UnverifiedSceneNumbering,
|
||||||
SeriesTitle = model.SeriesTitle,
|
SeriesTitle = model.SeriesTitle,
|
||||||
//Series = model.Series.MapToResource(),
|
//Series = model.Series.MapToResource(),
|
||||||
|
LastSearchTime = model.LastSearchTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
@@ -24,13 +25,60 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
[TestFixture(typeof(CurlHttpDispatcher))]
|
[TestFixture(typeof(CurlHttpDispatcher))]
|
||||||
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
|
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
|
||||||
{
|
{
|
||||||
private static string[] _httpBinHosts = new[] { "eu.httpbin.org", "httpbin.org" };
|
private string[] _httpBinHosts;
|
||||||
private static int _httpBinRandom;
|
private int _httpBinSleep;
|
||||||
|
private int _httpBinRandom;
|
||||||
private string _httpBinHost;
|
private string _httpBinHost;
|
||||||
|
private string _httpBinHost2;
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public void FixtureSetUp()
|
||||||
|
{
|
||||||
|
var candidates = new[] { "eu.httpbin.org", /*"httpbin.org",*/ "www.httpbin.org" };
|
||||||
|
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
|
||||||
|
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
|
||||||
|
|
||||||
|
TestLogger.Info($"{candidates.Length} TestSites available.");
|
||||||
|
|
||||||
|
_httpBinSleep = _httpBinHosts.Count() < 2 ? 100 : 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTestSiteAvailable(string site)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var req = WebRequest.Create($"http://{site}/get") as HttpWebRequest;
|
||||||
|
var res = req.GetResponse() as HttpWebResponse;
|
||||||
|
if (res.StatusCode != HttpStatusCode.OK) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
req = WebRequest.Create($"http://{site}/status/429") as HttpWebRequest;
|
||||||
|
res = req.GetResponse() as HttpWebResponse;
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
res = ex.Response as HttpWebResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == null || res.StatusCode != (HttpStatusCode)429) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
|
if (!_httpBinHosts.Any())
|
||||||
|
{
|
||||||
|
Assert.Inconclusive("No TestSites available");
|
||||||
|
}
|
||||||
|
|
||||||
Mocker.GetMock<IPlatformInfo>().Setup(c => c.Version).Returns(new Version("1.0.0"));
|
Mocker.GetMock<IPlatformInfo>().Setup(c => c.Version).Returns(new Version("1.0.0"));
|
||||||
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
|
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
|
||||||
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
|
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
|
||||||
@@ -50,6 +98,13 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
|
|
||||||
// Roundrobin over the two servers, to reduce the chance of hitting the ratelimiter.
|
// Roundrobin over the two servers, to reduce the chance of hitting the ratelimiter.
|
||||||
_httpBinHost = _httpBinHosts[_httpBinRandom++ % _httpBinHosts.Length];
|
_httpBinHost = _httpBinHosts[_httpBinRandom++ % _httpBinHosts.Length];
|
||||||
|
_httpBinHost2 = _httpBinHosts[_httpBinRandom % _httpBinHosts.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
Thread.Sleep(_httpBinSleep);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -75,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]
|
||||||
@@ -245,7 +301,12 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
|
|
||||||
public void GivenOldCookie()
|
public void GivenOldCookie()
|
||||||
{
|
{
|
||||||
var oldRequest = new HttpRequest("http://eu.httpbin.org/get");
|
if (_httpBinHost == _httpBinHost2)
|
||||||
|
{
|
||||||
|
Assert.Inconclusive("Need both httpbin.org and eu.httpbin.org to run this test.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldRequest = new HttpRequest($"http://{_httpBinHost2}/get");
|
||||||
oldRequest.Cookies["my"] = "cookie";
|
oldRequest.Cookies["my"] = "cookie";
|
||||||
|
|
||||||
var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.GetMock<IUserAgentBuilder>().Object, Mocker.Resolve<Logger>());
|
var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.GetMock<IUserAgentBuilder>().Object, Mocker.Resolve<Logger>());
|
||||||
@@ -262,7 +323,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
{
|
{
|
||||||
GivenOldCookie();
|
GivenOldCookie();
|
||||||
|
|
||||||
var request = new HttpRequest("http://eu.httpbin.org/get");
|
var request = new HttpRequest($"http://{_httpBinHost2}/get");
|
||||||
|
|
||||||
var response = Subject.Get<HttpBinResource>(request);
|
var response = Subject.Get<HttpBinResource>(request);
|
||||||
|
|
||||||
@@ -278,7 +339,7 @@ namespace NzbDrone.Common.Test.Http
|
|||||||
{
|
{
|
||||||
GivenOldCookie();
|
GivenOldCookie();
|
||||||
|
|
||||||
var request = new HttpRequest("http://httpbin.org/get");
|
var request = new HttpRequest($"http://{_httpBinHost}/get");
|
||||||
|
|
||||||
var response = Subject.Get<HttpBinResource>(request);
|
var response = Subject.Get<HttpBinResource>(request);
|
||||||
|
|
||||||
@@ -646,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)
|
||||||
@@ -191,6 +194,24 @@ namespace NzbDrone.Common.Extensions
|
|||||||
return directories;
|
return directories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetAncestorPath(this string path, string ancestorName)
|
||||||
|
{
|
||||||
|
var parent = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
while (parent != null)
|
||||||
|
{
|
||||||
|
var currentPath = parent;
|
||||||
|
parent = Path.GetDirectoryName(parent);
|
||||||
|
|
||||||
|
if (Path.GetFileName(currentPath) == ancestorName)
|
||||||
|
{
|
||||||
|
return currentPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
return appFolderInfo.AppDataFolder;
|
return appFolderInfo.AppDataFolder;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using NLog;
|
||||||
|
using NLog.Fluent;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
using NzbDrone.Common.Security;
|
using NzbDrone.Common.Security;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http.Dispatchers
|
namespace NzbDrone.Common.Http.Dispatchers
|
||||||
@@ -12,22 +18,35 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
||||||
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
||||||
private readonly IUserAgentBuilder _userAgentBuilder;
|
private readonly IUserAgentBuilder _userAgentBuilder;
|
||||||
|
private readonly IPlatformInfo _platformInfo;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder)
|
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
|
||||||
{
|
{
|
||||||
_proxySettingsProvider = proxySettingsProvider;
|
_proxySettingsProvider = proxySettingsProvider;
|
||||||
_createManagedWebProxy = createManagedWebProxy;
|
_createManagedWebProxy = createManagedWebProxy;
|
||||||
_userAgentBuilder = userAgentBuilder;
|
_userAgentBuilder = userAgentBuilder;
|
||||||
|
_platformInfo = platformInfo;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||||
{
|
{
|
||||||
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);
|
||||||
@@ -73,6 +92,9 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
if (httpWebResponse == null)
|
if (httpWebResponse == null)
|
||||||
{
|
{
|
||||||
|
// Workaround for mono not closing connections properly in certain situations.
|
||||||
|
AbortWebRequest(webRequest);
|
||||||
|
|
||||||
// The default messages for WebException on mono are pretty horrible.
|
// The default messages for WebException on mono are pretty horrible.
|
||||||
if (e.Status == WebExceptionStatus.NameResolutionFailure)
|
if (e.Status == WebExceptionStatus.NameResolutionFailure)
|
||||||
{
|
{
|
||||||
@@ -101,11 +123,24 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
using (var responseStream = httpWebResponse.GetResponseStream())
|
using (var responseStream = httpWebResponse.GetResponseStream())
|
||||||
{
|
{
|
||||||
if (responseStream != null)
|
if (responseStream != null && responseStream != Stream.Null)
|
||||||
{
|
{
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -174,5 +209,36 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround for mono not closing connections properly on timeouts
|
||||||
|
private void AbortWebRequest(HttpWebRequest webRequest)
|
||||||
|
{
|
||||||
|
// First affected version was mono 5.16
|
||||||
|
if (OsInfo.IsNotWindows && _platformInfo.Version >= new Version(5, 16))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentOperationInfo = webRequest.GetType().GetField("currentOperation", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
var currentOperation = currentOperationInfo.GetValue(webRequest);
|
||||||
|
|
||||||
|
if (currentOperation != null)
|
||||||
|
{
|
||||||
|
var responseStreamInfo = currentOperation.GetType().GetField("responseStream", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
var responseStream = responseStreamInfo.GetValue(currentOperation) as Stream;
|
||||||
|
// Note that responseStream will likely be null once mono fixes it.
|
||||||
|
responseStream?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// This can fail randomly on future mono versions that have been changed/fixed. Log to sentry and ignore.
|
||||||
|
_logger.Trace()
|
||||||
|
.Exception(ex)
|
||||||
|
.Message("Unable to dispose responseStream on mono {0}", _platformInfo.Version)
|
||||||
|
.WriteSentryWarn("MonoCloseWaitPatchFailed", ex.Message)
|
||||||
|
.Write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ namespace NzbDrone.Common.Http
|
|||||||
public HttpResponse<T> Get<T>(HttpRequest request) where T : new()
|
public HttpResponse<T> Get<T>(HttpRequest request) where T : new()
|
||||||
{
|
{
|
||||||
var response = Get(request);
|
var response = Get(request);
|
||||||
|
CheckResponseContentType(response);
|
||||||
return new HttpResponse<T>(response);
|
return new HttpResponse<T>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +286,16 @@ namespace NzbDrone.Common.Http
|
|||||||
public HttpResponse<T> Post<T>(HttpRequest request) where T : new()
|
public HttpResponse<T> Post<T>(HttpRequest request) where T : new()
|
||||||
{
|
{
|
||||||
var response = Post(request);
|
var response = Post(request);
|
||||||
|
CheckResponseContentType(response);
|
||||||
return new HttpResponse<T>(response);
|
return new HttpResponse<T>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckResponseContentType(HttpResponse response)
|
||||||
|
{
|
||||||
|
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
|
||||||
|
{
|
||||||
|
throw new UnexpectedHtmlContentException(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ namespace NzbDrone.Common.Http
|
|||||||
public HttpRequest Request { get; private set; }
|
public HttpRequest Request { get; private set; }
|
||||||
public HttpResponse Response { get; private set; }
|
public HttpResponse Response { get; private set; }
|
||||||
|
|
||||||
public HttpException(HttpRequest request, HttpResponse response)
|
public HttpException(HttpRequest request, HttpResponse response, string message)
|
||||||
: base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url))
|
: base(message)
|
||||||
{
|
{
|
||||||
Request = request;
|
Request = request;
|
||||||
Response = response;
|
Response = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpException(HttpRequest request, HttpResponse response)
|
||||||
|
: this(request, response, string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public HttpException(HttpResponse response)
|
public HttpException(HttpResponse response)
|
||||||
: this(response.Request, response)
|
: this(response.Request, response)
|
||||||
{
|
{
|
||||||
@@ -30,4 +36,4 @@ namespace NzbDrone.Common.Http
|
|||||||
return base.ToString();
|
return base.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ namespace NzbDrone.Common.Http
|
|||||||
return new HttpUri(Scheme, Host, Port, CombinePath(Path, path), Query, Fragment);
|
return new HttpUri(Scheme, Host, Port, CombinePath(Path, path), Query, Fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CombinePath(string basePath, string relativePath)
|
public static string CombinePath(string basePath, string relativePath)
|
||||||
{
|
{
|
||||||
if (relativePath.IsNullOrWhiteSpace())
|
if (relativePath.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Http
|
||||||
|
{
|
||||||
|
public class UnexpectedHtmlContentException : HttpException
|
||||||
|
{
|
||||||
|
public UnexpectedHtmlContentException(HttpResponse response)
|
||||||
|
: base(response.Request, response, $"Site responded with browser content instead of api data. This disruption may be temporary, please try again later. [{response.Request.Url}]")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,7 @@
|
|||||||
<Compile Include="Http\HttpRequestBuilderFactory.cs" />
|
<Compile Include="Http\HttpRequestBuilderFactory.cs" />
|
||||||
<Compile Include="Http\Proxy\ProxyType.cs" />
|
<Compile Include="Http\Proxy\ProxyType.cs" />
|
||||||
<Compile Include="Http\TlsFailureException.cs" />
|
<Compile Include="Http\TlsFailureException.cs" />
|
||||||
|
<Compile Include="Http\UnexpectedHtmlContentException.cs" />
|
||||||
<Compile Include="Http\TooManyRequestsException.cs" />
|
<Compile Include="Http\TooManyRequestsException.cs" />
|
||||||
<Compile Include="Extensions\IEnumerableExtensions.cs" />
|
<Compile Include="Extensions\IEnumerableExtensions.cs" />
|
||||||
<Compile Include="Http\UserAgentBuilder.cs" />
|
<Compile Include="Http\UserAgentBuilder.cs" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
@@ -14,13 +15,13 @@ namespace NzbDrone.Common.Serializer
|
|||||||
static Json()
|
static Json()
|
||||||
{
|
{
|
||||||
SerializerSetting = new JsonSerializerSettings
|
SerializerSetting = new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
Formatting = Formatting.Indented,
|
Formatting = Formatting.Indented,
|
||||||
DefaultValueHandling = DefaultValueHandling.Include,
|
DefaultValueHandling = DefaultValueHandling.Include,
|
||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true });
|
SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true });
|
||||||
@@ -34,12 +35,61 @@ namespace NzbDrone.Common.Serializer
|
|||||||
|
|
||||||
public static T Deserialize<T>(string json) where T : new()
|
public static T Deserialize<T>(string json) where T : new()
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<T>(json, SerializerSetting);
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<T>(json, SerializerSetting);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException ex)
|
||||||
|
{
|
||||||
|
throw DetailedJsonReaderException(ex, json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object Deserialize(string json, Type type)
|
public static object Deserialize(string json, Type type)
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject(json, type, SerializerSetting);
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject(json, type, SerializerSetting);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException ex)
|
||||||
|
{
|
||||||
|
throw DetailedJsonReaderException(ex, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonReaderException DetailedJsonReaderException(JsonReaderException ex, string json)
|
||||||
|
{
|
||||||
|
var lineNumber = ex.LineNumber == 0 ? 0 : (ex.LineNumber - 1);
|
||||||
|
var linePosition = ex.LinePosition;
|
||||||
|
|
||||||
|
var lines = json.Split('\n');
|
||||||
|
if (lineNumber >= 0 && lineNumber < lines.Length &&
|
||||||
|
linePosition >= 0 && linePosition < lines[lineNumber].Length)
|
||||||
|
{
|
||||||
|
var line = lines[lineNumber];
|
||||||
|
var start = Math.Max(0, linePosition - 20);
|
||||||
|
var end = Math.Min(line.Length, linePosition + 20);
|
||||||
|
|
||||||
|
var snippetBefore = line.Substring(start, linePosition - start);
|
||||||
|
var snippetAfter = line.Substring(linePosition, end - linePosition);
|
||||||
|
var message = ex.Message + " (Json snippet '" + snippetBefore + "<--error-->" + snippetAfter + "')";
|
||||||
|
|
||||||
|
// Not risking updating JSON.net from 9.x to 10.x just to get this as public ctor.
|
||||||
|
var ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(Exception), typeof(string), typeof(int), typeof(int) }, null);
|
||||||
|
if (ctor != null)
|
||||||
|
{
|
||||||
|
return (JsonReaderException)ctor.Invoke(new object[] { message, ex, ex.Path, ex.LineNumber, linePosition });
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON.net 10.x ctor in case we update later.
|
||||||
|
ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(string), typeof(int), typeof(int), typeof(Exception) }, null);
|
||||||
|
if (ctor != null)
|
||||||
|
{
|
||||||
|
return (JsonReaderException)ctor.Invoke(new object[] { message, ex.Path, ex.LineNumber, linePosition, ex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryDeserialize<T>(string json, out T result) where T : new()
|
public static bool TryDeserialize<T>(string json, out T result) where T : new()
|
||||||
@@ -78,4 +128,4 @@ namespace NzbDrone.Common.Serializer
|
|||||||
Serialize(model, new StreamWriter(outputStream));
|
Serialize(model, new StreamWriter(outputStream));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.History;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class AlreadyImportedSpecificationFixture : CoreTest<AlreadyImportedSpecification>
|
||||||
|
{
|
||||||
|
private const int FIRST_EPISODE_ID = 1;
|
||||||
|
private const string TITLE = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
|
||||||
|
|
||||||
|
private Series _series;
|
||||||
|
private QualityModel _hdtv720p;
|
||||||
|
private QualityModel _hdtv1080p;
|
||||||
|
private RemoteEpisode _remoteEpisode;
|
||||||
|
private List<History.History> _history;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
var singleEpisodeList = new List<Episode>
|
||||||
|
{
|
||||||
|
new Episode
|
||||||
|
{
|
||||||
|
Id = FIRST_EPISODE_ID,
|
||||||
|
SeasonNumber = 12,
|
||||||
|
EpisodeNumber = 3,
|
||||||
|
EpisodeFileId = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_series = Builder<Series>.CreateNew()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_hdtv720p = new QualityModel(Quality.HDTV720p, new Revision(version: 1));
|
||||||
|
_hdtv1080p = new QualityModel(Quality.HDTV1080p, new Revision(version: 1));
|
||||||
|
|
||||||
|
_remoteEpisode = new RemoteEpisode
|
||||||
|
{
|
||||||
|
Series = _series,
|
||||||
|
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = _hdtv720p },
|
||||||
|
Episodes = singleEpisodeList,
|
||||||
|
Release = Builder<ReleaseInfo>.CreateNew()
|
||||||
|
.Build()
|
||||||
|
};
|
||||||
|
|
||||||
|
_history = new List<History.History>();
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.SetupGet(s => s.EnableCompletedDownloadHandling)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
Mocker.GetMock<IHistoryService>()
|
||||||
|
.Setup(s => s.FindByEpisodeId(It.IsAny<int>()))
|
||||||
|
.Returns(_history);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenCdhDisabled()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.SetupGet(s => s.EnableCompletedDownloadHandling)
|
||||||
|
.Returns(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenHistoryItem(string downloadId, string sourceTitle, QualityModel quality, HistoryEventType eventType)
|
||||||
|
{
|
||||||
|
_history.Add(new History.History
|
||||||
|
{
|
||||||
|
DownloadId = downloadId,
|
||||||
|
SourceTitle = sourceTitle,
|
||||||
|
Quality = quality,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
EventType = eventType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_accepted_if_CDH_is_disabled()
|
||||||
|
{
|
||||||
|
GivenCdhDisabled();
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_accepted_if_episode_does_not_have_a_file()
|
||||||
|
{
|
||||||
|
_remoteEpisode.Episodes.First().EpisodeFileId = 0;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_accepted_if_episode_does_not_have_grabbed_event()
|
||||||
|
{
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_accepted_if_episode_does_not_have_imported_event()
|
||||||
|
{
|
||||||
|
GivenHistoryItem(Guid.NewGuid().ToString().ToUpper(), TITLE, _hdtv720p, HistoryEventType.Grabbed);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_accepted_if_grabbed_and_imported_quality_is_the_same()
|
||||||
|
{
|
||||||
|
var downloadId = Guid.NewGuid().ToString().ToUpper();
|
||||||
|
|
||||||
|
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
|
||||||
|
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.DownloadFolderImported);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_rejected_if_grabbed_download_id_matches_release_torrent_hash()
|
||||||
|
{
|
||||||
|
var downloadId = Guid.NewGuid().ToString().ToUpper();
|
||||||
|
|
||||||
|
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
|
||||||
|
GivenHistoryItem(downloadId, TITLE, _hdtv1080p, HistoryEventType.DownloadFolderImported);
|
||||||
|
|
||||||
|
_remoteEpisode.Release = Builder<TorrentInfo>.CreateNew()
|
||||||
|
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
|
||||||
|
.With(t => t.InfoHash = downloadId)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_rejected_if_release_title_matches_grabbed_event_source_title()
|
||||||
|
{
|
||||||
|
var downloadId = Guid.NewGuid().ToString().ToUpper();
|
||||||
|
|
||||||
|
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
|
||||||
|
GivenHistoryItem(downloadId, TITLE, _hdtv1080p, HistoryEventType.DownloadFolderImported);
|
||||||
|
|
||||||
|
_remoteEpisode.Release = Builder<TorrentInfo>.CreateNew()
|
||||||
|
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
|
||||||
|
.With(t => t.InfoHash = downloadId)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
@@ -16,7 +16,7 @@ using NzbDrone.Core.DecisionEngine;
|
|||||||
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class HistorySpecificationFixture : CoreTest<HistorySpecification>
|
public class HistorySpecificationFixture : CoreTest<HistorySpecification>
|
||||||
@@ -137,6 +137,7 @@ namespace NzbDrone.Core.Test.DiskSpace
|
|||||||
[TestCase("/var/lib/kubelet")]
|
[TestCase("/var/lib/kubelet")]
|
||||||
[TestCase("/var/lib/docker")]
|
[TestCase("/var/lib/docker")]
|
||||||
[TestCase("/some/place/docker/aufs")]
|
[TestCase("/some/place/docker/aufs")]
|
||||||
|
[TestCase("/etc/network")]
|
||||||
public void should_not_check_diskspace_for_irrelevant_mounts(string path)
|
public void should_not_check_diskspace_for_irrelevant_mounts(string path)
|
||||||
{
|
{
|
||||||
var mount = new Mock<IMount>();
|
var mount = new Mock<IMount>();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -290,6 +290,24 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
|
|||||||
item.CanBeRemoved.Should().Be(canBeRemoved);
|
item.CanBeRemoved.Should().Be(canBeRemoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetItems_should_ignore_items_without_hash()
|
||||||
|
{
|
||||||
|
_downloading.Hash = null;
|
||||||
|
|
||||||
|
GivenTorrents(new List<DelugeTorrent>
|
||||||
|
{
|
||||||
|
_downloading,
|
||||||
|
_queued
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
items.Should().HaveCount(1);
|
||||||
|
|
||||||
|
items.First().Status.Should().Be(DownloadItemStatus.Queued);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_status_with_outputdirs()
|
public void should_return_status_with_outputdirs()
|
||||||
{
|
{
|
||||||
|
|||||||
+196
-58
@@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles.TorrentInfo;
|
|||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.Clients.QBittorrent;
|
using NzbDrone.Core.Download.Clients.QBittorrent;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
||||||
{
|
{
|
||||||
@@ -20,13 +21,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
{
|
{
|
||||||
Subject.Definition = new DownloadClientDefinition();
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
Subject.Definition.Settings = new QBittorrentSettings
|
Subject.Definition.Settings = new QBittorrentSettings
|
||||||
{
|
{
|
||||||
Host = "127.0.0.1",
|
Host = "127.0.0.1",
|
||||||
Port = 2222,
|
Port = 2222,
|
||||||
Username = "admin",
|
Username = "admin",
|
||||||
Password = "pass",
|
Password = "pass",
|
||||||
TvCategory = "tv"
|
TvCategory = "tv"
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.GetMock<ITorrentFileInfoReader>()
|
Mocker.GetMock<ITorrentFileInfoReader>()
|
||||||
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
|
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
|
||||||
@@ -37,8 +38,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
|
||||||
|
|
||||||
Mocker.GetMock<IQBittorrentProxy>()
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||||
.Returns(new QBittorrentPreferences());
|
.Returns(new QBittorrentPreferences() { DhtEnabled = true });
|
||||||
|
|
||||||
|
Mocker.GetMock<IQBittorrentProxySelector>()
|
||||||
|
.Setup(s => s.GetProxy(It.IsAny<QBittorrentSettings>(), It.IsAny<bool>()))
|
||||||
|
.Returns(Mocker.GetMock<IQBittorrentProxy>().Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void GivenRedirectToMagnet()
|
protected void GivenRedirectToMagnet()
|
||||||
@@ -95,15 +100,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First;
|
Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true)
|
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, bool removeOnMaxRatio = false)
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IQBittorrentProxy>()
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||||
.Returns(new QBittorrentPreferences
|
.Returns(new QBittorrentPreferences
|
||||||
{
|
{
|
||||||
RemoveOnMaxRatio = removeOnMaxRatio,
|
RemoveOnMaxRatio = removeOnMaxRatio,
|
||||||
MaxRatio = maxRatio
|
MaxRatio = maxRatio,
|
||||||
});
|
MaxRatioEnabled = maxRatio >= 0,
|
||||||
|
MaxSeedingTime = maxSeedingTime,
|
||||||
|
MaxSeedingTimeEnabled = maxSeedingTime >= 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
||||||
@@ -154,7 +162,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
VerifyPaused(item);
|
VerifyPaused(item);
|
||||||
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
|
item.RemainingTime.Should().NotHaveValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("pausedUP")]
|
[TestCase("pausedUP")]
|
||||||
@@ -162,6 +170,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[TestCase("uploading")]
|
[TestCase("uploading")]
|
||||||
[TestCase("stalledUP")]
|
[TestCase("stalledUP")]
|
||||||
[TestCase("checkingUP")]
|
[TestCase("checkingUP")]
|
||||||
|
[TestCase("forcedUP")]
|
||||||
public void completed_item_should_have_required_properties(string state)
|
public void completed_item_should_have_required_properties(string state)
|
||||||
{
|
{
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
@@ -184,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
[TestCase("queuedDL")]
|
[TestCase("queuedDL")]
|
||||||
[TestCase("checkingDL")]
|
[TestCase("checkingDL")]
|
||||||
|
[TestCase("metaDL")]
|
||||||
public void queued_item_should_have_required_properties(string state)
|
public void queued_item_should_have_required_properties(string state)
|
||||||
{
|
{
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
@@ -201,7 +211,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
VerifyQueued(item);
|
VerifyQueued(item);
|
||||||
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
|
item.RemainingTime.Should().NotHaveValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -243,7 +253,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
VerifyWarning(item);
|
VerifyWarning(item);
|
||||||
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
|
item.RemainingTime.Should().NotHaveValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -271,6 +281,35 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
id.Should().Be(expectedHash);
|
id.Should().Be(expectedHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_refuse_magnet_if_no_trackers_provided_and_dht_is_disabled()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
|
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||||
|
.Returns(new QBittorrentPreferences() { DhtEnabled = false });
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
remoteEpisode.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR";
|
||||||
|
|
||||||
|
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteEpisode));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Download_should_accept_magnet_if_trackers_provided_and_dht_is_disabled()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
|
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||||
|
.Returns(new QBittorrentPreferences() { DhtEnabled = false });
|
||||||
|
|
||||||
|
var remoteEpisode = CreateRemoteEpisode();
|
||||||
|
remoteEpisode.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp://abc";
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => Subject.Download(remoteEpisode));
|
||||||
|
|
||||||
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
|
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Download_should_set_top_priority()
|
public void Download_should_set_top_priority()
|
||||||
{
|
{
|
||||||
@@ -352,7 +391,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached()
|
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached()
|
||||||
{
|
{
|
||||||
GivenMaxRatio(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -373,11 +412,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
item.CanMoveFiles.Should().BeFalse();
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
protected virtual QBittorrentTorrent GivenCompletedTorrent(
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused()
|
string state = "pausedUP",
|
||||||
|
float ratio = 0.1f, float ratioLimit = -2,
|
||||||
|
int seedingTime = 1, int seedingTimeLimit = -2)
|
||||||
{
|
{
|
||||||
GivenMaxRatio(1.0f);
|
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
Hash = "HASH",
|
Hash = "HASH",
|
||||||
@@ -385,12 +424,32 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
Size = 1000,
|
Size = 1000,
|
||||||
Progress = 1.0,
|
Progress = 1.0,
|
||||||
Eta = 8640000,
|
Eta = 8640000,
|
||||||
State = "uploading",
|
State = state,
|
||||||
Label = "",
|
Label = "",
|
||||||
SavePath = "",
|
SavePath = "",
|
||||||
Ratio = 1.0f
|
Ratio = ratio,
|
||||||
|
RatioLimit = ratioLimit,
|
||||||
|
SeedingTimeLimit = seedingTimeLimit
|
||||||
};
|
};
|
||||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
|
||||||
|
GivenTorrents(new List<QBittorrentTorrent>() { torrent });
|
||||||
|
|
||||||
|
Mocker.GetMock<IQBittorrentProxy>()
|
||||||
|
.Setup(s => s.GetTorrentProperties("HASH", It.IsAny<QBittorrentSettings>()))
|
||||||
|
.Returns(new QBittorrentTorrentProperties
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
SeedingTime = seedingTime
|
||||||
|
});
|
||||||
|
|
||||||
|
return torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
GivenCompletedTorrent("uploading", ratio: 1.0f);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -400,21 +459,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
|
||||||
{
|
{
|
||||||
GivenMaxRatio(1.0f, false);
|
GivenGlobalSeedLimits(-1);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||||
var torrent = new QBittorrentTorrent
|
|
||||||
{
|
|
||||||
Hash = "HASH",
|
|
||||||
Name = _title,
|
|
||||||
Size = 1000,
|
|
||||||
Progress = 1.0,
|
|
||||||
Eta = 8640000,
|
|
||||||
State = "uploading",
|
|
||||||
Label = "",
|
|
||||||
SavePath = "",
|
|
||||||
Ratio = 1.0f
|
|
||||||
};
|
|
||||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeFalse();
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
@@ -424,21 +470,86 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
|
||||||
{
|
{
|
||||||
GivenMaxRatio(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var item = Subject.GetItems().Single();
|
||||||
{
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
Hash = "HASH",
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
Name = _title,
|
}
|
||||||
Size = 1000,
|
|
||||||
Progress = 1.0,
|
[Test]
|
||||||
Eta = 8640000,
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
|
||||||
State = "pausedUP",
|
{
|
||||||
Label = "",
|
GivenGlobalSeedLimits(2.0f);
|
||||||
SavePath = "",
|
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
|
||||||
Ratio = 1.0f
|
|
||||||
};
|
var item = Subject.GetItems().Single();
|
||||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(0.2f);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_removable_and_should_not_allow_move_files_if_max_seedingtime_reached_and_not_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(-1, 20);
|
||||||
|
GivenCompletedTorrent("uploading", ratio: 2.0f, seedingTime: 30);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(-1, 20);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(-1, 40);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
|
item.CanMoveFiles.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(-1, 20);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
item.CanBeRemoved.Should().BeFalse();
|
||||||
|
item.CanMoveFiles.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
|
||||||
|
{
|
||||||
|
GivenGlobalSeedLimits(2.0f, 20);
|
||||||
|
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
|
||||||
|
|
||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.CanBeRemoved.Should().BeTrue();
|
item.CanBeRemoved.Should().BeTrue();
|
||||||
@@ -449,7 +560,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
public void should_get_category_from_the_category_if_set()
|
public void should_get_category_from_the_category_if_set()
|
||||||
{
|
{
|
||||||
const string category = "tv-sonarr";
|
const string category = "tv-sonarr";
|
||||||
GivenMaxRatio(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -474,7 +585,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
public void should_get_category_from_the_label_if_the_category_is_not_available()
|
||||||
{
|
{
|
||||||
const string category = "tv-sonarr";
|
const string category = "tv-sonarr";
|
||||||
GivenMaxRatio(1.0f);
|
GivenGlobalSeedLimits(1.0f);
|
||||||
|
|
||||||
var torrent = new QBittorrentTorrent
|
var torrent = new QBittorrentTorrent
|
||||||
{
|
{
|
||||||
@@ -494,5 +605,32 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||||||
var item = Subject.GetItems().Single();
|
var item = Subject.GetItems().Single();
|
||||||
item.Category.Should().Be(category);
|
item.Category.Should().Be(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_handle_eta_biginteger()
|
||||||
|
{
|
||||||
|
// Let this stand as a lesson to never write temporary unit tests on your dev machine and claim it works.
|
||||||
|
// Commit the tests and let it run with the official build on the official build agents.
|
||||||
|
// (Also don't replace library versions in your build script)
|
||||||
|
|
||||||
|
var json = "{ \"eta\": 18446744073709335000 }";
|
||||||
|
var torrent = Newtonsoft.Json.JsonConvert.DeserializeObject<QBittorrentTorrent>(json);
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
|
|
||||||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||||
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>()));
|
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger));
|
||||||
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<NLog.Logger>()));
|
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
|
||||||
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
|
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
|
||||||
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
|
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
|
||||||
Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());
|
Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+43
@@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
|
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -105,5 +106,47 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
|||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Verify(v => v.GetEpisodes(folderEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
|
.Verify(v => v.GetEpisodes(folderEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_file_when_folder_is_absolute_and_file_is_not()
|
||||||
|
{
|
||||||
|
var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
|
||||||
|
var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.01");
|
||||||
|
var localEpisode = new LocalEpisode
|
||||||
|
{
|
||||||
|
FileEpisodeInfo = fileEpisodeInfo,
|
||||||
|
FolderEpisodeInfo = folderEpisodeInfo,
|
||||||
|
Path = @"C:\Test\Unsorted TV\Series.Title.101\Series.Title.S01E01.mkv".AsOsAgnostic(),
|
||||||
|
Series = _series
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.Aggregate(localEpisode, false);
|
||||||
|
|
||||||
|
Mocker.GetMock<IParsingService>()
|
||||||
|
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_special_info_when_not_null()
|
||||||
|
{
|
||||||
|
var fileEpisodeInfo = Parser.Parser.ParseTitle("S00E01");
|
||||||
|
var specialEpisodeInfo = fileEpisodeInfo.JsonClone();
|
||||||
|
|
||||||
|
var localEpisode = new LocalEpisode
|
||||||
|
{
|
||||||
|
FileEpisodeInfo = fileEpisodeInfo,
|
||||||
|
Path = @"C:\Test\TV\Series\Specials\S00E01.mkv".AsOsAgnostic(),
|
||||||
|
Series = _series
|
||||||
|
};
|
||||||
|
|
||||||
|
Mocker.GetMock<IParsingService>()
|
||||||
|
.Setup(s => s.ParseSpecialEpisodeTitle(fileEpisodeInfo, It.IsAny<string>(), _series))
|
||||||
|
.Returns(specialEpisodeInfo);
|
||||||
|
|
||||||
|
Subject.Aggregate(localEpisode, false);
|
||||||
|
|
||||||
|
Mocker.GetMock<IParsingService>()
|
||||||
|
.Verify(v => v.GetEpisodes(specialEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
private void GivenAugmentationSuccess()
|
private void GivenAugmentationSuccess()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IAugmentingService>()
|
Mocker.GetMock<IAggregationService>()
|
||||||
.Setup(s => s.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
.Setup(s => s.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
||||||
.Callback<LocalEpisode, bool>((localEpisode, otherFiles) =>
|
.Callback<LocalEpisode, bool>((localEpisode, otherFiles) =>
|
||||||
{
|
{
|
||||||
@@ -158,7 +158,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
{
|
{
|
||||||
GivenSpecifications(_pass1);
|
GivenSpecifications(_pass1);
|
||||||
|
|
||||||
Mocker.GetMock<IAugmentingService>()
|
Mocker.GetMock<IAggregationService>()
|
||||||
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
||||||
.Throws<TestException>();
|
.Throws<TestException>();
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
Subject.GetImportDecisions(_videoFiles, _series);
|
Subject.GetImportDecisions(_videoFiles, _series);
|
||||||
|
|
||||||
Mocker.GetMock<IAugmentingService>()
|
Mocker.GetMock<IAggregationService>()
|
||||||
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
|
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
|
||||||
|
|
||||||
ExceptionVerification.ExpectedErrors(3);
|
ExceptionVerification.ExpectedErrors(3);
|
||||||
@@ -195,7 +195,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
var decisions = Subject.GetImportDecisions(_videoFiles, _series);
|
var decisions = Subject.GetImportDecisions(_videoFiles, _series);
|
||||||
|
|
||||||
Mocker.GetMock<IAugmentingService>()
|
Mocker.GetMock<IAggregationService>()
|
||||||
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
|
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
|
||||||
|
|
||||||
decisions.Should().HaveCount(3);
|
decisions.Should().HaveCount(3);
|
||||||
@@ -205,7 +205,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_a_decision_when_exception_is_caught()
|
public void should_return_a_decision_when_exception_is_caught()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IAugmentingService>()
|
Mocker.GetMock<IAggregationService>()
|
||||||
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
||||||
.Throws<TestException>();
|
.Throws<TestException>();
|
||||||
|
|
||||||
|
|||||||
+7
@@ -29,6 +29,13 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_no_fileinfo_available()
|
||||||
|
{
|
||||||
|
_localEpisode.FileEpisodeInfo = null;
|
||||||
|
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_false_when_file_contains_the_full_season()
|
public void should_return_false_when_file_contains_the_full_season()
|
||||||
{
|
{
|
||||||
|
|||||||
-123
@@ -1,123 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using FizzWare.NBuilder;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Moq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Core.Download;
|
|
||||||
using NzbDrone.Core.History;
|
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
|
||||||
using NzbDrone.Core.Qualities;
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class GrabbedReleaseQualityFixture : CoreTest<GrabbedReleaseQualitySpecification>
|
|
||||||
{
|
|
||||||
private LocalEpisode _localEpisode;
|
|
||||||
private DownloadClientItem _downloadClientItem;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
_localEpisode = Builder<LocalEpisode>.CreateNew()
|
|
||||||
.With(l => l.Quality = new QualityModel(Quality.Bluray720p))
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GivenHistory(List<History.History> history)
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IHistoryService>()
|
|
||||||
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
|
|
||||||
.Returns(history);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_accepted_when_downloadClientItem_is_null()
|
|
||||||
{
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_accepted_if_no_history_for_downloadId()
|
|
||||||
{
|
|
||||||
GivenHistory(new List<History.History>());
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_accepted_if_no_grabbed_history_for_downloadId()
|
|
||||||
{
|
|
||||||
var history = Builder<History.History>.CreateListOfSize(1)
|
|
||||||
.All()
|
|
||||||
.With(h => h.EventType = HistoryEventType.Unknown)
|
|
||||||
.BuildList();
|
|
||||||
|
|
||||||
GivenHistory(history);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_accepted_if_grabbed_history_is_for_a_season_pack()
|
|
||||||
{
|
|
||||||
var history = Builder<History.History>.CreateListOfSize(1)
|
|
||||||
.All()
|
|
||||||
.With(h => h.EventType = HistoryEventType.Grabbed)
|
|
||||||
.With(h => h.Quality = _localEpisode.Quality)
|
|
||||||
.With(h => h.SourceTitle = "Series.Title.S01.720p.HDTV.x264-RlsGroup")
|
|
||||||
.BuildList();
|
|
||||||
|
|
||||||
GivenHistory(history);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_accepted_if_grabbed_history_quality_is_unknown()
|
|
||||||
{
|
|
||||||
var history = Builder<History.History>.CreateListOfSize(1)
|
|
||||||
.All()
|
|
||||||
.With(h => h.EventType = HistoryEventType.Grabbed)
|
|
||||||
.With(h => h.Quality = new QualityModel(Quality.Unknown))
|
|
||||||
.BuildList();
|
|
||||||
|
|
||||||
GivenHistory(history);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_accepted_if_grabbed_history_quality_matches()
|
|
||||||
{
|
|
||||||
var history = Builder<History.History>.CreateListOfSize(1)
|
|
||||||
.All()
|
|
||||||
.With(h => h.EventType = HistoryEventType.Grabbed)
|
|
||||||
.With(h => h.Quality = _localEpisode.Quality)
|
|
||||||
.BuildList();
|
|
||||||
|
|
||||||
GivenHistory(history);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_rejected_if_grabbed_history_quality_does_not_match()
|
|
||||||
{
|
|
||||||
var history = Builder<History.History>.CreateListOfSize(1)
|
|
||||||
.All()
|
|
||||||
.With(h => h.EventType = HistoryEventType.Grabbed)
|
|
||||||
.With(h => h.Quality = new QualityModel(Quality.HDTV720p))
|
|
||||||
.BuildList();
|
|
||||||
|
|
||||||
GivenHistory(history);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeFalse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
@@ -50,6 +50,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
|||||||
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_accepted_if_file_name_is_not_parseable()
|
||||||
|
{
|
||||||
|
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01\AFDAFD.mkv".AsOsAgnostic();
|
||||||
|
_localEpisode.FileEpisodeInfo = null;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_should_be_accepted_for_full_season()
|
public void should_should_be_accepted_for_full_season()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using FizzWare.NBuilder;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
@@ -34,6 +35,8 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
_rejectedDecisions = new List<ImportDecision>();
|
_rejectedDecisions = new List<ImportDecision>();
|
||||||
_approvedDecisions = new List<ImportDecision>();
|
_approvedDecisions = new List<ImportDecision>();
|
||||||
|
|
||||||
|
var outputPath = @"C:\Test\Unsorted\TV\30.Rock.S01E01".AsOsAgnostic();
|
||||||
|
|
||||||
var series = Builder<Series>.CreateNew()
|
var series = Builder<Series>.CreateNew()
|
||||||
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||||
.With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic())
|
.With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic())
|
||||||
@@ -66,7 +69,14 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
|
||||||
.Returns(new EpisodeFileMoveResult());
|
.Returns(new EpisodeFileMoveResult());
|
||||||
|
|
||||||
_downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
|
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
|
||||||
|
.With(d => d.OutputPath = new OsPath(outputPath))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenNewDownload()
|
||||||
|
{
|
||||||
|
_approvedDecisions.ForEach(a => a.LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), Path.GetFileName(a.LocalEpisode.Path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -140,6 +150,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_use_nzb_title_as_scene_name()
|
public void should_use_nzb_title_as_scene_name()
|
||||||
{
|
{
|
||||||
|
GivenNewDownload();
|
||||||
_downloadClientItem.Title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot";
|
_downloadClientItem.Title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot";
|
||||||
|
|
||||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
||||||
@@ -152,6 +163,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[TestCase(".nzb")]
|
[TestCase(".nzb")]
|
||||||
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
|
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
|
||||||
{
|
{
|
||||||
|
GivenNewDownload();
|
||||||
var title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot";
|
var title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot";
|
||||||
|
|
||||||
_downloadClientItem.Title = title + extension;
|
_downloadClientItem.Title = title + extension;
|
||||||
@@ -164,7 +176,8 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_use_nzb_title_as_scene_name_if_full_season()
|
public void should_not_use_nzb_title_as_scene_name_if_full_season()
|
||||||
{
|
{
|
||||||
_approvedDecisions.First().LocalEpisode.Path = "c:\\tv\\season1\\malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv".AsOsAgnostic();
|
GivenNewDownload();
|
||||||
|
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv");
|
||||||
_downloadClientItem.Title = "malcolm.in.the.middle.s02.dvdrip.xvid-ingot";
|
_downloadClientItem.Title = "malcolm.in.the.middle.s02.dvdrip.xvid-ingot";
|
||||||
|
|
||||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
||||||
@@ -175,7 +188,8 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename()
|
public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename()
|
||||||
{
|
{
|
||||||
_approvedDecisions.First().LocalEpisode.Path = "c:\\tv\\malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv".AsOsAgnostic();
|
GivenNewDownload();
|
||||||
|
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv");
|
||||||
|
|
||||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||||
|
|
||||||
@@ -185,7 +199,8 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_not_use_file_name_as_scenename_if_it_doesnt_looks_like_scenename()
|
public void should_not_use_file_name_as_scenename_if_it_doesnt_looks_like_scenename()
|
||||||
{
|
{
|
||||||
_approvedDecisions.First().LocalEpisode.Path = "c:\\tv\\aaaaa.mkv".AsOsAgnostic();
|
GivenNewDownload();
|
||||||
|
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
|
||||||
|
|
||||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
|
||||||
|
|
||||||
@@ -223,7 +238,11 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_copy_when_cannot_move_files_downloads()
|
public void should_copy_when_cannot_move_files_downloads()
|
||||||
{
|
{
|
||||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", CanMoveFiles = false});
|
GivenNewDownload();
|
||||||
|
_downloadClientItem.Title = "30.Rock.S01E01";
|
||||||
|
_downloadClientItem.CanMoveFiles = false;
|
||||||
|
|
||||||
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
||||||
|
|
||||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||||
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once());
|
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once());
|
||||||
@@ -232,10 +251,155 @@ namespace NzbDrone.Core.Test.MediaFiles
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_use_override_importmode()
|
public void should_use_override_importmode()
|
||||||
{
|
{
|
||||||
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", CanMoveFiles = false }, ImportMode.Move);
|
GivenNewDownload();
|
||||||
|
_downloadClientItem.Title = "30.Rock.S01E01";
|
||||||
|
_downloadClientItem.CanMoveFiles = false;
|
||||||
|
|
||||||
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem, ImportMode.Move);
|
||||||
|
|
||||||
Mocker.GetMock<IUpgradeMediaFiles>()
|
Mocker.GetMock<IUpgradeMediaFiles>()
|
||||||
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once());
|
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_file_name_only_for_download_client_item_without_a_job_folder()
|
||||||
|
{
|
||||||
|
var fileName = "Series.Title.S01E01.720p.HDTV.x264-Sonarr.mkv";
|
||||||
|
var path = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), fileName);
|
||||||
|
|
||||||
|
_downloadClientItem.OutputPath = new OsPath(path);
|
||||||
|
_approvedDecisions.First().LocalEpisode.Path = path;
|
||||||
|
|
||||||
|
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
|
||||||
|
|
||||||
|
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == fileName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_folder_and_file_name_only_for_download_client_item_with_a_job_folder()
|
||||||
|
{
|
||||||
|
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
|
||||||
|
var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name);
|
||||||
|
|
||||||
|
_downloadClientItem.OutputPath = new OsPath(outputPath);
|
||||||
|
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(outputPath, 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}\\{name}.mkv".AsOsAgnostic())));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_include_intermediate_folders_for_download_client_item_with_a_job_folder()
|
||||||
|
{
|
||||||
|
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
|
||||||
|
var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name);
|
||||||
|
|
||||||
|
_downloadClientItem.OutputPath = new OsPath(outputPath);
|
||||||
|
_approvedDecisions.First().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()
|
||||||
|
{
|
||||||
|
var name = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
|
||||||
|
var outputPath = Path.Combine(@"C:\Test\Unsorted\TV\".AsOsAgnostic(), name);
|
||||||
|
var localEpisode = _approvedDecisions.First().LocalEpisode;
|
||||||
|
|
||||||
|
localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo { ReleaseTitle = name };
|
||||||
|
localEpisode.Path = Path.Combine(outputPath, "subfolder", 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}\\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())));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerSearch;
|
using NzbDrone.Core.IndexerSearch;
|
||||||
using NzbDrone.Core.MediaFiles.Commands;
|
using NzbDrone.Core.MediaFiles.Commands;
|
||||||
|
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Update.Commands;
|
using NzbDrone.Core.Update.Commands;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Messaging.Commands
|
namespace NzbDrone.Core.Test.Messaging.Commands
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class CommandEqualityComparerFixture
|
public class CommandEqualityComparerFixture
|
||||||
{
|
{
|
||||||
|
private string GivenRandomPath()
|
||||||
|
{
|
||||||
|
return Path.Combine(@"C:\Tesst\", Guid.NewGuid().ToString()).AsOsAgnostic();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_when_there_are_no_properties()
|
public void should_return_true_when_there_are_no_properties()
|
||||||
{
|
{
|
||||||
@@ -107,5 +119,43 @@ namespace NzbDrone.Core.Test.Messaging.Commands
|
|||||||
|
|
||||||
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse();
|
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_when_commands_list_for_non_primitive_type_match()
|
||||||
|
{
|
||||||
|
var files1 = Builder<ManualImportFile>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(m => m.Path = GivenRandomPath())
|
||||||
|
.Build()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var files2 = files1.JsonClone();
|
||||||
|
|
||||||
|
var command1 = new ManualImportCommand { Files = files1 };
|
||||||
|
var command2 = new ManualImportCommand { Files = files2 };
|
||||||
|
|
||||||
|
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_when_commands_list_for_non_primitive_type_dont_match()
|
||||||
|
{
|
||||||
|
var files1 = Builder<ManualImportFile>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(m => m.Path = GivenRandomPath())
|
||||||
|
.Build()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var files2 = Builder<ManualImportFile>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(m => m.Path = GivenRandomPath())
|
||||||
|
.Build()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var command1 = new ManualImportCommand { Files = files1 };
|
||||||
|
var command2 = new ManualImportCommand { Files = files2 };
|
||||||
|
|
||||||
|
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
|
|||||||
[TestCase("tvdbid: 0")]
|
[TestCase("tvdbid: 0")]
|
||||||
[TestCase("tvdbid: -12")]
|
[TestCase("tvdbid: -12")]
|
||||||
[TestCase("tvdbid:289578")]
|
[TestCase("tvdbid:289578")]
|
||||||
[TestCase("adjalkwdjkalwdjklawjdlKAJD;EF")]
|
[TestCase("adjalkwdjkalwdjklawjdlKAJD")]
|
||||||
public void no_search_result(string term)
|
public void no_search_result(string term)
|
||||||
{
|
{
|
||||||
var result = Subject.SearchForNewSeries(term);
|
var result = Subject.SearchForNewSeries(term);
|
||||||
result.Should().BeEmpty();
|
result.Should().BeEmpty();
|
||||||
|
|
||||||
ExceptionVerification.IgnoreWarns();
|
ExceptionVerification.IgnoreWarns();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,8 @@
|
|||||||
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\HistorySpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\AlreadyImportedSpecificationFixture.cs" />
|
||||||
|
<Compile Include="DecisionEngineTests\RssSync\HistorySpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\MonitoredEpisodeSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\MonitoredEpisodeSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\QueueSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\QueueSpecificationFixture.cs" />
|
||||||
@@ -316,7 +317,6 @@
|
|||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\SameFileSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\SameFileSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\GrabbedReleaseQualityFixture.cs" />
|
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Love Rerun EP06 720p x265 AOZ.mp4", "Love Rerun", 6, 0, 0)]
|
[TestCase("Love Rerun EP06 720p x265 AOZ.mp4", "Love Rerun", 6, 0, 0)]
|
||||||
[TestCase("Love Rerun 2018 EP06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
|
[TestCase("Love Rerun 2018 EP06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
|
||||||
[TestCase("Love Rerun 2018 06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
|
[TestCase("Love Rerun 2018 06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
|
||||||
|
[TestCase("Boku No Hero Academia S03 - EP14 VOSTFR [1080p] [HardSub] Yass'Kun", "Boku No Hero Academia S03", 14, 0, 0)]
|
||||||
|
[TestCase("Boku No Hero Academia S3 - 15 VOSTFR [720p]", "Boku No Hero Academia S3", 15, 0, 0)]
|
||||||
|
[TestCase("Tokyo Ghoul: RE S2 - Episode 4 VOSTFR (1080p)", "Tokyo Ghoul RE S2", 4, 0, 0)]
|
||||||
|
[TestCase("To Aru Majutsu no Index III - Episode 5 VOSTFR (1080p)", "To Aru Majutsu no Index III", 5, 0, 0)]
|
||||||
|
[TestCase("[Prout] Steins;Gate 0 - Episode 5 VOSTFR (BDRip 1920x1080 x264 FLAC)", "Steins;Gate 0", 5, 0, 0)]
|
||||||
//[TestCase("", "", 0, 0, 0)]
|
//[TestCase("", "", 0, 0, 0)]
|
||||||
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
||||||
{
|
{
|
||||||
@@ -131,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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("At_Midnight_140722_720p_HDTV_x264-YesTV", "At Midnight", 2014, 07, 22)]
|
[TestCase("At_Midnight_140722_720p_HDTV_x264-YesTV", "At Midnight", 2014, 07, 22)]
|
||||||
//[TestCase("Corrie.07.01.15", "Corrie", 2015, 1, 7)]
|
//[TestCase("Corrie.07.01.15", "Corrie", 2015, 1, 7)]
|
||||||
[TestCase("The Nightly Show with Larry Wilmore 2015 02 09 WEBRIP s01e13", "The Nightly Show with Larry Wilmore", 2015, 2, 9)]
|
[TestCase("The Nightly Show with Larry Wilmore 2015 02 09 WEBRIP s01e13", "The Nightly Show with Larry Wilmore", 2015, 2, 9)]
|
||||||
|
[TestCase("Jimmy_Fallon_2018_06_22_Seth_Meyers_720p_HEVC_x265-MeGusta", "Jimmy Fallon", 2018, 6, 22)]
|
||||||
|
[TestCase("20161024- Exotic Payback.21x41_720.mkv", "", 2016, 10, 24)]
|
||||||
|
[TestCase("2018-11-14.1080.all.mp4", "", 2018, 11, 14)]
|
||||||
//[TestCase("", "", 0, 0, 0)]
|
//[TestCase("", "", 0, 0, 0)]
|
||||||
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
|
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
@@ -80,6 +80,20 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
"The Good Wife",
|
"The Good Wife",
|
||||||
Quality.HDTV720p,
|
Quality.HDTV720p,
|
||||||
"NZBgeek"
|
"NZBgeek"
|
||||||
|
},
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
@"C:\Test\Fargo.S03E04.1080p.WEB-DL.DD5.1.H264-RARBG\170424_26.mkv".AsOsAgnostic(),
|
||||||
|
"Fargo",
|
||||||
|
Quality.WEBDL1080p,
|
||||||
|
"RARBG"
|
||||||
|
},
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
@"C:\Test\XxQVHK4GJMP3n2dLpmhW\XxQVHK4GJMP3n2dLpmhW\MKV\010E70S.yhcranA.fo.snoS.mkv".AsOsAgnostic(),
|
||||||
|
"Sons of Anarchy",
|
||||||
|
Quality.HDTV720p,
|
||||||
|
null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("S01E01-E03 - Episode Title.HDTV-720p", "", 1, new [] { 1, 2, 3 })]
|
[TestCase("S01E01-E03 - Episode Title.HDTV-720p", "", 1, new [] { 1, 2, 3 })]
|
||||||
[TestCase("1x01-x03 - Episode Title.HDTV-720p", "", 1, new [] { 1, 2, 3 })]
|
[TestCase("1x01-x03 - Episode Title.HDTV-720p", "", 1, new [] { 1, 2, 3 })]
|
||||||
[TestCase("Are.You.Human.Too.E07-E08.180612.1080p-NEXT", "Are You Human Too", 1, new[] { 7, 8 })]
|
[TestCase("Are.You.Human.Too.E07-E08.180612.1080p-NEXT", "Are You Human Too", 1, new[] { 7, 8 })]
|
||||||
|
[TestCase("Are You Human Too? E11-E12 1080p HDTV AAC H.264-NEXT", "Are You Human Too", 1, new[] { 11, 12 })]
|
||||||
|
[TestCase("The Series Title (2010) - [S01E01-02-03] - Episode Title", "The Series Title (2010)", 1, new [] { 1, 2, 3 })]
|
||||||
//[TestCase("", "", , new [] { })]
|
//[TestCase("", "", , new [] { })]
|
||||||
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
|
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Hells.Kitchen.US.S12E17.HR.WS.PDTV.X264-DIMENSION", false)]
|
[TestCase("Hells.Kitchen.US.S12E17.HR.WS.PDTV.X264-DIMENSION", false)]
|
||||||
[TestCase("Survivorman.The.Lost.Pilots.Summer.HR.WS.PDTV.x264-DHD", false)]
|
[TestCase("Survivorman.The.Lost.Pilots.Summer.HR.WS.PDTV.x264-DHD", false)]
|
||||||
[TestCase("Victoria S01E07 - Motor zmen (CZ)[TvRip][HEVC][720p]", false)]
|
[TestCase("Victoria S01E07 - Motor zmen (CZ)[TvRip][HEVC][720p]", false)]
|
||||||
|
[TestCase("flashpoint.S05E06.720p.HDTV.x264-FHD", false)]
|
||||||
public void should_parse_hdtv720p_quality(string title, bool proper)
|
public void should_parse_hdtv720p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.HDTV720p, proper);
|
ParseAndVerifyQuality(title, Quality.HDTV720p, proper);
|
||||||
@@ -130,11 +131,27 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Dexter - S01E01 - Title [HDTV-1080p]", false)]
|
[TestCase("Dexter - S01E01 - Title [HDTV-1080p]", false)]
|
||||||
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", false)]
|
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", false)]
|
||||||
[TestCase("Victoria S01E07 - Motor zmen (CZ)[TvRip][HEVC][1080p]", false)]
|
[TestCase("Victoria S01E07 - Motor zmen (CZ)[TvRip][HEVC][1080p]", false)]
|
||||||
|
[TestCase("Sword Art Online Alicization 04 vostfr FHD", false)]
|
||||||
|
[TestCase("Goblin Slayer 04 vostfr FHD.mkv", false)]
|
||||||
|
[TestCase("[Onii-ChanSub] SSSS.Gridman - 02 vostfr (FHD 1080p 10bits).mkv", false)]
|
||||||
|
[TestCase("[Miaou] Akanesasu Shoujo 02 VOSTFR FHD 10 bits", false)]
|
||||||
|
[TestCase("[mhastream.com]_Episode_05_FHD.mp4", false)]
|
||||||
|
[TestCase("[Kousei]_One_Piece_ - _609_[FHD][648A87C7].mp4", false)]
|
||||||
|
[TestCase("Presunto culpable 1x02 Culpabilidad [HDTV 1080i AVC MP2 2.0 Sub][GrupoHDS]", false)]
|
||||||
|
[TestCase("Cuéntame cómo pasó - 19x15 [344] Cuarenta años de baile [HDTV 1080i AVC MP2 2.0 Sub][GrupoHDS]", false)]
|
||||||
public void should_parse_hdtv1080p_quality(string title, bool proper)
|
public void should_parse_hdtv1080p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.HDTV1080p, proper);
|
ParseAndVerifyQuality(title, Quality.HDTV1080p, proper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("My Title - S01E01 - EpTitle [HEVC 4k DTSHD-MA-6ch]", false)]
|
||||||
|
[TestCase("My Title - S01E01 - EpTitle [HEVC-4k DTSHD-MA-6ch]", false)]
|
||||||
|
[TestCase("My Title - S01E01 - EpTitle [4k HEVC DTSHD-MA-6ch]", false)]
|
||||||
|
public void should_parse_hdtv2160p_quality(string title, bool proper)
|
||||||
|
{
|
||||||
|
ParseAndVerifyQuality(title, Quality.HDTV2160p, proper);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", false)]
|
[TestCase("Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", false)]
|
||||||
[TestCase("Vanguard S01E04 Mexicos Death Train 720p WEB DL", false)]
|
[TestCase("Vanguard S01E04 Mexicos Death Train 720p WEB DL", false)]
|
||||||
[TestCase("Hawaii Five 0 S02E21 720p WEB DL DD5 1 H 264", false)]
|
[TestCase("Hawaii Five 0 S02E21 720p WEB DL DD5 1 H 264", false)]
|
||||||
@@ -178,6 +195,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Incorporated.S01E08.Das.geloeschte.Ich.German.DD51.Dubbed.DL.1080p.AmazonHD.x264-TVS", false)]
|
[TestCase("Incorporated.S01E08.Das.geloeschte.Ich.German.DD51.Dubbed.DL.1080p.AmazonHD.x264-TVS", false)]
|
||||||
[TestCase("Death.Note.2017.German.DD51.DL.1080p.NetflixHD.x264-TVS", false)]
|
[TestCase("Death.Note.2017.German.DD51.DL.1080p.NetflixHD.x264-TVS", false)]
|
||||||
[TestCase("Played.S01E08.Pro.Gamer.1440p.BKPL.WEB-DL.H.264-LiGHT", false)]
|
[TestCase("Played.S01E08.Pro.Gamer.1440p.BKPL.WEB-DL.H.264-LiGHT", false)]
|
||||||
|
[TestCase("Good.Luck.Charlie.S04E11.Teddy's.Choice.FHD.1080p.Web-DL", false)]
|
||||||
|
[TestCase("Outlander.S04E03.The.False.Bride.1080p.NF.WEB.DDP5.1.x264-NTb[rartv]", false)]
|
||||||
public void should_parse_webdl1080p_quality(string title, bool proper)
|
public void should_parse_webdl1080p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
|
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
|
||||||
@@ -210,6 +229,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[Elysium]Lucky.Star.01(BD.720p.AAC.DA)[0BB96AD8].mkv", false)]
|
[TestCase("[Elysium]Lucky.Star.01(BD.720p.AAC.DA)[0BB96AD8].mkv", false)]
|
||||||
[TestCase("Battlestar.Galactica.S01E01.33.720p.HDDVD.x264-SiNNERS.mkv", false)]
|
[TestCase("Battlestar.Galactica.S01E01.33.720p.HDDVD.x264-SiNNERS.mkv", false)]
|
||||||
[TestCase("The.Expanse.S01E07.RERIP.720p.BluRay.x264-DEMAND", true)]
|
[TestCase("The.Expanse.S01E07.RERIP.720p.BluRay.x264-DEMAND", true)]
|
||||||
|
[TestCase("Sans.Laisser.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)]
|
||||||
public void should_parse_bluray720p_quality(string title, bool proper)
|
public void should_parse_bluray720p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
|
ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
|
||||||
@@ -224,6 +244,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[Zurako] Log Horizon - 01 - The Apocalypse (BD 1080p AAC) [7AE12174].mkv", false)]
|
[TestCase("[Zurako] Log Horizon - 01 - The Apocalypse (BD 1080p AAC) [7AE12174].mkv", false)]
|
||||||
[TestCase("WEEDS.S03E01-06.DUAL.1080p.Blu-ray.AC3.-HELLYWOOD.avi", false)]
|
[TestCase("WEEDS.S03E01-06.DUAL.1080p.Blu-ray.AC3.-HELLYWOOD.avi", false)]
|
||||||
[TestCase("[Coalgirls]_Durarara!!_01_(1920x1080_Blu-ray_FLAC)_[8370CB8F].mkv", false)]
|
[TestCase("[Coalgirls]_Durarara!!_01_(1920x1080_Blu-ray_FLAC)_[8370CB8F].mkv", false)]
|
||||||
|
[TestCase("Planet.Earth.S01E11.Ocean.Deep.1080p.HD-DVD.DD.VC1-TRB", false)]
|
||||||
|
[TestCase("Spirited Away(2001) Bluray FHD Hi10P.mkv", false)]
|
||||||
public void should_parse_bluray1080p_quality(string title, bool proper)
|
public void should_parse_bluray1080p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.Bluray1080p, proper);
|
ParseAndVerifyQuality(title, Quality.Bluray1080p, proper);
|
||||||
@@ -231,6 +253,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
|
|
||||||
[TestCase("House.of.Cards.US.s05e13.4K.UHD.Bluray", false)]
|
[TestCase("House.of.Cards.US.s05e13.4K.UHD.Bluray", false)]
|
||||||
[TestCase("House.of.Cards.US.s05e13.UHD.4K.Bluray", false)]
|
[TestCase("House.of.Cards.US.s05e13.UHD.4K.Bluray", false)]
|
||||||
|
[TestCase("[DameDesuYo] Backlog Bundle - Part 1 (BD 4K 8bit FLAC)", false)]
|
||||||
public void should_parse_bluray2160p_quality(string title, bool proper)
|
public void should_parse_bluray2160p_quality(string title, bool proper)
|
||||||
{
|
{
|
||||||
ParseAndVerifyQuality(title, Quality.Bluray2160p, proper);
|
ParseAndVerifyQuality(title, Quality.Bluray2160p, proper);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using NzbDrone.Common.Extensions;
|
|||||||
using NzbDrone.Core.MetadataSource.SkyHook;
|
using NzbDrone.Core.MetadataSource.SkyHook;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.TvTests
|
namespace NzbDrone.Core.Test.TvTests
|
||||||
{
|
{
|
||||||
@@ -77,12 +78,14 @@ namespace NzbDrone.Core.Test.TvTests
|
|||||||
{
|
{
|
||||||
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
|
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
|
||||||
.Returns(new List<Episode>());
|
.Returns(new List<Episode>());
|
||||||
|
|
||||||
Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes());
|
Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes());
|
||||||
|
|
||||||
_insertedEpisodes.Should().HaveSameCount(GetEpisodes());
|
_insertedEpisodes.Should().HaveSameCount(GetEpisodes());
|
||||||
_updatedEpisodes.Should().BeEmpty();
|
_updatedEpisodes.Should().BeEmpty();
|
||||||
_deletedEpisodes.Should().BeEmpty();
|
_deletedEpisodes.Should().BeEmpty();
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedWarns(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -146,6 +149,63 @@ namespace NzbDrone.Core.Test.TvTests
|
|||||||
_updatedEpisodes.Should().OnlyContain(e => e.Monitored == true);
|
_updatedEpisodes.Should().OnlyContain(e => e.Monitored == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_set_monitored_status_for_old_episodes_to_false_if_recent_enough()
|
||||||
|
{
|
||||||
|
var series = GetSeries();
|
||||||
|
series.Seasons = new List<Season>();
|
||||||
|
series.Seasons.Add(new Season { SeasonNumber = 1, Monitored = true });
|
||||||
|
|
||||||
|
var episodes = GetEpisodes().OrderBy(v => v.SeasonNumber).ThenBy(v => v.EpisodeNumber).Take(5).ToList();
|
||||||
|
|
||||||
|
episodes[1].AirDateUtc = DateTime.UtcNow.AddDays(-15);
|
||||||
|
episodes[2].AirDateUtc = DateTime.UtcNow.AddDays(-10);
|
||||||
|
episodes[3].AirDateUtc = DateTime.UtcNow.AddDays(1);
|
||||||
|
|
||||||
|
var existingEpisodes = episodes.Skip(4).ToList();
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
|
||||||
|
.Returns(existingEpisodes);
|
||||||
|
|
||||||
|
Subject.RefreshEpisodeInfo(series, episodes);
|
||||||
|
|
||||||
|
_insertedEpisodes = _insertedEpisodes.OrderBy(v => v.EpisodeNumber).ToList();
|
||||||
|
|
||||||
|
_insertedEpisodes.Should().HaveCount(4);
|
||||||
|
_insertedEpisodes[0].Monitored.Should().Be(true);
|
||||||
|
_insertedEpisodes[1].Monitored.Should().Be(true);
|
||||||
|
_insertedEpisodes[2].Monitored.Should().Be(true);
|
||||||
|
_insertedEpisodes[3].Monitored.Should().Be(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_set_monitored_status_for_old_episodes_to_false_if_no_episodes_existed()
|
||||||
|
{
|
||||||
|
var series = GetSeries();
|
||||||
|
series.Seasons = new List<Season>();
|
||||||
|
|
||||||
|
var episodes = GetEpisodes().OrderBy(v => v.SeasonNumber).ThenBy(v => v.EpisodeNumber).Take(4).ToList();
|
||||||
|
|
||||||
|
episodes[1].AirDateUtc = DateTime.UtcNow.AddDays(-15);
|
||||||
|
episodes[2].AirDateUtc = DateTime.UtcNow.AddDays(-10);
|
||||||
|
episodes[3].AirDateUtc = DateTime.UtcNow.AddDays(1);
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
|
||||||
|
.Returns(new List<Episode>());
|
||||||
|
|
||||||
|
Subject.RefreshEpisodeInfo(series, episodes);
|
||||||
|
|
||||||
|
_insertedEpisodes = _insertedEpisodes.OrderBy(v => v.EpisodeNumber).ToList();
|
||||||
|
|
||||||
|
_insertedEpisodes.Should().HaveSameCount(episodes);
|
||||||
|
_insertedEpisodes[0].Monitored.Should().Be(false);
|
||||||
|
_insertedEpisodes[1].Monitored.Should().Be(false);
|
||||||
|
_insertedEpisodes[2].Monitored.Should().Be(false);
|
||||||
|
_insertedEpisodes[3].Monitored.Should().Be(true);
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedWarns(1);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_remove_duplicate_remote_episodes_before_processing()
|
public void should_remove_duplicate_remote_episodes_before_processing()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Test.TvTests
|
|||||||
[TestCase("A to Z", 281588, "a to z")]
|
[TestCase("A to Z", 281588, "a to z")]
|
||||||
[TestCase("A.D. The Bible Continues", 289260, "ad bible continues")]
|
[TestCase("A.D. The Bible Continues", 289260, "ad bible continues")]
|
||||||
[TestCase("A.P. Bio", 328534, "ap bio")]
|
[TestCase("A.P. Bio", 328534, "ap bio")]
|
||||||
|
[TestCase("The A-Team", 77904, "ateam")]
|
||||||
public void should_use_precomputed_title(string title, int tvdbId, string expected)
|
public void should_use_precomputed_title(string title, int tvdbId, string expected)
|
||||||
{
|
{
|
||||||
SeriesTitleNormalizer.Normalize(title, tvdbId).Should().Be(expected);
|
SeriesTitleNormalizer.Normalize(title, tvdbId).Should().Be(expected);
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
using NzbDrone.Common.EnvironmentInfo;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.History;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Analytics
|
namespace NzbDrone.Core.Analytics
|
||||||
{
|
{
|
||||||
public interface IAnalyticsService
|
public interface IAnalyticsService
|
||||||
{
|
{
|
||||||
bool IsEnabled { get; }
|
bool IsEnabled { get; }
|
||||||
|
bool InstallIsActive { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnalyticsService : IAnalyticsService
|
public class AnalyticsService : IAnalyticsService
|
||||||
{
|
{
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
private readonly IHistoryService _historyService;
|
||||||
|
|
||||||
public AnalyticsService(IConfigFileProvider configFileProvider)
|
public AnalyticsService(IHistoryService historyService, IConfigFileProvider configFileProvider)
|
||||||
{
|
{
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
|
_historyService = historyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfo.IsProduction;
|
public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfo.IsProduction;
|
||||||
|
|
||||||
|
public bool InstallIsActive
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var lastRecord = _historyService.Paged(new PagingSpec<History.History>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
|
||||||
|
var monthAgo = DateTime.UtcNow.AddMonths(-1);
|
||||||
|
|
||||||
|
return lastRecord.Records.Any(v => v.Date > monthAgo);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
var connectionBuilder = new SQLiteConnectionStringBuilder();
|
var connectionBuilder = new SQLiteConnectionStringBuilder();
|
||||||
|
|
||||||
connectionBuilder.DataSource = dbPath;
|
connectionBuilder.DataSource = dbPath;
|
||||||
connectionBuilder.CacheSize = (int)-10.Megabytes();
|
connectionBuilder.CacheSize = (int)-10000;
|
||||||
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
|
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
|
||||||
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
|
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
|
||||||
connectionBuilder.Pooling = true;
|
connectionBuilder.Pooling = true;
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(129)]
|
||||||
|
public class add_relative_original_path_to_episode_file : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("EpisodeFiles").AddColumn("OriginalFilePath").AsString().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(130)]
|
||||||
|
public class episode_last_searched_time : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("Episodes").AddColumn("LastSearchTime").AsDateTime().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.History;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||||
|
{
|
||||||
|
public class AlreadyImportedSpecification : IDecisionEngineSpecification
|
||||||
|
{
|
||||||
|
private readonly IHistoryService _historyService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public AlreadyImportedSpecification(IHistoryService historyService,
|
||||||
|
IConfigService configService,
|
||||||
|
Logger logger)
|
||||||
|
{
|
||||||
|
_historyService = historyService;
|
||||||
|
_configService = configService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||||
|
public RejectionType Type => RejectionType.Permanent;
|
||||||
|
|
||||||
|
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||||
|
{
|
||||||
|
var cdhEnabled = _configService.EnableCompletedDownloadHandling;
|
||||||
|
|
||||||
|
if (!cdhEnabled)
|
||||||
|
{
|
||||||
|
_logger.Debug("Skipping already imported check because CDH is disabled");
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Performing alerady imported check on report");
|
||||||
|
foreach (var episode in subject.Episodes)
|
||||||
|
{
|
||||||
|
if (!episode.HasFile)
|
||||||
|
{
|
||||||
|
_logger.Debug("Skipping already imported check for episode without file");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var historyForEpisode = _historyService.FindByEpisodeId(episode.Id);
|
||||||
|
var lastGrabbed = historyForEpisode.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed);
|
||||||
|
|
||||||
|
if (lastGrabbed == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imported = historyForEpisode.FirstOrDefault(h =>
|
||||||
|
h.EventType == HistoryEventType.DownloadFolderImported &&
|
||||||
|
h.DownloadId == lastGrabbed.DownloadId);
|
||||||
|
|
||||||
|
if (imported == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is really only a guard against redownloading the same release over
|
||||||
|
// and over when the grabbed and imported qualities do not match, if they do
|
||||||
|
// match skip this check.
|
||||||
|
if (lastGrabbed.Quality.Equals(imported.Quality))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var release = subject.Release;
|
||||||
|
|
||||||
|
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||||
|
{
|
||||||
|
var torrentInfo = release as TorrentInfo;
|
||||||
|
|
||||||
|
if (torrentInfo != null && torrentInfo.InfoHash.ToUpper() == lastGrabbed.DownloadId)
|
||||||
|
{
|
||||||
|
_logger.Debug("Has same torrent hash as a grabbed and imported release");
|
||||||
|
return Decision.Reject("Has same torrent hash as a grabbed and imported release");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only based on title because a release with the same title on another indexer/released at
|
||||||
|
// a different time very likely has the exact same content and we don't need to also try it.
|
||||||
|
|
||||||
|
if (release.Title.Equals(lastGrabbed.SourceTitle, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.Debug("Has same release name as a grabbed and imported release");
|
||||||
|
return Decision.Reject("Has same release name as a grabbed and imported release");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-3
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||||||
|
|
||||||
if (!subject.Series.Monitored)
|
if (!subject.Series.Monitored)
|
||||||
{
|
{
|
||||||
_logger.Debug("{0} is present in the DB but not tracked. skipping.", subject.Series);
|
_logger.Debug("{0} is present in the DB but not tracked. Rejecting", subject.Series);
|
||||||
return Decision.Reject("Series is not monitored");
|
return Decision.Reject("Series is not monitored");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +40,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||||||
return Decision.Accept();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("Only {0}/{1} episodes are monitored. skipping.", monitoredCount, subject.Episodes.Count);
|
if (subject.Episodes.Count == 1)
|
||||||
return Decision.Reject("Episode is not monitored");
|
{
|
||||||
|
_logger.Debug("Episode is not monitored. Rejecting", monitoredCount, subject.Episodes.Count);
|
||||||
|
return Decision.Reject("Episode is not monitored");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitoredCount == 0)
|
||||||
|
{
|
||||||
|
_logger.Debug("No episodes in the release are monitored. Rejecting", monitoredCount, subject.Episodes.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Only {0}/{1} episodes in the release are monitored. Rejecting", monitoredCount, subject.Episodes.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decision.Reject("One or more episodes is not monitored");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.DiskSpace
|
|||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/boot(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled);
|
private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/(boot|etc)(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled);
|
||||||
|
|
||||||
public DiskSpaceService(ISeriesService seriesService, IConfigService configService, IDiskProvider diskProvider, Logger logger)
|
public DiskSpaceService(ISeriesService seriesService, IConfigService configService, IDiskProvider diskProvider, Logger logger)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
|
|
||||||
foreach (var torrent in torrents)
|
foreach (var torrent in torrents)
|
||||||
{
|
{
|
||||||
|
if (torrent.Hash == null) continue;
|
||||||
|
|
||||||
var item = new DownloadClientItem();
|
var item = new DownloadClientItem();
|
||||||
item.DownloadId = torrent.Hash.ToUpper();
|
item.DownloadId = torrent.Hash.ToUpper();
|
||||||
item.Title = torrent.Name;
|
item.Title = torrent.Name;
|
||||||
@@ -196,7 +198,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestCategory());
|
failures.AddIfNotNull(TestCategory());
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,25 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||||||
|
|
||||||
public string GetVersion(DelugeSettings settings)
|
public string GetVersion(DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<string>(settings, "daemon.info");
|
try
|
||||||
|
{
|
||||||
|
var response = ProcessRequest<string>(settings, "daemon.info");
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
if (ex.Message.Contains("Unknown method"))
|
||||||
|
{
|
||||||
|
// Deluge v2 beta replaced 'daemon.info' with 'daemon.get_version'.
|
||||||
|
// It may return or become official, for now we just retry with the get_version api.
|
||||||
|
var response = ProcessRequest<string>(settings, "daemon.get_version");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, object> GetConfig(DelugeSettings settings)
|
public Dictionary<string, object> GetConfig(DelugeSettings settings)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -13,6 +13,7 @@ using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
|||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||||
@@ -47,6 +48,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
|
|
||||||
public override string Name => "Download Station";
|
public override string Name => "Download Station";
|
||||||
|
|
||||||
|
public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
|
||||||
|
|
||||||
protected IEnumerable<DownloadStationTask> GetTasks()
|
protected IEnumerable<DownloadStationTask> GetTasks()
|
||||||
{
|
{
|
||||||
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.BT.ToString().ToLower());
|
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.BT.ToString().ToLower());
|
||||||
@@ -191,7 +194,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestOutputPath());
|
failures.AddIfNotNull(TestOutputPath());
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using NzbDrone.Core.Configuration;
|
|||||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||||
@@ -46,6 +47,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
|
|
||||||
public override string Name => "Download Station";
|
public override string Name => "Download Station";
|
||||||
|
|
||||||
|
public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
|
||||||
|
|
||||||
protected IEnumerable<DownloadStationTask> GetTasks()
|
protected IEnumerable<DownloadStationTask> GetTasks()
|
||||||
{
|
{
|
||||||
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.NZB.ToString().ToLower());
|
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.NZB.ToString().ToLower());
|
||||||
@@ -186,7 +189,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestOutputPath());
|
failures.AddIfNotNull(TestOutputPath());
|
||||||
failures.AddIfNotNull(TestGetNZB());
|
failures.AddIfNotNull(TestGetNZB());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("TvCategory", "Category does not exist")
|
return new NzbDroneValidationFailure("TvCategory", "Category does not exist")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings),
|
||||||
DetailedDescription = "The Category your entered doesn't exist in NzbGet. Go to NzbGet to create it."
|
DetailedDescription = "The Category your entered doesn't exist in NzbGet. Go to NzbGet to create it."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -304,7 +304,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0")
|
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings),
|
||||||
DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Sonarr from seeing completed downloads."
|
DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Sonarr from seeing completed downloads."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -312,7 +312,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be less than 25000")
|
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be less than 25000")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings),
|
||||||
DetailedDescription = "NzbGet setting KeepHistory is set too high."
|
DetailedDescription = "NzbGet setting KeepHistory is set too high."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
{
|
{
|
||||||
public interface INzbgetProxy
|
public interface INzbgetProxy
|
||||||
{
|
{
|
||||||
|
string GetBaseUrl(NzbgetSettings settings, string relativePath = null);
|
||||||
string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings);
|
string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings);
|
||||||
NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings);
|
NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings);
|
||||||
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
|
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
|
||||||
@@ -36,9 +37,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
_versionCache = cacheManager.GetCache<string>(GetType(), "versions");
|
_versionCache = cacheManager.GetCache<string>(GetType(), "versions");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetBaseUrl(NzbgetSettings settings, string relativePath = null)
|
||||||
|
{
|
||||||
|
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
|
||||||
|
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
private bool HasVersion(int minimumVersion, NzbgetSettings settings)
|
private bool HasVersion(int minimumVersion, NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var versionString = _versionCache.Find(settings.Host + ":" + settings.Port) ?? GetVersion(settings);
|
var versionString = _versionCache.Find(GetBaseUrl(settings)) ?? GetVersion(settings);
|
||||||
|
|
||||||
var version = int.Parse(versionString.Split(new[] { '.', '-' })[0]);
|
var version = int.Parse(versionString.Split(new[] { '.', '-' })[0]);
|
||||||
|
|
||||||
@@ -139,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
{
|
{
|
||||||
var response = ProcessRequest<string>(settings, "version");
|
var response = ProcessRequest<string>(settings, "version");
|
||||||
|
|
||||||
_versionCache.Set(settings.Host + ":" + settings.Port, response, TimeSpan.FromDays(1));
|
_versionCache.Set(GetBaseUrl(settings), response, TimeSpan.FromDays(1));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -170,7 +179,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
queueItem = queue.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string)));
|
queueItem = queue.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string)));
|
||||||
historyItem = history.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string)));
|
historyItem = history.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queueItem != null)
|
if (queueItem != null)
|
||||||
{
|
{
|
||||||
if (!EditQueue("GroupFinalDelete", 0, "", queueItem.NzbId, settings))
|
if (!EditQueue("GroupFinalDelete", 0, "", queueItem.NzbId, settings))
|
||||||
@@ -218,7 +227,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
|
|
||||||
private T ProcessRequest<T>(NzbgetSettings settings, string method, params object[] parameters)
|
private T ProcessRequest<T>(NzbgetSettings settings, string method, params object[] parameters)
|
||||||
{
|
{
|
||||||
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, "jsonrpc");
|
var baseUrl = GetBaseUrl(settings, "jsonrpc");
|
||||||
|
|
||||||
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
|
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
|
||||||
requestBuilder.LogResponseContent = true;
|
requestBuilder.LogResponseContent = true;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -11,6 +12,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
{
|
{
|
||||||
RuleFor(c => c.Host).ValidHost();
|
RuleFor(c => c.Host).ValidHost();
|
||||||
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||||
|
RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace());
|
||||||
|
|
||||||
RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password));
|
RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password));
|
||||||
RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username));
|
RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username));
|
||||||
|
|
||||||
@@ -37,25 +40,28 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||||||
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
|
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)]
|
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
|
||||||
|
public string UrlBase { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
|
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
|
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||||
public string TvCategory { get; set; }
|
public string TvCategory { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
||||||
public int RecentTvPriority { get; set; }
|
public int RecentTvPriority { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
||||||
public int OlderTvPriority { get; set; }
|
public int OlderTvPriority { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(7, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
|
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
|
||||||
public bool AddPaused { get; set; }
|
public bool AddPaused { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
|
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
|
||||||
public bool UseSsl { get; set; }
|
public bool UseSsl { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
|
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
|
||||||
{
|
{
|
||||||
private readonly IQBittorrentProxy _proxy;
|
private readonly IQBittorrentProxySelector _proxySelector;
|
||||||
|
|
||||||
public QBittorrent(IQBittorrentProxy proxy,
|
public QBittorrent(IQBittorrentProxySelector proxySelector,
|
||||||
ITorrentFileInfoReader torrentFileInfoReader,
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
@@ -28,16 +28,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
Logger logger)
|
Logger logger)
|
||||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxySelector = proxySelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
|
||||||
|
|
||||||
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
||||||
{
|
{
|
||||||
_proxy.AddTorrentFromUrl(magnetLink, Settings);
|
if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr="))
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
Proxy.AddTorrentFromUrl(magnetLink, Settings);
|
||||||
|
|
||||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
||||||
@@ -45,23 +52,28 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||||
{
|
{
|
||||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetInitialState(hash.ToLower());
|
SetInitialState(hash.ToLower());
|
||||||
|
|
||||||
|
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
|
||||||
|
{
|
||||||
|
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
|
||||||
|
}
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
|
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
|
||||||
{
|
{
|
||||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -76,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||||
{
|
{
|
||||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -86,6 +98,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
SetInitialState(hash.ToLower());
|
SetInitialState(hash.ToLower());
|
||||||
|
|
||||||
|
if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue))
|
||||||
|
{
|
||||||
|
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings);
|
||||||
|
}
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,28 +110,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
public override IEnumerable<DownloadClientItem> GetItems()
|
public override IEnumerable<DownloadClientItem> GetItems()
|
||||||
{
|
{
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
var torrents = _proxy.GetTorrents(Settings);
|
var torrents = Proxy.GetTorrents(Settings);
|
||||||
|
|
||||||
var queueItems = new List<DownloadClientItem>();
|
var queueItems = new List<DownloadClientItem>();
|
||||||
|
|
||||||
foreach (var torrent in torrents)
|
foreach (var torrent in torrents)
|
||||||
{
|
{
|
||||||
var item = new DownloadClientItem();
|
var item = new DownloadClientItem()
|
||||||
item.DownloadId = torrent.Hash.ToUpper();
|
{
|
||||||
item.Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label;
|
DownloadId = torrent.Hash.ToUpper(),
|
||||||
item.Title = torrent.Name;
|
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
||||||
item.TotalSize = torrent.Size;
|
Title = torrent.Name,
|
||||||
item.DownloadClient = Definition.Name;
|
TotalSize = torrent.Size,
|
||||||
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
|
DownloadClient = Definition.Name,
|
||||||
item.RemainingTime = GetRemainingTime(torrent);
|
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
||||||
item.SeedRatio = torrent.Ratio;
|
RemainingTime = GetRemainingTime(torrent),
|
||||||
|
SeedRatio = torrent.Ratio,
|
||||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
|
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)),
|
||||||
|
};
|
||||||
|
|
||||||
// Avoid removing torrents that haven't reached the global max ratio.
|
// Avoid removing torrents that haven't reached the global max ratio.
|
||||||
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
||||||
item.CanMoveFiles = item.CanBeRemoved = (!config.MaxRatioEnabled || config.MaxRatio <= torrent.Ratio) && torrent.State == "pausedUP";
|
item.CanMoveFiles = item.CanBeRemoved = (torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config));
|
||||||
|
|
||||||
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
|
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
|
||||||
{
|
{
|
||||||
@@ -142,6 +160,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
case "stalledUP": // torrent is being seeded, but no connection were made
|
case "stalledUP": // torrent is being seeded, but no connection were made
|
||||||
case "queuedUP": // queuing is enabled and torrent is queued for upload
|
case "queuedUP": // queuing is enabled and torrent is queued for upload
|
||||||
case "checkingUP": // torrent has finished downloading and is being checked
|
case "checkingUP": // torrent has finished downloading and is being checked
|
||||||
|
case "forcedUP": // torrent has finished downloading and is being forcibly seeded
|
||||||
item.Status = DownloadItemStatus.Completed;
|
item.Status = DownloadItemStatus.Completed;
|
||||||
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
|
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
|
||||||
break;
|
break;
|
||||||
@@ -151,6 +170,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
item.Message = "The download is stalled with no connections";
|
item.Message = "The download is stalled with no connections";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "metaDL": // torrent magnet is being downloaded
|
||||||
|
if (config.DhtEnabled)
|
||||||
|
{
|
||||||
|
item.Status = DownloadItemStatus.Queued;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item.Status = DownloadItemStatus.Warning;
|
||||||
|
item.Message = "qBittorrent cannot resolve magnet link with DHT disabled";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "downloading": // torrent is being downloaded and data is being transfered
|
case "downloading": // torrent is being downloaded and data is being transfered
|
||||||
default: // new status in API? default to downloading
|
default: // new status in API? default to downloading
|
||||||
item.Status = DownloadItemStatus.Downloading;
|
item.Status = DownloadItemStatus.Downloading;
|
||||||
@@ -165,12 +196,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
public override void RemoveItem(string hash, bool deleteData)
|
public override void RemoveItem(string hash, bool deleteData)
|
||||||
{
|
{
|
||||||
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
Proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override DownloadClientInfo GetStatus()
|
public override DownloadClientInfo GetStatus()
|
||||||
{
|
{
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
|
|
||||||
var destDir = new OsPath(config.SavePath);
|
var destDir = new OsPath(config.SavePath);
|
||||||
|
|
||||||
@@ -184,7 +215,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestPrioritySupport());
|
failures.AddIfNotNull(TestPrioritySupport());
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
@@ -193,8 +224,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var version = _proxy.GetVersion(Settings);
|
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
|
||||||
if (version < 5)
|
if (version < Version.Parse("1.5"))
|
||||||
{
|
{
|
||||||
// API version 5 introduced the "save_path" property in /query/torrents
|
// API version 5 introduced the "save_path" property in /query/torrents
|
||||||
return new NzbDroneValidationFailure("Host", "Unsupported client version")
|
return new NzbDroneValidationFailure("Host", "Unsupported client version")
|
||||||
@@ -202,7 +233,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
|
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (version < 6)
|
else if (version < Version.Parse("1.6"))
|
||||||
{
|
{
|
||||||
// API version 6 introduced support for labels
|
// API version 6 introduced support for labels
|
||||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
@@ -224,8 +255,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Complain if qBittorrent is configured to remove torrents on max ratio
|
// Complain if qBittorrent is configured to remove torrents on max ratio
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
|
if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && config.RemoveOnMaxRatio)
|
||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
|
return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
|
||||||
{
|
{
|
||||||
@@ -274,7 +305,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = _proxy.GetConfig(Settings);
|
var config = Proxy.GetConfig(Settings);
|
||||||
|
|
||||||
if (!config.QueueingEnabled)
|
if (!config.QueueingEnabled)
|
||||||
{
|
{
|
||||||
@@ -301,7 +332,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_proxy.GetTorrents(Settings);
|
Proxy.GetTorrents(Settings);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -319,13 +350,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
switch ((QBittorrentState)Settings.InitialState)
|
switch ((QBittorrentState)Settings.InitialState)
|
||||||
{
|
{
|
||||||
case QBittorrentState.ForceStart:
|
case QBittorrentState.ForceStart:
|
||||||
_proxy.SetForceStart(hash, true, Settings);
|
Proxy.SetForceStart(hash, true, Settings);
|
||||||
break;
|
break;
|
||||||
case QBittorrentState.Start:
|
case QBittorrentState.Start:
|
||||||
_proxy.ResumeTorrent(hash, Settings);
|
Proxy.ResumeTorrent(hash, Settings);
|
||||||
break;
|
break;
|
||||||
case QBittorrentState.Pause:
|
case QBittorrentState.Pause:
|
||||||
_proxy.PauseTorrent(hash, Settings);
|
Proxy.PauseTorrent(hash, Settings);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,7 +373,53 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// qBittorrent sends eta=8640000 if unknown such as queued
|
||||||
|
if (torrent.Eta == 8640000)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return TimeSpan.FromSeconds((int)torrent.Eta);
|
return TimeSpan.FromSeconds((int)torrent.Eta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected bool HasReachedSeedLimit(QBittorrentTorrent torrent, QBittorrentPreferences config)
|
||||||
|
{
|
||||||
|
if (torrent.RatioLimit >= 0)
|
||||||
|
{
|
||||||
|
if (torrent.Ratio >= torrent.RatioLimit) return true;
|
||||||
|
}
|
||||||
|
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
||||||
|
{
|
||||||
|
if (torrent.Ratio >= config.MaxRatio) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.SeedingTimeLimit >= 0)
|
||||||
|
{
|
||||||
|
if (!torrent.SeedingTime.HasValue)
|
||||||
|
{
|
||||||
|
FetchTorrentDetails(torrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.SeedingTime >= torrent.SeedingTimeLimit) return true;
|
||||||
|
}
|
||||||
|
else if (torrent.SeedingTimeLimit == -2 && config.MaxSeedingTimeEnabled)
|
||||||
|
{
|
||||||
|
if (!torrent.SeedingTime.HasValue)
|
||||||
|
{
|
||||||
|
FetchTorrentDetails(torrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.SeedingTime >= config.MaxSeedingTime) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void FetchTorrentDetails(QBittorrentTorrent torrent)
|
||||||
|
{
|
||||||
|
var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings);
|
||||||
|
|
||||||
|
torrent.SeedingTime = torrentProperties.SeedingTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
{
|
{
|
||||||
@@ -14,10 +14,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
[JsonProperty(PropertyName = "max_ratio")]
|
[JsonProperty(PropertyName = "max_ratio")]
|
||||||
public float MaxRatio { get; set; } // Get the global share ratio limit
|
public float MaxRatio { get; set; } // Get the global share ratio limit
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "max_seeding_time_enabled")]
|
||||||
|
public bool MaxSeedingTimeEnabled { get; set; } // True if share time limit is enabled
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "max_seeding_time")]
|
||||||
|
public long MaxSeedingTime { get; set; } // Get the global share time limit in minutes
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "max_ratio_act")]
|
[JsonProperty(PropertyName = "max_ratio_act")]
|
||||||
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
|
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "queueing_enabled")]
|
[JsonProperty(PropertyName = "queueing_enabled")]
|
||||||
public bool QueueingEnabled { get; set; } = true;
|
public bool QueueingEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "dht")]
|
||||||
|
public bool DhtEnabled { get; set; } // DHT enabled (needed for more peers and magnet downloads)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
{
|
||||||
|
public interface IQBittorrentProxy
|
||||||
|
{
|
||||||
|
bool IsApiSupported(QBittorrentSettings settings);
|
||||||
|
Version GetApiVersion(QBittorrentSettings settings);
|
||||||
|
string GetVersion(QBittorrentSettings settings);
|
||||||
|
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
|
||||||
|
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
|
||||||
|
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
|
||||||
|
|
||||||
|
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
|
||||||
|
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
|
||||||
|
|
||||||
|
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
|
||||||
|
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
|
||||||
|
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
|
||||||
|
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
||||||
|
void PauseTorrent(string hash, QBittorrentSettings settings);
|
||||||
|
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
||||||
|
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IQBittorrentProxySelector
|
||||||
|
{
|
||||||
|
IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QBittorrentProxySelector : IQBittorrentProxySelector
|
||||||
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly ICached<IQBittorrentProxy> _proxyCache;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
private readonly IQBittorrentProxy _proxyV1;
|
||||||
|
private readonly IQBittorrentProxy _proxyV2;
|
||||||
|
|
||||||
|
public QBittorrentProxySelector(QBittorrentProxyV1 proxyV1,
|
||||||
|
QBittorrentProxyV2 proxyV2,
|
||||||
|
IHttpClient httpClient,
|
||||||
|
ICacheManager cacheManager,
|
||||||
|
Logger logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_proxyCache = cacheManager.GetCache<IQBittorrentProxy>(GetType());
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_proxyV1 = proxyV1;
|
||||||
|
_proxyV2 = proxyV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force)
|
||||||
|
{
|
||||||
|
var proxyKey = $"{settings.Host}_{settings.Port}";
|
||||||
|
|
||||||
|
if (force)
|
||||||
|
{
|
||||||
|
_proxyCache.Remove(proxyKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IQBittorrentProxy FetchProxy(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
if (_proxyV2.IsApiSupported(settings))
|
||||||
|
{
|
||||||
|
_logger.Trace("Using qbitTorrent API v2");
|
||||||
|
return _proxyV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_proxyV1.IsApiSupported(settings))
|
||||||
|
{
|
||||||
|
_logger.Trace("Using qbitTorrent API v1");
|
||||||
|
return _proxyV1;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DownloadClientException("Unable to determine qBittorrent API version");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-49
@@ -11,41 +11,68 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
|
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
|
||||||
|
|
||||||
public interface IQBittorrentProxy
|
public class QBittorrentProxyV1 : IQBittorrentProxy
|
||||||
{
|
|
||||||
int GetVersion(QBittorrentSettings settings);
|
|
||||||
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
|
|
||||||
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
|
|
||||||
|
|
||||||
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
|
|
||||||
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
|
|
||||||
|
|
||||||
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
|
|
||||||
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
|
|
||||||
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
|
||||||
void PauseTorrent(string hash, QBittorrentSettings settings);
|
|
||||||
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
|
||||||
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QBittorrentProxy : IQBittorrentProxy
|
|
||||||
{
|
{
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||||
|
|
||||||
public QBittorrentProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
public QBittorrentProxyV1(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetVersion(QBittorrentSettings settings)
|
public bool IsApiSupported(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
|
// We can do the api test without having to authenticate since v4.1 will return 404 on the request.
|
||||||
var request = BuildRequest(settings).Resource("/version/api");
|
var request = BuildRequest(settings).Resource("/version/api");
|
||||||
var response = ProcessRequest<int>(request, settings);
|
request.SuppressHttpError = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = _httpClient.Execute(request.Build());
|
||||||
|
|
||||||
|
// Version request will return 404 if it doesn't exist.
|
||||||
|
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.HasHttpError)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Version GetApiVersion(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
// Version request does not require authentication and will return 404 if it doesn't exist.
|
||||||
|
var request = BuildRequest(settings).Resource("/version/api");
|
||||||
|
var response = Version.Parse("1." + ProcessRequest(request, settings));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetVersion(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
// Version request does not require authentication.
|
||||||
|
var request = BuildRequest(settings).Resource("/version/qbittorrent");
|
||||||
|
var response = ProcessRequest(request, settings).TrimStart('v');
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -60,15 +87,25 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/query/torrents")
|
var request = BuildRequest(settings).Resource("/query/torrents");
|
||||||
.AddQueryParam("label", settings.TvCategory)
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
.AddQueryParam("category", settings.TvCategory);
|
{
|
||||||
|
request.AddQueryParam("label", settings.TvCategory);
|
||||||
|
request.AddQueryParam("category", settings.TvCategory);
|
||||||
|
}
|
||||||
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
|
||||||
|
var response = ProcessRequest<QBittorrentTorrentProperties>(request, settings);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/command/download")
|
var request = BuildRequest(settings).Resource("/command/download")
|
||||||
@@ -107,7 +144,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
|
|
||||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||||
{
|
{
|
||||||
request.AddFormParameter("paused", true);
|
request.AddFormParameter("paused", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = ProcessRequest(request, settings);
|
var result = ProcessRequest(request, settings);
|
||||||
@@ -122,8 +159,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
|
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hashes", hash);
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
@@ -138,7 +175,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
ProcessRequest(setCategoryRequest, settings);
|
ProcessRequest(setCategoryRequest, settings);
|
||||||
}
|
}
|
||||||
catch(DownloadClientException ex)
|
catch (DownloadClientException ex)
|
||||||
{
|
{
|
||||||
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
|
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
|
||||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||||
@@ -153,12 +190,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
// Not supported on api v1
|
||||||
|
}
|
||||||
|
|
||||||
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/command/topPrio")
|
var request = BuildRequest(settings).Resource("/command/topPrio")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hashes", hash);
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
@@ -166,7 +207,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
catch (DownloadClientException ex)
|
catch (DownloadClientException ex)
|
||||||
{
|
{
|
||||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||||
#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden?
|
|
||||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -180,9 +220,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(settings).Resource("/command/pause")
|
var request = BuildRequest(settings).Resource("/command/pause")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hash", hash);
|
.AddFormParameter("hash", hash);
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +230,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
var request = BuildRequest(settings).Resource("/command/resume")
|
var request = BuildRequest(settings).Resource("/command/resume")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hash", hash);
|
.AddFormParameter("hash", hash);
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,17 +238,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("hashes", hash)
|
.AddFormParameter("hashes", hash)
|
||||||
.AddFormParameter("value", enabled ? "true": "false");
|
.AddFormParameter("value", enabled ? "true" : "false");
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
ProcessRequest(request, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port);
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
|
||||||
requestBuilder.LogResponseContent = true;
|
{
|
||||||
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
LogResponseContent = true,
|
||||||
|
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
|
||||||
|
};
|
||||||
return requestBuilder;
|
return requestBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,11 +312,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
{
|
{
|
||||||
_authCookieCache.Remove(authKey);
|
_authCookieCache.Remove(authKey);
|
||||||
|
|
||||||
var authLoginRequest = BuildRequest(settings).Resource("/login")
|
var authLoginRequest = BuildRequest(settings).Resource( "/login")
|
||||||
.Post()
|
.Post()
|
||||||
.AddFormParameter("username", settings.Username ?? string.Empty)
|
.AddFormParameter("username", settings.Username ?? string.Empty)
|
||||||
.AddFormParameter("password", settings.Password ?? string.Empty)
|
.AddFormParameter("password", settings.Password ?? string.Empty)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
try
|
try
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
{
|
||||||
|
// API https://github.com/qbittorrent/qBittorrent/wiki/Web-API-Documentation
|
||||||
|
|
||||||
|
public class QBittorrentProxyV2 : IQBittorrentProxy
|
||||||
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||||
|
|
||||||
|
public QBittorrentProxyV2(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsApiSupported(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
// We can do the api test without having to authenticate since v3.2.0-v4.0.4 will return 404 on the request.
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/app/webapiVersion");
|
||||||
|
request.SuppressHttpError = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = _httpClient.Execute(request.Build());
|
||||||
|
|
||||||
|
// Version request will return 404 if it doesn't exist.
|
||||||
|
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.HasHttpError)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Version GetApiVersion(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/app/webapiVersion");
|
||||||
|
var response = Version.Parse(ProcessRequest(request, settings));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetVersion(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/app/version");
|
||||||
|
var response = ProcessRequest(request, settings).TrimStart('v');
|
||||||
|
|
||||||
|
// eg "4.2alpha"
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QBittorrentPreferences GetConfig(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/app/preferences");
|
||||||
|
var response = ProcessRequest<QBittorrentPreferences>(request, settings);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/info");
|
||||||
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
request.AddQueryParam("category", settings.TvCategory);
|
||||||
|
}
|
||||||
|
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/properties")
|
||||||
|
.AddQueryParam("hash", hash);
|
||||||
|
var response = ProcessRequest<QBittorrentTorrentProperties>(request, settings);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("urls", torrentUrl);
|
||||||
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
request.AddFormParameter("category", settings.TvCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("paused", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ProcessRequest(request, settings);
|
||||||
|
|
||||||
|
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here.
|
||||||
|
if (result == "Fails.")
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Download client failed to add torrent by url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||||
|
.Post()
|
||||||
|
.AddFormUpload("torrents", fileName, fileContent);
|
||||||
|
|
||||||
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
request.AddFormParameter("category", settings.TvCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("paused", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ProcessRequest(request, settings);
|
||||||
|
|
||||||
|
// Note: Current qbit versions return nothing, so we can't do != "Ok." here.
|
||||||
|
if (result == "Fails.")
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Download client failed to add torrent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/delete")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
|
if (removeData)
|
||||||
|
{
|
||||||
|
request.AddFormParameter("deleteFiles", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/setCategory")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash)
|
||||||
|
.AddFormParameter("category", label);
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2;
|
||||||
|
var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2;
|
||||||
|
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash)
|
||||||
|
.AddFormParameter("ratioLimit", ratioLimit)
|
||||||
|
.AddFormParameter("seedingTimeLimit", seedingTimeLimit);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
// setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0
|
||||||
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/topPrio")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||||
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash);
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash);
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash)
|
||||||
|
.AddFormParameter("value", enabled ? "true" : "false");
|
||||||
|
ProcessRequest(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
|
||||||
|
{
|
||||||
|
LogResponseContent = true,
|
||||||
|
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
|
||||||
|
};
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
||||||
|
where TResult : new()
|
||||||
|
{
|
||||||
|
var responseContent = ProcessRequest(requestBuilder, settings);
|
||||||
|
|
||||||
|
return Json.Deserialize<TResult>(responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
AuthenticateClient(requestBuilder, settings);
|
||||||
|
|
||||||
|
var request = requestBuilder.Build();
|
||||||
|
request.LogResponseContent = true;
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(request);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
_logger.Debug("Authentication required, logging in.");
|
||||||
|
|
||||||
|
AuthenticateClient(requestBuilder, settings, true);
|
||||||
|
|
||||||
|
request = requestBuilder.Build();
|
||||||
|
|
||||||
|
response = _httpClient.Execute(request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
|
||||||
|
{
|
||||||
|
if (settings.Username.IsNullOrWhiteSpace() || settings.Password.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
if (reauthenticate)
|
||||||
|
{
|
||||||
|
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||||
|
|
||||||
|
var cookies = _authCookieCache.Find(authKey);
|
||||||
|
|
||||||
|
if (cookies == null || reauthenticate)
|
||||||
|
{
|
||||||
|
_authCookieCache.Remove(authKey);
|
||||||
|
|
||||||
|
var authLoginRequest = BuildRequest(settings).Resource("/api/v2/auth/login")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("username", settings.Username ?? string.Empty)
|
||||||
|
.AddFormParameter("password", settings.Password ?? string.Empty)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(authLoginRequest);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
_logger.Debug("qbitTorrent authentication failed.");
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Content != "Ok.") // returns "Fails." on bad login
|
||||||
|
{
|
||||||
|
_logger.Debug("qbitTorrent authentication failed.");
|
||||||
|
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("qBittorrent authentication succeeded.");
|
||||||
|
|
||||||
|
cookies = response.GetCookies();
|
||||||
|
|
||||||
|
_authCookieCache.Set(authKey, cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBuilder.SetCookies(cookies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
@@ -25,5 +25,22 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||||||
public string SavePath { get; set; } // Torrent save path
|
public string SavePath { get; set; } // Torrent save path
|
||||||
|
|
||||||
public float Ratio { get; set; } // Torrent share ratio
|
public float Ratio { get; set; } // Torrent share ratio
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "ratio_limit")] // Per torrent seeding ratio limit (-2 = use global, -1 = unlimited)
|
||||||
|
public float RatioLimit { get; set; } = -2;
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "seeding_time")]
|
||||||
|
public long? SeedingTime { get; set; } // Torrent seeding time (not provided by the list api)
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited)
|
||||||
|
public long SeedingTimeLimit { get; set; } = -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QBittorrentTorrentProperties
|
||||||
|
{
|
||||||
|
public string Hash { get; set; } // Torrent hash
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "seeding_time")]
|
||||||
|
public long SeedingTime { get; set; } // Torrent seeding time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("", "Disable 'Check before download' option in Sabnbzd")
|
return new NzbDroneValidationFailure("", "Disable 'Check before download' option in Sabnbzd")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/switches/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings, "config/switches/"),
|
||||||
DetailedDescription = "Using Check before download affects Sonarr ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective."
|
DetailedDescription = "Using Check before download affects Sonarr ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -438,7 +438,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("TvCategory", "Enable Job folders")
|
return new NzbDroneValidationFailure("TvCategory", "Enable Job folders")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/categories/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
|
||||||
DetailedDescription = "Sonarr prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it."
|
DetailedDescription = "Sonarr prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -449,7 +449,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("TvCategory", "Category does not exist")
|
return new NzbDroneValidationFailure("TvCategory", "Category does not exist")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/categories/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
|
||||||
DetailedDescription = "The Category your entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
|
DetailedDescription = "The Category your entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -458,7 +458,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("TvCategory", "Disable TV Sorting")
|
return new NzbDroneValidationFailure("TvCategory", "Disable TV Sorting")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"),
|
||||||
DetailedDescription = "You must disable Sabnzbd TV Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
|
DetailedDescription = "You must disable Sabnzbd TV Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("TvCategory", "Disable Movie Sorting")
|
return new NzbDroneValidationFailure("TvCategory", "Disable Movie Sorting")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"),
|
||||||
DetailedDescription = "You must disable Sabnzbd Movie Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
|
DetailedDescription = "You must disable Sabnzbd Movie Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -474,7 +474,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
return new NzbDroneValidationFailure("TvCategory", "Disable Date Sorting")
|
return new NzbDroneValidationFailure("TvCategory", "Disable Date Sorting")
|
||||||
{
|
{
|
||||||
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port),
|
InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"),
|
||||||
DetailedDescription = "You must disable Sabnzbd Date Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
|
DetailedDescription = "You must disable Sabnzbd Date Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
public interface ISabnzbdProxy
|
public interface ISabnzbdProxy
|
||||||
{
|
{
|
||||||
|
string GetBaseUrl(SabnzbdSettings settings, string relativePath = null);
|
||||||
SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings);
|
SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings);
|
||||||
void RemoveFrom(string source, string id,bool deleteData, SabnzbdSettings settings);
|
void RemoveFrom(string source, string id,bool deleteData, SabnzbdSettings settings);
|
||||||
string GetVersion(SabnzbdSettings settings);
|
string GetVersion(SabnzbdSettings settings);
|
||||||
@@ -32,6 +33,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetBaseUrl(SabnzbdSettings settings, string relativePath = null)
|
||||||
|
{
|
||||||
|
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
|
||||||
|
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings)
|
public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest("addfile", settings).Post();
|
var request = BuildRequest("addfile", settings).Post();
|
||||||
@@ -140,10 +149,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
|
|
||||||
private HttpRequestBuilder BuildRequest(string mode, SabnzbdSettings settings)
|
private HttpRequestBuilder BuildRequest(string mode, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var baseUrl = string.Format(@"{0}://{1}:{2}/api",
|
var baseUrl = GetBaseUrl(settings, "api");
|
||||||
settings.UseSsl ? "https" : "http",
|
|
||||||
settings.Host,
|
|
||||||
settings.Port);
|
|
||||||
|
|
||||||
var requestBuilder = new HttpRequestBuilder(baseUrl)
|
var requestBuilder = new HttpRequestBuilder(baseUrl)
|
||||||
.Accept(HttpAccept.Json)
|
.Accept(HttpAccept.Json)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -11,6 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
{
|
{
|
||||||
RuleFor(c => c.Host).ValidHost();
|
RuleFor(c => c.Host).ValidHost();
|
||||||
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||||
|
RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace());
|
||||||
|
|
||||||
RuleFor(c => c.ApiKey).NotEmpty()
|
RuleFor(c => c.ApiKey).NotEmpty()
|
||||||
.WithMessage("API Key is required when username/password are not configured")
|
.WithMessage("API Key is required when username/password are not configured")
|
||||||
@@ -49,25 +51,28 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||||||
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
|
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "API Key", Type = FieldType.Textbox)]
|
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")]
|
||||||
|
public string UrlBase { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox)]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)]
|
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox)]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
|
[FieldDefinition(5, Label = "Password", Type = FieldType.Password)]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
|
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
|
||||||
public string TvCategory { get; set; }
|
public string TvCategory { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
||||||
public int RecentTvPriority { get; set; }
|
public int RecentTvPriority { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
||||||
public int OlderTvPriority { get; set; }
|
public int OlderTvPriority { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
|
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
|
||||||
public bool UseSsl { get; set; }
|
public bool UseSsl { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
"errorString",
|
"errorString",
|
||||||
"uploadedEver",
|
"uploadedEver",
|
||||||
"downloadedEver",
|
"downloadedEver",
|
||||||
"seedRatioLimit"
|
"seedRatioLimit",
|
||||||
|
"fileCount"
|
||||||
};
|
};
|
||||||
|
|
||||||
var arguments = new Dictionary<string, object>();
|
var arguments = new Dictionary<string, object>();
|
||||||
|
|||||||
@@ -3,31 +3,19 @@
|
|||||||
public class TransmissionTorrent
|
public class TransmissionTorrent
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string HashString { get; set; }
|
public string HashString { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public string DownloadDir { get; set; }
|
public string DownloadDir { get; set; }
|
||||||
|
|
||||||
public long TotalSize { get; set; }
|
public long TotalSize { get; set; }
|
||||||
|
|
||||||
public long LeftUntilDone { get; set; }
|
public long LeftUntilDone { get; set; }
|
||||||
|
|
||||||
public bool IsFinished { get; set; }
|
public bool IsFinished { get; set; }
|
||||||
|
|
||||||
public int Eta { get; set; }
|
public int Eta { get; set; }
|
||||||
|
|
||||||
public TransmissionTorrentStatus Status { get; set; }
|
public TransmissionTorrentStatus Status { get; set; }
|
||||||
|
|
||||||
public int SecondsDownloading { get; set; }
|
public int SecondsDownloading { get; set; }
|
||||||
|
|
||||||
public string ErrorString { get; set; }
|
public string ErrorString { get; set; }
|
||||||
|
|
||||||
public long DownloadedEver { get; set; }
|
public long DownloadedEver { get; set; }
|
||||||
|
|
||||||
public long UploadedEver { get; set; }
|
public long UploadedEver { get; set; }
|
||||||
|
|
||||||
public long SeedRatioLimit { get; set; }
|
public long SeedRatioLimit { get; set; }
|
||||||
|
public int FileCount { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Download.Clients.Vuze
|
|||||||
// - A multi-file torrent is downloaded in a job folder and 'outputPath' points to that directory directly.
|
// - A multi-file torrent is downloaded in a job folder and 'outputPath' points to that directory directly.
|
||||||
// - A single-file torrent is downloaded in the root folder and 'outputPath' poinst to that root folder.
|
// - A single-file torrent is downloaded in the root folder and 'outputPath' poinst to that root folder.
|
||||||
// We have to make sure the return value points to the job folder OR file.
|
// We have to make sure the return value points to the job folder OR file.
|
||||||
if (outputPath == null || outputPath.FileName == torrent.Name)
|
if (outputPath == null || outputPath.FileName == torrent.Name || torrent.FileCount > 1)
|
||||||
{
|
{
|
||||||
_logger.Trace("Vuze output directory: {0}", outputPath);
|
_logger.Trace("Vuze output directory: {0}", outputPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
foreach (RTorrentTorrent torrent in torrents)
|
foreach (RTorrentTorrent torrent in torrents)
|
||||||
{
|
{
|
||||||
// Don't concern ourselves with categories other than specified
|
// Don't concern ourselves with categories other than specified
|
||||||
if (torrent.Category != Settings.TvCategory) continue;
|
if (Settings.TvCategory.IsNotNullOrWhiteSpace() && torrent.Category != Settings.TvCategory) continue;
|
||||||
|
|
||||||
if (torrent.Path.StartsWith("."))
|
if (torrent.Path.StartsWith("."))
|
||||||
{
|
{
|
||||||
throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent.");
|
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = new DownloadClientItem();
|
var item = new DownloadClientItem();
|
||||||
@@ -163,7 +163,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
failures.AddIfNotNull(TestDirectory());
|
failures.AddIfNotNull(TestDirectory());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
{
|
{
|
||||||
public enum RTorrentPriority
|
public enum RTorrentPriority
|
||||||
{
|
{
|
||||||
DoNotDownload = 0,
|
VeryLow = 0,
|
||||||
Low = 1,
|
Low = 1,
|
||||||
Normal = 2,
|
Normal = 2,
|
||||||
High = 3
|
High = 3
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -26,9 +26,15 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
[XmlRpcMethod("d.multicall2")]
|
[XmlRpcMethod("d.multicall2")]
|
||||||
object[] TorrentMulticall(params string[] parameters);
|
object[] TorrentMulticall(params string[] parameters);
|
||||||
|
|
||||||
|
[XmlRpcMethod("load.normal")]
|
||||||
|
int LoadNormal(string target, string data, params string[] commands);
|
||||||
|
|
||||||
[XmlRpcMethod("load.start")]
|
[XmlRpcMethod("load.start")]
|
||||||
int LoadStart(string target, string data, params string[] commands);
|
int LoadStart(string target, string data, params string[] commands);
|
||||||
|
|
||||||
|
[XmlRpcMethod("load.raw")]
|
||||||
|
int LoadRaw(string target, byte[] data, params string[] commands);
|
||||||
|
|
||||||
[XmlRpcMethod("load.raw_start")]
|
[XmlRpcMethod("load.raw_start")]
|
||||||
int LoadRawStart(string target, byte[] data, params string[] commands);
|
int LoadRawStart(string target, byte[] data, params string[] commands);
|
||||||
|
|
||||||
@@ -107,10 +113,20 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
|
|
||||||
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
|
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
|
||||||
{
|
{
|
||||||
_logger.Debug("Executing remote method: load.normal");
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
var client = BuildClient(settings);
|
||||||
var response = ExecuteRequest(() => client.LoadStart("", torrentUrl, GetCommands(label, priority, directory)));
|
var response = ExecuteRequest(() =>
|
||||||
|
{
|
||||||
|
if (settings.AddStopped)
|
||||||
|
{
|
||||||
|
_logger.Debug("Executing remote method: load.normal");
|
||||||
|
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Executing remote method: load.start");
|
||||||
|
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response != 0)
|
if (response != 0)
|
||||||
{
|
{
|
||||||
@@ -120,10 +136,20 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
|
|
||||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
|
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
|
||||||
{
|
{
|
||||||
_logger.Debug("Executing remote method: load.raw");
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
var client = BuildClient(settings);
|
||||||
var response = ExecuteRequest(() => client.LoadRawStart("", fileContent, GetCommands(label, priority, directory)));
|
var response = ExecuteRequest(() =>
|
||||||
|
{
|
||||||
|
if (settings.AddStopped)
|
||||||
|
{
|
||||||
|
_logger.Debug("Executing remote method: load.raw");
|
||||||
|
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Executing remote method: load.raw_start");
|
||||||
|
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response != 0)
|
if (response != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@@ -61,6 +61,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||||||
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
||||||
public int OlderTvPriority { get; set; }
|
public int OlderTvPriority { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(10, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")]
|
||||||
|
public bool AddStopped { get; set; }
|
||||||
|
|
||||||
public NzbDroneValidationResult Validate()
|
public NzbDroneValidationResult Validate()
|
||||||
{
|
{
|
||||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
failures.AddIfNotNull(TestConnection());
|
failures.AddIfNotNull(TestConnection());
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestGetTorrents());
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -163,6 +163,18 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
|||||||
tvShow.Add(new XElement("mpaa", series.Certification));
|
tvShow.Add(new XElement("mpaa", series.Certification));
|
||||||
tvShow.Add(new XElement("id", series.TvdbId));
|
tvShow.Add(new XElement("id", series.TvdbId));
|
||||||
|
|
||||||
|
var uniqueId = new XElement("uniqueid", series.TvdbId);
|
||||||
|
uniqueId.SetAttributeValue("type", "tvdb");
|
||||||
|
uniqueId.SetAttributeValue("default", true);
|
||||||
|
tvShow.Add(uniqueId);
|
||||||
|
|
||||||
|
if (series.ImdbId.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var imdbId = new XElement("uniqueid", series.ImdbId);
|
||||||
|
imdbId.SetAttributeValue("type", "imdb");
|
||||||
|
tvShow.Add(imdbId);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var genre in series.Genres)
|
foreach (var genre in series.Genres)
|
||||||
{
|
{
|
||||||
tvShow.Add(new XElement("genre", genre));
|
tvShow.Add(new XElement("genre", genre));
|
||||||
|
|||||||
@@ -16,18 +16,18 @@ namespace NzbDrone.Core.Extras.Metadata
|
|||||||
public class ExistingMetadataImporter : ImportExistingExtraFilesBase<MetadataFile>
|
public class ExistingMetadataImporter : ImportExistingExtraFilesBase<MetadataFile>
|
||||||
{
|
{
|
||||||
private readonly IExtraFileService<MetadataFile> _metadataFileService;
|
private readonly IExtraFileService<MetadataFile> _metadataFileService;
|
||||||
private readonly IAugmentingService _augmentingService;
|
private readonly IAggregationService _aggregationService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly List<IMetadata> _consumers;
|
private readonly List<IMetadata> _consumers;
|
||||||
|
|
||||||
public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileService,
|
public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileService,
|
||||||
IEnumerable<IMetadata> consumers,
|
IEnumerable<IMetadata> consumers,
|
||||||
IAugmentingService augmentingService,
|
IAggregationService aggregationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(metadataFileService)
|
: base(metadataFileService)
|
||||||
{
|
{
|
||||||
_metadataFileService = metadataFileService;
|
_metadataFileService = metadataFileService;
|
||||||
_augmentingService = augmentingService;
|
_aggregationService = aggregationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_consumers = consumers.ToList();
|
_consumers = consumers.ToList();
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Extras.Metadata
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_augmentingService.Augment(localEpisode, false);
|
_aggregationService.Augment(localEpisode, false);
|
||||||
}
|
}
|
||||||
catch (AugmentingFailedException ex)
|
catch (AugmentingFailedException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ namespace NzbDrone.Core.Extras.Others
|
|||||||
public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase<OtherExtraFile>
|
public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase<OtherExtraFile>
|
||||||
{
|
{
|
||||||
private readonly IExtraFileService<OtherExtraFile> _otherExtraFileService;
|
private readonly IExtraFileService<OtherExtraFile> _otherExtraFileService;
|
||||||
private readonly IAugmentingService _augmentingService;
|
private readonly IAggregationService _aggregationService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFileService,
|
public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFileService,
|
||||||
IAugmentingService augmentingService,
|
IAggregationService aggregationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(otherExtraFileService)
|
: base(otherExtraFileService)
|
||||||
{
|
{
|
||||||
_otherExtraFileService = otherExtraFileService;
|
_otherExtraFileService = otherExtraFileService;
|
||||||
_augmentingService = augmentingService;
|
_aggregationService = aggregationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Extras.Others
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_augmentingService.Augment(localEpisode, false);
|
_aggregationService.Augment(localEpisode, false);
|
||||||
}
|
}
|
||||||
catch (AugmentingFailedException ex)
|
catch (AugmentingFailedException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ namespace NzbDrone.Core.Extras.Subtitles
|
|||||||
public class ExistingSubtitleImporter : ImportExistingExtraFilesBase<SubtitleFile>
|
public class ExistingSubtitleImporter : ImportExistingExtraFilesBase<SubtitleFile>
|
||||||
{
|
{
|
||||||
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
|
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
|
||||||
private readonly IAugmentingService _augmentingService;
|
private readonly IAggregationService _aggregationService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ExistingSubtitleImporter(IExtraFileService<SubtitleFile> subtitleFileService,
|
public ExistingSubtitleImporter(IExtraFileService<SubtitleFile> subtitleFileService,
|
||||||
IAugmentingService augmentingService,
|
IAggregationService aggregationService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base (subtitleFileService)
|
: base (subtitleFileService)
|
||||||
{
|
{
|
||||||
_subtitleFileService = subtitleFileService;
|
_subtitleFileService = subtitleFileService;
|
||||||
_augmentingService = augmentingService;
|
_aggregationService = aggregationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Extras.Subtitles
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_augmentingService.Augment(localEpisode, false);
|
_aggregationService.Augment(localEpisode, false);
|
||||||
}
|
}
|
||||||
catch (AugmentingFailedException ex)
|
catch (AugmentingFailedException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Extras.Subtitles
|
namespace NzbDrone.Core.Extras.Subtitles
|
||||||
@@ -24,7 +24,8 @@ namespace NzbDrone.Core.Extras.Subtitles
|
|||||||
".txt",
|
".txt",
|
||||||
".utf",
|
".utf",
|
||||||
".utf8",
|
".utf8",
|
||||||
".utf-8"
|
".utf-8",
|
||||||
|
".vtt"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace NzbDrone.Core.History
|
|||||||
{
|
{
|
||||||
List<QualityModel> GetBestQualityInHistory(int episodeId);
|
List<QualityModel> GetBestQualityInHistory(int episodeId);
|
||||||
History MostRecentForEpisode(int episodeId);
|
History MostRecentForEpisode(int episodeId);
|
||||||
|
List<History> FindByEpisodeId(int episodeId);
|
||||||
History MostRecentForDownloadId(string downloadId);
|
History MostRecentForDownloadId(string downloadId);
|
||||||
List<History> FindByDownloadId(string downloadId);
|
List<History> FindByDownloadId(string downloadId);
|
||||||
List<History> FindDownloadHistory(int idSeriesId, QualityModel quality);
|
List<History> FindDownloadHistory(int idSeriesId, QualityModel quality);
|
||||||
@@ -43,6 +44,13 @@ namespace NzbDrone.Core.History
|
|||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<History> FindByEpisodeId(int episodeId)
|
||||||
|
{
|
||||||
|
return Query.Where(h => h.EpisodeId == episodeId)
|
||||||
|
.OrderByDescending(h => h.Date)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public History MostRecentForDownloadId(string downloadId)
|
public History MostRecentForDownloadId(string downloadId)
|
||||||
{
|
{
|
||||||
return Query.Where(h => h.DownloadId == downloadId)
|
return Query.Where(h => h.DownloadId == downloadId)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace NzbDrone.Core.History
|
|||||||
QualityModel GetBestQualityInHistory(Profile profile, int episodeId);
|
QualityModel GetBestQualityInHistory(Profile profile, int episodeId);
|
||||||
PagingSpec<History> Paged(PagingSpec<History> pagingSpec);
|
PagingSpec<History> Paged(PagingSpec<History> pagingSpec);
|
||||||
History MostRecentForEpisode(int episodeId);
|
History MostRecentForEpisode(int episodeId);
|
||||||
|
List<History> FindByEpisodeId(int episodeId);
|
||||||
History MostRecentForDownloadId(string downloadId);
|
History MostRecentForDownloadId(string downloadId);
|
||||||
History Get(int historyId);
|
History Get(int historyId);
|
||||||
List<History> Find(string downloadId, HistoryEventType eventType);
|
List<History> Find(string downloadId, HistoryEventType eventType);
|
||||||
@@ -55,6 +56,11 @@ namespace NzbDrone.Core.History
|
|||||||
return _historyRepository.MostRecentForEpisode(episodeId);
|
return _historyRepository.MostRecentForEpisode(episodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<History> FindByEpisodeId(int episodeId)
|
||||||
|
{
|
||||||
|
return _historyRepository.FindByEpisodeId(episodeId);
|
||||||
|
}
|
||||||
|
|
||||||
public History MostRecentForDownloadId(string downloadId)
|
public History MostRecentForDownloadId(string downloadId)
|
||||||
{
|
{
|
||||||
return _historyRepository.MostRecentForDownloadId(downloadId);
|
return _historyRepository.MostRecentForDownloadId(downloadId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -314,6 +314,16 @@ namespace NzbDrone.Core.IndexerSearch
|
|||||||
|
|
||||||
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
_logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count);
|
||||||
|
|
||||||
|
// Update the last search time for all episodes if at least 1 indexer was searched.
|
||||||
|
if (indexers.Any())
|
||||||
|
{
|
||||||
|
var lastSearchTime = DateTime.UtcNow;
|
||||||
|
_logger.Debug("Setting last search time to: {0}", lastSearchTime);
|
||||||
|
|
||||||
|
criteriaBase.Episodes.ForEach(e => e.LastSearchTime = lastSearchTime);
|
||||||
|
_episodeService.UpdateEpisodes(criteriaBase.Episodes);
|
||||||
|
}
|
||||||
|
|
||||||
return _makeDownloadDecision.GetSearchDecision(reports, criteriaBase).ToList();
|
return _makeDownloadDecision.GetSearchDecision(reports, criteriaBase).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using NzbDrone.Common.Http;
|
|||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Newznab
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
@@ -102,7 +103,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
base.Test(failures);
|
base.Test(failures);
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestCapabilities());
|
failures.AddIfNotNull(TestCapabilities());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -253,12 +253,25 @@ namespace NzbDrone.Core.Indexers
|
|||||||
protected virtual RssEnclosure[] GetEnclosures(XElement item)
|
protected virtual RssEnclosure[] GetEnclosures(XElement item)
|
||||||
{
|
{
|
||||||
var enclosures = item.Elements("enclosure")
|
var enclosures = item.Elements("enclosure")
|
||||||
.Select(v => new RssEnclosure
|
.Select(v =>
|
||||||
{
|
{
|
||||||
Url = v.Attribute("url").Value,
|
try
|
||||||
Type = v.Attribute("type").Value,
|
{
|
||||||
Length = (long)v.Attribute("length")
|
return new RssEnclosure
|
||||||
|
{
|
||||||
|
Url = v.Attribute("url").Value,
|
||||||
|
Type = v.Attribute("type").Value,
|
||||||
|
Length = (long)v.Attribute("length")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warn(e, "Failed to get enclosure for: {0}", item.Title());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
})
|
})
|
||||||
|
.Where(v => v != null)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return enclosures;
|
return enclosures;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using NzbDrone.Core.Configuration;
|
|||||||
using NzbDrone.Core.Indexers.Newznab;
|
using NzbDrone.Core.Indexers.Newznab;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Torznab
|
namespace NzbDrone.Core.Indexers.Torznab
|
||||||
{
|
{
|
||||||
@@ -91,7 +92,7 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||||||
protected override void Test(List<ValidationFailure> failures)
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
{
|
{
|
||||||
base.Test(failures);
|
base.Test(failures);
|
||||||
if (failures.Any()) return;
|
if (failures.HasErrors()) return;
|
||||||
failures.AddIfNotNull(TestCapabilities());
|
failures.AddIfNotNull(TestCapabilities());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Marr.Data;
|
using Marr.Data;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public long Size { get; set; }
|
public long Size { get; set; }
|
||||||
public DateTime DateAdded { get; set; }
|
public DateTime DateAdded { get; set; }
|
||||||
|
public string OriginalFilePath { get; set; }
|
||||||
public string SceneName { get; set; }
|
public string SceneName { get; set; }
|
||||||
public string ReleaseGroup { get; set; }
|
public string ReleaseGroup { get; set; }
|
||||||
public QualityModel Quality { get; set; }
|
public QualityModel Quality { get; set; }
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ using NzbDrone.Core.Parser.Model;
|
|||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
|
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
|
||||||
{
|
{
|
||||||
public interface IAugmentingService
|
public interface IAggregationService
|
||||||
{
|
{
|
||||||
LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles);
|
LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AugmentingService : IAugmentingService
|
public class AggregationService : IAggregationService
|
||||||
{
|
{
|
||||||
private readonly IEnumerable<IAggregateLocalEpisode> _augmenters;
|
private readonly IEnumerable<IAggregateLocalEpisode> _augmenters;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
|
|||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public AugmentingService(IEnumerable<IAggregateLocalEpisode> augmenters,
|
public AggregationService(IEnumerable<IAggregateLocalEpisode> augmenters,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IVideoFileInfoReader videoFileInfoReader,
|
IVideoFileInfoReader videoFileInfoReader,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
@@ -38,11 +38,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
|
|||||||
|
|
||||||
public LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles)
|
public LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles)
|
||||||
{
|
{
|
||||||
|
var isMediaFile = MediaFileExtensions.Extensions.Contains(Path.GetExtension(localEpisode.Path));
|
||||||
|
|
||||||
if (localEpisode.DownloadClientEpisodeInfo == null &&
|
if (localEpisode.DownloadClientEpisodeInfo == null &&
|
||||||
localEpisode.FolderEpisodeInfo == null &&
|
localEpisode.FolderEpisodeInfo == null &&
|
||||||
localEpisode.FileEpisodeInfo == null)
|
localEpisode.FileEpisodeInfo == null)
|
||||||
{
|
{
|
||||||
if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(localEpisode.Path)))
|
if (isMediaFile)
|
||||||
{
|
{
|
||||||
throw new AugmentingFailedException("Unable to parse episode info from path: {0}", localEpisode.Path);
|
throw new AugmentingFailedException("Unable to parse episode info from path: {0}", localEpisode.Path);
|
||||||
}
|
}
|
||||||
@@ -50,7 +52,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
|
|||||||
|
|
||||||
localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path);
|
localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path);
|
||||||
|
|
||||||
if (!localEpisode.ExistingFile || _configService.EnableMediaInfo)
|
if (isMediaFile && (!localEpisode.ExistingFile || _configService.EnableMediaInfo))
|
||||||
{
|
{
|
||||||
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(localEpisode.Path);
|
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(localEpisode.Path);
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-3
@@ -30,11 +30,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
|||||||
|
|
||||||
if (!otherFiles && !SceneChecker.IsSceneTitle(Path.GetFileNameWithoutExtension(localEpisode.Path)))
|
if (!otherFiles && !SceneChecker.IsSceneTitle(Path.GetFileNameWithoutExtension(localEpisode.Path)))
|
||||||
{
|
{
|
||||||
if (downloadClientEpisodeInfo != null && !downloadClientEpisodeInfo.FullSeason)
|
if (downloadClientEpisodeInfo != null &&
|
||||||
|
!downloadClientEpisodeInfo.FullSeason &&
|
||||||
|
PreferOtherEpisodeInfo(parsedEpisodeInfo, downloadClientEpisodeInfo))
|
||||||
{
|
{
|
||||||
parsedEpisodeInfo = localEpisode.DownloadClientEpisodeInfo;
|
parsedEpisodeInfo = localEpisode.DownloadClientEpisodeInfo;
|
||||||
}
|
}
|
||||||
else if (folderEpisodeInfo != null && !folderEpisodeInfo.FullSeason)
|
else if (folderEpisodeInfo != null &&
|
||||||
|
!folderEpisodeInfo.FullSeason &&
|
||||||
|
PreferOtherEpisodeInfo(parsedEpisodeInfo, folderEpisodeInfo))
|
||||||
{
|
{
|
||||||
parsedEpisodeInfo = localEpisode.FolderEpisodeInfo;
|
parsedEpisodeInfo = localEpisode.FolderEpisodeInfo;
|
||||||
}
|
}
|
||||||
@@ -45,7 +49,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
|||||||
var title = Path.GetFileNameWithoutExtension(localEpisode.Path);
|
var title = Path.GetFileNameWithoutExtension(localEpisode.Path);
|
||||||
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, title, localEpisode.Series);
|
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, title, localEpisode.Series);
|
||||||
|
|
||||||
return specialEpisodeInfo;
|
if (specialEpisodeInfo != null)
|
||||||
|
{
|
||||||
|
parsedEpisodeInfo = specialEpisodeInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedEpisodeInfo;
|
return parsedEpisodeInfo;
|
||||||
@@ -68,5 +75,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
|
|||||||
|
|
||||||
return new List<Episode>();
|
return new List<Episode>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool PreferOtherEpisodeInfo(ParsedEpisodeInfo fileEpisodeInfo, ParsedEpisodeInfo otherEpisodeInfo)
|
||||||
|
{
|
||||||
|
if (fileEpisodeInfo == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the files episode info is not absolute prefer it over a parsed episode info that is absolute
|
||||||
|
if (!fileEpisodeInfo.IsAbsoluteNumbering && otherEpisodeInfo.IsAbsoluteNumbering)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
if (newDownload)
|
if (newDownload)
|
||||||
{
|
{
|
||||||
|
episodeFile.OriginalFilePath = GetOriginalFilePath(downloadClientItem, localEpisode);
|
||||||
episodeFile.SceneName = GetSceneName(downloadClientItem, localEpisode);
|
episodeFile.SceneName = GetSceneName(downloadClientItem, localEpisode);
|
||||||
|
|
||||||
var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly);
|
var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly);
|
||||||
@@ -148,6 +149,43 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
return importResults;
|
return importResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetOriginalFilePath(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
|
||||||
|
{
|
||||||
|
var path = localEpisode.Path;
|
||||||
|
|
||||||
|
if (downloadClientItem != null && !downloadClientItem.OutputPath.IsEmpty)
|
||||||
|
{
|
||||||
|
var outputDirectory = downloadClientItem.OutputPath.Directory.ToString();
|
||||||
|
|
||||||
|
if (outputDirectory.IsParentPath(path))
|
||||||
|
{
|
||||||
|
return outputDirectory.GetRelativePath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var folderEpisodeInfo = localEpisode.FolderEpisodeInfo;
|
||||||
|
|
||||||
|
if (folderEpisodeInfo != null)
|
||||||
|
{
|
||||||
|
var folderPath = path.GetAncestorPath(folderEpisodeInfo.ReleaseTitle);
|
||||||
|
|
||||||
|
if (folderPath != null)
|
||||||
|
{
|
||||||
|
return folderPath.GetParentPath().GetRelativePath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentPath = path.GetParentPath();
|
||||||
|
var grandparentPath = parentPath.GetParentPath();
|
||||||
|
|
||||||
|
if (grandparentPath != null)
|
||||||
|
{
|
||||||
|
return grandparentPath.GetRelativePath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileName(path);
|
||||||
|
}
|
||||||
|
|
||||||
private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
|
private string GetSceneName(DownloadClientItem downloadClientItem, LocalEpisode localEpisode)
|
||||||
{
|
{
|
||||||
if (downloadClientItem != null)
|
if (downloadClientItem != null)
|
||||||
|
|||||||
@@ -23,21 +23,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
{
|
{
|
||||||
private readonly IEnumerable<IImportDecisionEngineSpecification> _specifications;
|
private readonly IEnumerable<IImportDecisionEngineSpecification> _specifications;
|
||||||
private readonly IMediaFileService _mediaFileService;
|
private readonly IMediaFileService _mediaFileService;
|
||||||
private readonly IAugmentingService _augmentingService;
|
private readonly IAggregationService _aggregationService;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly IDetectSample _detectSample;
|
private readonly IDetectSample _detectSample;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification> specifications,
|
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification> specifications,
|
||||||
IMediaFileService mediaFileService,
|
IMediaFileService mediaFileService,
|
||||||
IAugmentingService augmentingService,
|
IAggregationService aggregationService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IDetectSample detectSample,
|
IDetectSample detectSample,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_specifications = specifications;
|
_specifications = specifications;
|
||||||
_mediaFileService = mediaFileService;
|
_mediaFileService = mediaFileService;
|
||||||
_augmentingService = augmentingService;
|
_aggregationService = aggregationService;
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_detectSample = detectSample;
|
_detectSample = detectSample;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -61,7 +61,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title);
|
downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nonSampleVideoFileCount = GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo);
|
// If not importing from a scene source (series folder for example), then assume all files are not samples
|
||||||
|
// to avoid using media info on every file needlessly (especially if Analyse Media Files is disabled).
|
||||||
|
var nonSampleVideoFileCount = sceneSource ? GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo) : videoFiles.Count;
|
||||||
|
|
||||||
var decisions = new List<ImportDecision>();
|
var decisions = new List<ImportDecision>();
|
||||||
|
|
||||||
@@ -94,7 +96,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_augmentingService.Augment(localEpisode, otherFiles);
|
_aggregationService.Augment(localEpisode, otherFiles);
|
||||||
|
|
||||||
if (localEpisode.Episodes.Empty())
|
if (localEpisode.Episodes.Empty())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
||||||
{
|
{
|
||||||
public class ManualImportFile
|
public class ManualImportFile : IEquatable<ManualImportFile>
|
||||||
{
|
{
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public string FolderName { get; set; }
|
public string FolderName { get; set; }
|
||||||
@@ -11,5 +13,35 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||||||
public List<int> EpisodeIds { get; set; }
|
public List<int> EpisodeIds { get; set; }
|
||||||
public QualityModel Quality { get; set; }
|
public QualityModel Quality { get; set; }
|
||||||
public string DownloadId { get; set; }
|
public string DownloadId { get; set; }
|
||||||
|
|
||||||
|
public bool Equals(ManualImportFile other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.PathEquals(other.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.GetType() != GetType())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.PathEquals(((ManualImportFile)obj).Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return Path != null ? Path.GetHashCode() : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||||||
private readonly IEpisodeService _episodeService;
|
private readonly IEpisodeService _episodeService;
|
||||||
private readonly IVideoFileInfoReader _videoFileInfoReader;
|
private readonly IVideoFileInfoReader _videoFileInfoReader;
|
||||||
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
|
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
|
||||||
private readonly IAugmentingService _augmentingService;
|
private readonly IAggregationService _aggregationService;
|
||||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||||
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
|
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||||||
ISeriesService seriesService,
|
ISeriesService seriesService,
|
||||||
IEpisodeService episodeService,
|
IEpisodeService episodeService,
|
||||||
IVideoFileInfoReader videoFileInfoReader,
|
IVideoFileInfoReader videoFileInfoReader,
|
||||||
IAugmentingService augmentingService,
|
IAggregationService aggregationService,
|
||||||
IImportApprovedEpisodes importApprovedEpisodes,
|
IImportApprovedEpisodes importApprovedEpisodes,
|
||||||
ITrackedDownloadService trackedDownloadService,
|
ITrackedDownloadService trackedDownloadService,
|
||||||
IDownloadedEpisodesImportService downloadedEpisodesImportService,
|
IDownloadedEpisodesImportService downloadedEpisodesImportService,
|
||||||
@@ -61,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
_episodeService = episodeService;
|
_episodeService = episodeService;
|
||||||
_videoFileInfoReader = videoFileInfoReader;
|
_videoFileInfoReader = videoFileInfoReader;
|
||||||
_augmentingService = augmentingService;
|
_aggregationService = aggregationService;
|
||||||
_importApprovedEpisodes = importApprovedEpisodes;
|
_importApprovedEpisodes = importApprovedEpisodes;
|
||||||
_trackedDownloadService = trackedDownloadService;
|
_trackedDownloadService = trackedDownloadService;
|
||||||
_downloadedEpisodesImportService = downloadedEpisodesImportService;
|
_downloadedEpisodesImportService = downloadedEpisodesImportService;
|
||||||
@@ -281,7 +281,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
|
|||||||
localEpisode.FolderEpisodeInfo = Parser.Parser.ParseTitle(file.FolderName);
|
localEpisode.FolderEpisodeInfo = Parser.Parser.ParseTitle(file.FolderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
localEpisode = _augmentingService.Augment(localEpisode, false);
|
localEpisode = _aggregationService.Augment(localEpisode, false);
|
||||||
|
|
||||||
// Apply the user-chosen values.
|
// Apply the user-chosen values.
|
||||||
localEpisode.Series = series;
|
localEpisode.Series = series;
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
|||||||
|
|
||||||
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
|
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
|
||||||
{
|
{
|
||||||
|
if (localEpisode.FileEpisodeInfo == null)
|
||||||
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
if (localEpisode.FileEpisodeInfo.FullSeason)
|
if (localEpisode.FileEpisodeInfo.FullSeason)
|
||||||
{
|
{
|
||||||
_logger.Debug("Single episode file detected as containing all episodes in the season");
|
_logger.Debug("Single episode file detected as containing all episodes in the season");
|
||||||
|
|||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Core.DecisionEngine;
|
|
||||||
using NzbDrone.Core.Download;
|
|
||||||
using NzbDrone.Core.History;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
|
||||||
using NzbDrone.Core.Qualities;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
|
||||||
{
|
|
||||||
public class GrabbedReleaseQualitySpecification : IImportDecisionEngineSpecification
|
|
||||||
{
|
|
||||||
private readonly IHistoryService _historyService;
|
|
||||||
private readonly Logger _logger;
|
|
||||||
|
|
||||||
public GrabbedReleaseQualitySpecification(IHistoryService historyService, Logger logger)
|
|
||||||
{
|
|
||||||
_historyService = historyService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
|
|
||||||
{
|
|
||||||
if (downloadClientItem == null)
|
|
||||||
{
|
|
||||||
_logger.Debug("No download client item provided, skipping.");
|
|
||||||
return Decision.Accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
var grabbedHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId)
|
|
||||||
.Where(h => h.EventType == HistoryEventType.Grabbed)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (grabbedHistory.Empty())
|
|
||||||
{
|
|
||||||
_logger.Debug("No grabbed history for this download client item");
|
|
||||||
return Decision.Accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
var parsedReleaseName = Parser.Parser.ParseTitle(grabbedHistory.First().SourceTitle);
|
|
||||||
|
|
||||||
if (parsedReleaseName != null && parsedReleaseName.FullSeason)
|
|
||||||
{
|
|
||||||
_logger.Debug("File is part of a season pack, skipping.");
|
|
||||||
return Decision.Accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in grabbedHistory)
|
|
||||||
{
|
|
||||||
if (item.Quality.Quality != Quality.Unknown && item.Quality != localEpisode.Quality)
|
|
||||||
{
|
|
||||||
_logger.Debug("Quality for grabbed release ({0}) does not match the quality of the file ({1})", item.Quality, localEpisode.Quality);
|
|
||||||
return Decision.Reject("File quality does not match quality of the grabbed release");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Decision.Accept();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
@@ -34,11 +34,19 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
|||||||
|
|
||||||
if (folderInfo == null)
|
if (folderInfo == null)
|
||||||
{
|
{
|
||||||
|
_logger.Debug("No folder ParsedEpisodeInfo, skipping check");
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo == null)
|
||||||
|
{
|
||||||
|
_logger.Debug("No file ParsedEpisodeInfo, skipping check");
|
||||||
return Decision.Accept();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!folderInfo.EpisodeNumbers.Any())
|
if (!folderInfo.EpisodeNumbers.Any())
|
||||||
{
|
{
|
||||||
|
_logger.Debug("No episode numbers in folder ParsedEpisodeInfo, skipping check");
|
||||||
return Decision.Accept();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user