1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-06 13:31:28 -05:00

Compare commits

...

19 Commits

Author SHA1 Message Date
Rick van Hattem
080d0a9b04 New: Added links to wiki and examples from add/edit custom tags dialog 2019-11-24 00:37:41 -05:00
Pierre Spring
1b58ae7d47 Fixed: open links in new tab on meta click (#3831)
This adds support to open links in new tabs for all the operating
systems.

Fixes #3830
2019-11-03 21:45:24 -05:00
Jef LeCompte
a5e2e5777c Fixed: Added qBittorent state 'moving' (#3847)
* Added qBittorent state 'moving'

The state 'moving' wasn't being recording in Radarr, so it would show up
as a warning.

* Updated unknown state to be info
2019-11-03 21:42:54 -05:00
Rick van Hattem
ddc2b42923 Fixed: link to the custom format examples (#3785) 2019-10-23 21:03:27 -04:00
Matthew Jacques
3ac3737de9 Fixed: Replaced episode with movie in UpgradeSpecification (#3805) 2019-10-11 14:49:43 -04:00
Qstick
01ad015b14 Changed: Regenerate package.json for secondary package updates (#3750) 2019-09-08 23:05:16 -04:00
Qstick
3d57d5aba7 Fixed: qBit V2 and metaDL Support 2019-09-06 09:59:19 -04:00
Qstick
d65fe3a530 Fixed: Integration Test Failing CircleCi 2019-09-06 09:59:19 -04:00
Qstick
e100759e71 New: Platform Updates, Socket Closure Workaround 2019-09-06 09:59:19 -04:00
Qstick
df068e9f0a Fixed: TMDB Failing due to missing response header (#3610) 2019-09-01 11:55:33 -04:00
jpogs
c27f08738a Fixed: .srt files in subfolders are not being imported (#3647)
* Fixed #1958
- extra file module will search for any subfolder and filename
- fixed language parser to match RARBG format
- Add .srt subs according to level of details such that higher detailed sub gets loaded to media player first

* Fixed Code Factor issue on SubtitleService.cs:104

* Fixed: issues on unit test for TV subs; added test cases for RARBG movie subs

* Updated RARBG parser so it won't match movie title format

* Cleaned up code for review

* Update SubtitleService.cs
2019-09-01 11:53:40 -04:00
rubasace
61629a527c Fixed: Parse UHDRemux as Remux and not as WEB-DL (#3696)
* Parse UHDRemux as Remux and not as WEB-DL

* Add test case for UHDRemux parsing
2019-09-01 11:46:09 -04:00
desimaniac
5291f42905 New: Added repost exclusions to CleanReleaseGroupRegex (#3720) 2019-09-01 11:39:27 -04:00
Qstick
0ce5857094 Fixed: Wanted Cutoff Page filters (#3611)
* Fixed: Cutoff Filters Broken

* Fixed: Wanted Filters Broken

* Fixed: CutoffUnmet Integration Tests

* Really fixed CutoffUnmet Integration tests.

* Added: Some more integration tests for CutoffUnmet

* Fixed: Integration tests for MissingFixture.
2019-07-10 18:12:30 -04:00
FuNK3Y
443078a7e4 Added: Ability to set categories for search for RARBG (#3544)
Also fixes an issue where the rargb movie category would not actually contain all movie categories. Fixes #3543
2019-07-10 10:41:58 +02:00
Kyrylo Mikos
dbdda0da13 Added: Support for Ukrainian language. (#3594) 2019-07-10 10:38:55 +02:00
Leonardo Galli
b5e1b83de3 Fixed: All integration tests and some code which was wrong. (#3604)
* Fixed: All integration tests and some code which was wrong.

* Hopefully fix Movie Fixture issues.

* Fixed: HttpFixture. Took commit from sonarr.
2019-07-08 00:45:52 +02:00
Leonardo Galli
8e43f5c4ae Fixed: When refreshing info about a movie, the alt titles should now correctly be deleted / updated, even from TMDB. (#3603)
* Fixed: When refreshing info about a movie, the alt titles should now correctly be deleted / updated, even from TMDB.
Fixes #3542

* Fixed: Small things fixup.
2019-07-08 00:15:35 +02:00
Mike S
e6d3954e79 Update to work with Deluge v2 (#3577) 2019-06-21 18:36:27 -04:00
124 changed files with 7214 additions and 6287 deletions

View File

@@ -154,9 +154,9 @@ workflows:
- unit_tests:
requires:
- build
#- integration_tests:
# requires:
# - build
- integration_tests:
requires:
- build
- publish_artifacts:
requires:
- build

9900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Radarr",
"version": "2.0.0",
"name": "radarr",
"version": "1.0.0",
"description": "Radarr",
"main": "main.js",
"scripts": {

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -23,7 +23,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
_logger = logger;
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
_writeGZipStream = OsInfo.IsMonoRuntime ? WriteGZipStreamMono : (Action<Action<Stream>, Stream>)WriteGZipStream;
_writeGZipStream = PlatformInfo.IsMono ? WriteGZipStreamMono : (Action<Action<Stream>, Stream>)WriteGZipStream;
}
public void Register(IPipelines pipelines)

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Api.Frontend
{
public bool IsCacheable(NancyContext context)
{
if (!RuntimeInfoBase.IsProduction)
if (!RuntimeInfo.IsProduction)
{
return false;
}
@@ -46,4 +46,4 @@ namespace NzbDrone.Api.Frontend
return true;
}
}
}
}

View File

@@ -79,7 +79,7 @@ namespace NzbDrone.Api.Frontend.Mappers
private string GetIndexText()
{
if (RuntimeInfoBase.IsProduction && _generatedContent != null)
if (RuntimeInfo.IsProduction && _generatedContent != null)
{
return _generatedContent;
}
@@ -111,7 +111,7 @@ namespace NzbDrone.Api.Frontend.Mappers
text = text.Replace("APP_BRANCH", _configFileProvider.Branch.ToLower());
text = text.Replace("APP_ANALYTICS", _analyticsService.IsEnabled.ToString().ToLowerInvariant());
text = text.Replace("URL_BASE", URL_BASE);
text = text.Replace("PRODUCTION", RuntimeInfoBase.IsProduction.ToString().ToLowerInvariant());
text = text.Replace("PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant());
_generatedContent = text;

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Api.Frontend.Mappers
private string GetLoginText()
{
if (RuntimeInfoBase.IsProduction && _generatedContent != null)
if (RuntimeInfo.IsProduction && _generatedContent != null)
{
return _generatedContent;
}

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Api.Frontend.Mappers
_diskProvider = diskProvider;
_logger = logger;
if (!RuntimeInfoBase.IsProduction)
if (!RuntimeInfo.IsProduction)
{
_caseSensitive = StringComparison.OrdinalIgnoreCase;
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Nancy.Bootstrapper;
using Nancy.Diagnostics;
using NLog;
@@ -24,9 +24,9 @@ namespace NzbDrone.Api
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
Logger.Info("Starting NzbDrone API");
Logger.Info("Starting Web Server");
if (RuntimeInfoBase.IsProduction)
if (RuntimeInfo.IsProduction)
{
DiagnosticsHook.Disable(pipelines);
}

View File

@@ -1,4 +1,4 @@
using Nancy;
using Nancy;
using Nancy.Routing;
using NzbDrone.Api.Extensions;
using NzbDrone.Common.EnvironmentInfo;
@@ -13,6 +13,8 @@ namespace NzbDrone.Api.System
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IPlatformInfo _platformInfo;
private readonly IOsInfo _osInfo;
private readonly IRouteCacheProvider _routeCacheProvider;
private readonly IConfigFileProvider _configFileProvider;
private readonly IMainDatabase _database;
@@ -20,14 +22,17 @@ namespace NzbDrone.Api.System
public SystemModule(IAppFolderInfo appFolderInfo,
IRuntimeInfo runtimeInfo,
IPlatformInfo platformInfo,
IOsInfo osInfo,
IRouteCacheProvider routeCacheProvider,
IConfigFileProvider configFileProvider,
IMainDatabase database,
ILifecycleService lifecycleService)
: base("system")
ILifecycleService lifecycleService) : base("system")
{
_appFolderInfo = appFolderInfo;
_runtimeInfo = runtimeInfo;
_platformInfo = platformInfo;
_osInfo = osInfo;
_routeCacheProvider = routeCacheProvider;
_configFileProvider = configFileProvider;
_database = database;
@@ -41,27 +46,29 @@ namespace NzbDrone.Api.System
private Response GetStatus()
{
return new
{
Version = BuildInfo.Version.ToString(),
BuildTime = BuildInfo.BuildDateTime,
IsDebug = BuildInfo.IsDebug,
IsProduction = RuntimeInfoBase.IsProduction,
IsAdmin = _runtimeInfo.IsAdmin,
IsUserInteractive = RuntimeInfoBase.IsUserInteractive,
StartupPath = _appFolderInfo.StartUpFolder,
AppData = _appFolderInfo.GetAppDataPath(),
OsVersion = OsInfo.Version.ToString(),
IsMonoRuntime = OsInfo.IsMonoRuntime,
IsMono = OsInfo.IsNotWindows,
IsLinux = OsInfo.IsLinux,
IsOsx = OsInfo.IsOsx,
IsWindows = OsInfo.IsWindows,
Branch = _configFileProvider.Branch,
Authentication = _configFileProvider.AuthenticationMethod,
SqliteVersion = _database.Version,
UrlBase = _configFileProvider.UrlBase,
RuntimeVersion = _runtimeInfo.RuntimeVersion
}.AsResponse();
{
Version = BuildInfo.Version.ToString(),
BuildTime = BuildInfo.BuildDateTime,
IsDebug = BuildInfo.IsDebug,
IsProduction = RuntimeInfo.IsProduction,
IsAdmin = _runtimeInfo.IsAdmin,
IsUserInteractive = RuntimeInfo.IsUserInteractive,
StartupPath = _appFolderInfo.StartUpFolder,
AppData = _appFolderInfo.GetAppDataPath(),
OsName = _osInfo.Name,
OsVersion = _osInfo.Version,
IsMonoRuntime = PlatformInfo.IsMono,
IsMono = PlatformInfo.IsMono,
IsLinux = OsInfo.IsLinux,
IsOsx = OsInfo.IsOsx,
IsWindows = OsInfo.IsWindows,
Branch = _configFileProvider.Branch,
Authentication = _configFileProvider.AuthenticationMethod,
SqliteVersion = _database.Version,
UrlBase = _configFileProvider.UrlBase,
RuntimeVersion = _platformInfo.Version,
RuntimeName = PlatformInfo.Platform
}.AsResponse();
}
private Response GetRoutes()

View File

@@ -15,9 +15,9 @@ namespace NzbDrone.Api.Wanted
{
protected readonly IMovieService _movieService;
public MovieMissingModule(IMovieService movieService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
public MovieMissingModule(IMovieService movieService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(movieService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing")
{

View File

@@ -29,7 +29,7 @@ namespace NzbDrone.Common.Test
[Test]
public void IsProduction_should_return_false_when_run_within_nunit()
{
RuntimeInfoBase.IsProduction.Should().BeFalse("Process name is " + Process.GetCurrentProcess().ProcessName + " Folder is " + Directory.GetCurrentDirectory());
RuntimeInfo.IsProduction.Should().BeFalse("Process name is " + Process.GetCurrentProcess().ProcessName + " Folder is " + Directory.GetCurrentDirectory());
}
[Test]

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -9,6 +9,7 @@ using Moq;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy;
@@ -30,6 +31,12 @@ namespace NzbDrone.Common.Test.Http
[SetUp]
public void SetUp()
{
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.Version).Returns("9.0.0");
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
@@ -48,7 +55,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_execute_simple_get()
{
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
var response = Subject.Execute(request);
@@ -58,7 +65,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_execute_https_get()
{
var request = new HttpRequest(string.Format("https://{0}/get", _httpBinHost));
var request = new HttpRequest($"https://{_httpBinHost}/get");
var response = Subject.Execute(request);
@@ -68,11 +75,12 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_execute_typed_get()
{
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get?test=1");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Url.Should().Be(request.Url.FullUri);
response.Resource.Url.EndsWith("/get?test=1");
response.Resource.Args.Should().Contain("test", "1");
}
[Test]
@@ -80,7 +88,7 @@ namespace NzbDrone.Common.Test.Http
{
var message = "{ my: 1 }";
var request = new HttpRequest(string.Format("http://{0}/post", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/post");
request.SetContent(message);
var response = Subject.Post<HttpBinResource>(request);
@@ -91,7 +99,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("gzip")]
public void should_execute_get_using_gzip(string compression)
{
var request = new HttpRequest(string.Format("http://{0}/{1}", _httpBinHost, compression));
var request = new HttpRequest($"http://{_httpBinHost}/{compression}");
var response = Subject.Get<HttpBinResource>(request);
@@ -107,7 +115,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase(HttpStatusCode.BadGateway)]
public void should_throw_on_unsuccessful_status_codes(int statusCode)
{
var request = new HttpRequest(string.Format("http://{0}/status/{1}", _httpBinHost, statusCode));
var request = new HttpRequest($"http://{_httpBinHost}/status/{statusCode}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
@@ -119,7 +127,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_not_follow_redirects_when_not_in_production()
{
var request = new HttpRequest(string.Format("http://{0}/redirect/1", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
Subject.Get(request);
@@ -129,7 +137,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_follow_redirects()
{
var request = new HttpRequest(string.Format("http://{0}/redirect/1", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = true;
var response = Subject.Get(request);
@@ -182,7 +190,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_send_user_agent()
{
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
@@ -196,7 +204,7 @@ namespace NzbDrone.Common.Test.Http
[TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")]
public void should_send_headers(string header, string value)
{
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
request.Headers.Add(header, value);
var response = Subject.Get<HttpBinResource>(request);
@@ -219,7 +227,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_send_cookie()
{
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
request.Cookies["my"] = "cookie";
var response = Subject.Get<HttpBinResource>(request);
@@ -236,7 +244,7 @@ namespace NzbDrone.Common.Test.Http
var oldRequest = new HttpRequest("http://eu.httpbin.org/get");
oldRequest.Cookies["my"] = "cookie";
var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), 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>());
oldClient.Should().NotBeSameAs(Subject);
@@ -329,7 +337,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_not_store_response_cookie()
{
var requestSet = new HttpRequest(string.Format("http://{0}/cookies/set?my=cookie", _httpBinHost));
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
@@ -348,7 +356,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_store_response_cookie()
{
var requestSet = new HttpRequest(string.Format("http://{0}/cookies/set?my=cookie", _httpBinHost));
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
@@ -514,7 +522,7 @@ namespace NzbDrone.Common.Test.Http
public void should_not_send_old_cookie()
{
GivenOldCookie();
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.IgnorePersistentCookies = true;
requestCookies.StoreRequestCookie = false;
@@ -527,7 +535,7 @@ namespace NzbDrone.Common.Test.Http
[Test]
public void should_throw_on_http429_too_many_requests()
{
var request = new HttpRequest(string.Format("http://{0}/status/429", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/status/429");
Assert.Throws<TooManyRequestsException>(() => Subject.Get(request));
@@ -547,7 +555,7 @@ namespace NzbDrone.Common.Test.Http
.Setup(v => v.PostResponse(It.IsAny<HttpResponse>()))
.Returns<HttpResponse>(r => r);
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
Subject.Get(request);
@@ -569,7 +577,7 @@ namespace NzbDrone.Common.Test.Http
{
// the date is bad in the below - should be 13-Jul-2026
string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly";
var requestSet = new HttpRequestBuilder(string.Format("http://{0}/response-headers", _httpBinHost))
var requestSet = new HttpRequestBuilder($"http://{_httpBinHost}/response-headers")
.AddQueryParam("Set-Cookie", malformedCookie)
.Build();
@@ -578,7 +586,7 @@ namespace NzbDrone.Common.Test.Http
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
@@ -602,7 +610,8 @@ namespace NzbDrone.Common.Test.Http
{
try
{
string url = string.Format("http://{0}/response-headers?Set-Cookie={1}", _httpBinHost, Uri.EscapeUriString(malformedCookie));
string url =
$"http://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}";
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
@@ -610,7 +619,7 @@ namespace NzbDrone.Common.Test.Http
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost));
var request = new HttpRequest($"http://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);
@@ -626,6 +635,7 @@ namespace NzbDrone.Common.Test.Http
public class HttpBinResource
{
public Dictionary<string, object> Args { get; set; }
public Dictionary<string, object> Headers { get; set; }
public string Origin { get; set; }
public string Url { get; set; }

View File

@@ -0,0 +1,30 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Test.Common;
namespace NzbDrone.Common.Test.Http
{
[TestFixture]
public class UserAgentBuilderFixture : TestBase<UserAgentBuilder>
{
[Test]
public void should_get_user_agent_if_os_version_is_null()
{
Mocker.GetMock<IOsInfo>().SetupGet(c => c.Version).Returns((string)null);
Mocker.GetMock<IOsInfo>().SetupGet(c => c.Name).Returns("TestOS");
Subject.GetUserAgent(false).Should().NotBeNullOrWhiteSpace();
}
[Test]
public void should_get_use_os_family_if_name_is_null()
{
Mocker.GetMock<IOsInfo>().SetupGet(c => c.Version).Returns((string)null);
Mocker.GetMock<IOsInfo>().SetupGet(c => c.Name).Returns((string)null);
Subject.GetUserAgent(false).Should().NotBeNullOrWhiteSpace();
}
}
}

View File

@@ -91,6 +91,7 @@
<Compile Include="Http\HttpRequestBuilderFixture.cs" />
<Compile Include="Http\HttpRequestFixture.cs" />
<Compile Include="Http\HttpUriFixture.cs" />
<Compile Include="Http\UserAgentBuilderFixture.cs" />
<Compile Include="InstrumentationTests\CleanseLogMessageFixture.cs" />
<Compile Include="LevenshteinDistanceFixture.cs" />
<Compile Include="OsPathFixture.cs" />

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -13,12 +13,15 @@ namespace NzbDrone.Common.Composition
{
private readonly List<Type> _loadedTypes;
public IContainer Container { get; private set; }
protected IContainer Container { get; }
protected ContainerBuilderBase(IStartupContext args, params string[] assemblies)
protected ContainerBuilderBase(IStartupContext args, List<string> assemblies)
{
_loadedTypes = new List<Type>();
assemblies.Add(OsInfo.IsWindows ? "NzbDrone.Windows" : "NzbDrone.Mono");
assemblies.Add("NzbDrone.Common");
foreach (var assembly in assemblies)
{
_loadedTypes.AddRange(Assembly.Load(assembly).GetTypes());

View File

@@ -17,6 +17,19 @@ namespace NzbDrone.Common.Disk
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DiskProviderBase));
public static StringComparison PathStringComparison
{
get
{
if (OsInfo.IsWindows)
{
return StringComparison.OrdinalIgnoreCase;
}
return StringComparison.Ordinal;
}
}
public abstract long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename);
public abstract void SetPermissions(string path, string mask, string user, string group);
@@ -87,7 +100,7 @@ namespace NzbDrone.Common.Disk
public bool FileExists(string path)
{
Ensure.That(path, () => path).IsValidPath();
return FileExists(path, OsInfo.PathStringComparison);
return FileExists(path, PathStringComparison);
}
public bool FileExists(string path, StringComparison stringComparison)

View File

@@ -101,12 +101,12 @@ namespace NzbDrone.Common.EnsureThat
if (param.Value.IsPathValid()) return param;
if (OsInfo.IsNotWindows)
if (OsInfo.IsWindows)
{
throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid *nix path. paths must start with /", param.Value));
throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid Windows path. paths must be a full path eg. C:\\Windows", param.Value));
}
throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid Windows path. paths must be a full path eg. C:\\Windows", param.Value));
throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid *nix path. paths must start with /", param.Value));
}
}
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Common.EnvironmentInfo
{
public interface IOperatingSystemVersionInfo
{
string Version { get; }
string Name { get; }
string FullName { get; }
}
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Common.EnvironmentInfo
{
public interface IOsVersionAdapter
{
bool Enabled { get; }
OsVersionModel Read();
}
}

View File

@@ -0,0 +1,50 @@
using System;
namespace NzbDrone.Common.EnvironmentInfo
{
public enum PlatformType
{
DotNet = 0,
Mono = 1
}
public interface IPlatformInfo
{
Version Version { get; }
}
public abstract class PlatformInfo : IPlatformInfo
{
static PlatformInfo()
{
if (Type.GetType("Mono.Runtime") != null)
{
Platform = PlatformType.Mono;
}
else
{
Platform = PlatformType.DotNet;
}
}
public static PlatformType Platform { get; }
public static bool IsMono => Platform == PlatformType.Mono;
public static bool IsDotNet => Platform == PlatformType.DotNet;
public static string PlatformName
{
get
{
if (IsDotNet)
{
return ".NET";
}
return "Mono";
}
}
public abstract Version Version { get; }
}
}

View File

@@ -1,14 +1,13 @@
namespace NzbDrone.Common.EnvironmentInfo
namespace NzbDrone.Common.EnvironmentInfo
{
public interface IRuntimeInfo
{
bool IsUserInteractive { get; }
bool IsAdmin { get; }
bool IsWindowsService { get; }
bool IsConsole { get; }
bool IsRunning { get; set; }
bool IsWindowsTray { get; }
bool IsExiting { get; set; }
bool RestartPending { get; set; }
string ExecutingApplication { get; }
string RuntimeVersion { get; }
}
}
}

View File

@@ -1,87 +1,95 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
namespace NzbDrone.Common.EnvironmentInfo
{
public static class OsInfo
public class OsInfo : IOsInfo
{
public static Os Os { get; }
public static bool IsNotWindows => !IsWindows;
public static bool IsLinux => Os == Os.Linux;
public static bool IsOsx => Os == Os.Osx;
public static bool IsWindows => Os == Os.Windows;
public string Version { get; }
public string Name { get; }
public string FullName { get; }
static OsInfo()
{
var platform = (int)Environment.OSVersion.Platform;
var platform = Environment.OSVersion.Platform;
Version = Environment.OSVersion.Version;
IsMonoRuntime = Type.GetType("Mono.Runtime") != null;
IsNotWindows = (platform == 4) || (platform == 6) || (platform == 128);
IsOsx = IsRunningOnMac();
IsLinux = IsNotWindows && !IsOsx;
IsWindows = !IsNotWindows;
FirstDayOfWeek = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
if (IsWindows)
switch (platform)
{
Os = Os.Windows;
PathStringComparison = StringComparison.OrdinalIgnoreCase;
case PlatformID.Win32NT:
{
Os = Os.Windows;
break;
}
case PlatformID.MacOSX:
case PlatformID.Unix:
{
// Sometimes Mac OS reports itself as Unix
if (Directory.Exists("/System/Library/CoreServices/") &&
(File.Exists("/System/Library/CoreServices/SystemVersion.plist") ||
File.Exists("/System/Library/CoreServices/ServerVersion.plist"))
)
{
Os = Os.Osx;
}
else
{
Os = Os.Linux;
}
break;
}
}
}
public OsInfo(IEnumerable<IOsVersionAdapter> versionAdapters, Logger logger)
{
OsVersionModel osInfo = null;
foreach (var osVersionAdapter in versionAdapters.Where(c => c.Enabled))
{
try
{
osInfo = osVersionAdapter.Read();
}
catch (Exception e)
{
logger.Error(e, "Couldn't get OS Version info");
}
if (osInfo != null)
{
break;
}
}
if (osInfo != null)
{
Name = osInfo.Name;
Version = osInfo.Version;
FullName = osInfo.FullName;
}
else
{
Os = IsOsx ? Os.Osx : Os.Linux;
PathStringComparison = StringComparison.Ordinal;
Name = Os.ToString();
FullName = Name;
}
}
}
public static Version Version { get; private set; }
public static bool IsMonoRuntime { get; private set; }
public static bool IsNotWindows { get; private set; }
public static bool IsLinux { get; private set; }
public static bool IsOsx { get; private set; }
public static bool IsWindows { get; private set; }
public static Os Os { get; private set; }
public static DayOfWeek FirstDayOfWeek { get; private set; }
public static StringComparison PathStringComparison { get; private set; }
//Borrowed from: https://github.com/jpobst/Pinta/blob/master/Pinta.Core/Managers/SystemManager.cs
//From Managed.Windows.Forms/XplatUI
[DllImport("libc")]
static extern int uname(IntPtr buf);
[DebuggerStepThrough]
static bool IsRunningOnMac()
{
var buf = IntPtr.Zero;
try
{
buf = Marshal.AllocHGlobal(8192);
// This is a hacktastic way of getting sysname from uname ()
if (uname(buf) == 0)
{
var os = Marshal.PtrToStringAnsi(buf);
if (os == "Darwin")
{
return true;
}
}
}
catch
{
}
finally
{
if (buf != IntPtr.Zero)
{
Marshal.FreeHGlobal(buf);
}
}
return false;
}
public interface IOsInfo
{
string Version { get; }
string Name { get; }
string FullName { get; }
}
public enum Os
@@ -90,4 +98,4 @@ namespace NzbDrone.Common.EnvironmentInfo
Linux,
Osx
}
}
}

View File

@@ -0,0 +1,29 @@
namespace NzbDrone.Common.EnvironmentInfo
{
public class OsVersionModel
{
public OsVersionModel(string name, string version, string fullName = null)
{
Name = Trim(name);
Version = Trim(version);
if (string.IsNullOrWhiteSpace(fullName))
{
fullName = $"{Name} {Version}";
}
FullName = Trim(fullName);
}
private static string Trim(string source)
{
return source.Trim().Trim('"', '\'');
}
public string Name { get; }
public string FullName { get; }
public string Version { get; }
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
@@ -9,11 +9,11 @@ using NzbDrone.Common.Processes;
namespace NzbDrone.Common.EnvironmentInfo
{
public abstract class RuntimeInfoBase : IRuntimeInfo
public class RuntimeInfo : IRuntimeInfo
{
private readonly Logger _logger;
public RuntimeInfoBase(IServiceProvider serviceProvider, Logger logger)
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
{
_logger = logger;
@@ -28,10 +28,11 @@ namespace NzbDrone.Common.EnvironmentInfo
if (entry != null)
{
ExecutingApplication = entry.Location;
IsWindowsTray = entry.ManifestModule.Name == $"{ProcessProvider.NZB_DRONE_PROCESS_NAME}.exe";
}
}
static RuntimeInfoBase()
static RuntimeInfo()
{
IsProduction = InternalIsProduction();
}
@@ -59,31 +60,18 @@ namespace NzbDrone.Common.EnvironmentInfo
public bool IsWindowsService { get; private set; }
public bool IsConsole
{
get
{
if (OsInfo.IsWindows)
{
return IsUserInteractive && Process.GetCurrentProcess().ProcessName.Equals(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase);
}
return true;
}
}
public bool IsRunning { get; set; }
public bool IsExiting { get; set; }
public bool RestartPending { get; set; }
public string ExecutingApplication { get; private set; }
public string ExecutingApplication { get; }
public abstract string RuntimeVersion { get; }
public static bool IsProduction { get; private set; }
public static bool IsProduction { get; }
private static bool InternalIsProduction()
{
if (BuildInfo.IsDebug || Debugger.IsAttached) return false;
if (BuildInfo.Version.Revision > 10000) return false; //Official builds will never have such a high revision
//Official builds will never have such a high revision
if (BuildInfo.Version.Revision > 10000) return false;
try
{
@@ -99,21 +87,23 @@ namespace NzbDrone.Common.EnvironmentInfo
}
try
{
var currentAssmeblyLocation = typeof(RuntimeInfoBase).Assembly.Location;
if(currentAssmeblyLocation.ToLower().Contains("_output"))return false;
}
catch
{
try
{
var currentAssmeblyLocation = typeof(RuntimeInfo).Assembly.Location;
if (currentAssmeblyLocation.ToLower().Contains("_output")) return false;
}
catch
{
}
}
string lowerCurrentDir = Directory.GetCurrentDirectory().ToLower();
var lowerCurrentDir = Directory.GetCurrentDirectory().ToLower();
if (lowerCurrentDir.Contains("teamcity")) return false;
if (lowerCurrentDir.Contains("_output")) return false;
return true;
}
public bool IsWindowsTray { get; private set; }
}
}

View File

@@ -1,31 +0,0 @@
using System;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Exceptron
{
public static class ExceptionExtentions
{
private const string IGNORE_FLAG = "exceptron_ignore";
public static Exception ExceptronIgnoreOnMono(this Exception exception)
{
if (OsInfo.IsNotWindows)
{
exception.ExceptronIgnore();
}
return exception;
}
public static Exception ExceptronIgnore(this Exception exception)
{
exception.Data.Add(IGNORE_FLAG, true);
return exception;
}
public static bool ExceptronShouldIgnore(this Exception exception)
{
return exception.Data.Contains(IGNORE_FLAG);
}
}
}

View File

@@ -1,7 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
@@ -47,7 +48,7 @@ namespace NzbDrone.Common.Extensions
{
if (!comparison.HasValue)
{
comparison = OsInfo.PathStringComparison;
comparison = DiskProviderBase.PathStringComparison;
}
if (firstPath.Equals(secondPath, comparison.Value)) return true;
@@ -93,7 +94,7 @@ namespace NzbDrone.Common.Extensions
while (child.Parent != null)
{
if (child.Parent.FullName.Equals(parent.FullName, OsInfo.PathStringComparison))
if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison))
{
return true;
}
@@ -275,4 +276,4 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE);
}
}
}
}

View File

@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Http.Dispatchers
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly Logger _logger;
private const string _caBundleFileName = "curl-ca-bundle.crt";
@@ -37,10 +38,11 @@ namespace NzbDrone.Common.Http.Dispatchers
_caBundleFilePath = _caBundleFileName;
}
}
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, Logger logger)
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_userAgentBuilder = userAgentBuilder;
_logger = logger;
}
@@ -68,94 +70,93 @@ namespace NzbDrone.Common.Http.Dispatchers
{
using (Stream responseStream = new MemoryStream())
using (Stream headerStream = new MemoryStream())
using (var curlEasy = new CurlEasy())
{
using (var curlEasy = new CurlEasy())
curlEasy.AutoReferer = false;
curlEasy.WriteFunction = (b, s, n, o) =>
{
curlEasy.AutoReferer = false;
curlEasy.WriteFunction = (b, s, n, o) =>
responseStream.Write(b, 0, s * n);
return s * n;
};
curlEasy.HeaderFunction = (b, s, n, o) =>
{
headerStream.Write(b, 0, s * n);
return s * n;
};
AddProxy(curlEasy, request);
curlEasy.Url = request.Url.FullUri;
switch (request.Method)
{
case HttpMethod.GET:
curlEasy.HttpGet = true;
break;
case HttpMethod.POST:
curlEasy.Post = true;
break;
case HttpMethod.PUT:
curlEasy.Put = true;
break;
default:
throw new NotSupportedException($"HttpCurl method {request.Method} not supported");
}
curlEasy.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
curlEasy.FollowLocation = false;
if (request.RequestTimeout != TimeSpan.Zero)
{
curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds);
}
if (OsInfo.IsWindows)
{
curlEasy.CaInfo = _caBundleFilePath;
}
if (cookies != null)
{
curlEasy.Cookie = cookies.GetCookieHeader((Uri)request.Url);
}
if (request.ContentData != null)
{
curlEasy.PostFieldSize = request.ContentData.Length;
curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v)));
}
// Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state
using (var httpRequestHeaders = SerializeHeaders(request))
{
curlEasy.HttpHeader = httpRequestHeaders;
var result = curlEasy.Perform();
if (result != CurlCode.Ok)
{
responseStream.Write(b, 0, s * n);
return s * n;
};
curlEasy.HeaderFunction = (b, s, n, o) =>
{
headerStream.Write(b, 0, s * n);
return s * n;
};
AddProxy(curlEasy, request);
curlEasy.Url = request.Url.FullUri;
switch (request.Method)
{
case HttpMethod.GET:
curlEasy.HttpGet = true;
break;
case HttpMethod.POST:
curlEasy.Post = true;
break;
case HttpMethod.PUT:
curlEasy.Put = true;
break;
default:
throw new NotSupportedException(string.Format("HttpCurl method {0} not supported", request.Method));
}
curlEasy.FollowLocation = false;
curlEasy.UserAgent = request.UseSimplifiedUserAgent ? UserAgentBuilder.UserAgentSimplified : UserAgentBuilder.UserAgent; ;
if (request.RequestTimeout != TimeSpan.Zero)
{
curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds);
}
if (OsInfo.IsWindows)
{
curlEasy.CaInfo = _caBundleFilePath;
}
if (cookies != null)
{
curlEasy.Cookie = cookies.GetCookieHeader((Uri)request.Url);
}
if (request.ContentData != null)
{
curlEasy.PostFieldSize = request.ContentData.Length;
curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v)));
}
// Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state
using (var httpRequestHeaders = SerializeHeaders(request))
{
curlEasy.HttpHeader = httpRequestHeaders;
var result = curlEasy.Perform();
if (result != CurlCode.Ok)
switch (result)
{
switch (result)
{
case CurlCode.SslCaCert:
case (CurlCode)77:
throw new WebException(string.Format("Curl Error {0} for Url {1}, issues with your operating system SSL Root Certificate Bundle (ca-bundle).", result, curlEasy.Url));
default:
throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url));
}
case CurlCode.SslCaCert:
case (CurlCode)77:
throw new WebException(string.Format("Curl Error {0} for Url {1}, issues with your operating system SSL Root Certificate Bundle (ca-bundle).", result, curlEasy.Url));
default:
throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url));
}
}
var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream);
var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection);
var httpHeader = new HttpHeader(webHeaderCollection);
return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
}
var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream);
var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection);
var httpHeader = new HttpHeader(webHeaderCollection);
return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
}
}
}
@@ -243,7 +244,7 @@ namespace NzbDrone.Common.Http.Dispatchers
private string FixSetCookieHeader(string setCookie)
{
// fix up the date if it was malformed
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
var setCookieClean = ExpiryDate.Replace(setCookie, delegate (Match match)
{
string shortFormat = "ddd, dd-MMM-yy HH:mm:ss";
string longFormat = "ddd, dd-MMM-yyyy HH:mm:ss";
@@ -260,7 +261,6 @@ namespace NzbDrone.Common.Http.Dispatchers
private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, WebHeaderCollection webHeaderCollection)
{
byte[] bytes = null;
responseStream.Position = 0;
if (responseStream.Length != 0)
@@ -270,27 +270,20 @@ namespace NzbDrone.Common.Http.Dispatchers
{
if (encoding.IndexOf("gzip") != -1)
{
using (var zipStream = new GZipStream(responseStream, CompressionMode.Decompress))
{
bytes = zipStream.ToBytes();
}
responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
webHeaderCollection.Remove("Content-Encoding");
}
else if (encoding.IndexOf("deflate") != -1)
{
using (var deflateStream = new DeflateStream(responseStream, CompressionMode.Decompress))
{
bytes = deflateStream.ToBytes();
}
responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);
webHeaderCollection.Remove("Content-Encoding");
}
}
}
if (bytes == null) bytes = responseStream.ToBytes();
return bytes;
return responseStream.ToBytes();
}
}

View File

@@ -10,21 +10,23 @@ namespace NzbDrone.Common.Http.Dispatchers
{
private readonly ManagedHttpDispatcher _managedDispatcher;
private readonly CurlHttpDispatcher _curlDispatcher;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
private readonly ICached<bool> _curlTLSFallbackCache;
public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, Logger logger)
public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, IPlatformInfo platformInfo, Logger logger)
{
_managedDispatcher = managedDispatcher;
_curlDispatcher = curlDispatcher;
_platformInfo = platformInfo;
_curlTLSFallbackCache = cacheManager.GetCache<bool>(GetType(), "curlTLSFallback");
_logger = logger;
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{
if (OsInfo.IsMonoRuntime && request.Url.Scheme == "https")
if (PlatformInfo.IsMono && request.Url.Scheme == "https")
{
if (!_curlTLSFallbackCache.Find(request.Url.Host))
{

View File

@@ -2,9 +2,13 @@ using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Reflection;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Security;
namespace NzbDrone.Common.Http.Dispatchers
@@ -13,55 +17,59 @@ namespace NzbDrone.Common.Http.Dispatchers
{
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy)
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
_userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger;
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{
HttpWebResponse httpWebResponse = null;
HttpWebRequest webRequest = null;
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
if (PlatformInfo.IsMono)
{
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
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.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
{
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
}
AddProxy(webRequest, request);
if (request.Headers != null)
{
AddRequestHeaders(webRequest, request.Headers);
}
HttpWebResponse httpWebResponse;
try
{
webRequest = (HttpWebRequest) WebRequest.Create((Uri) request.Url);
if (OsInfo.IsMonoRuntime)
{
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case.
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.UserAgent = request.UseSimplifiedUserAgent
? UserAgentBuilder.UserAgentSimplified
: UserAgentBuilder.UserAgent;
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
{
webRequest.Timeout = (int) Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
}
AddProxy(webRequest, request);
if (request.Headers != null)
{
AddRequestHeaders(webRequest, request.Headers);
}
if (request.ContentData != null)
{
webRequest.ContentLength = request.ContentData.Length;
@@ -71,34 +79,57 @@ namespace NzbDrone.Common.Http.Dispatchers
}
}
try
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
}
catch (WebException e)
{
if (e.Status == WebExceptionStatus.SecureChannelFailure && OsInfo.IsWindows)
{
httpWebResponse = (HttpWebResponse) webRequest.GetResponse();
SecurityProtocolPolicy.DisableTls12();
}
catch (WebException e)
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{
if (e.Status == WebExceptionStatus.SecureChannelFailure && OsInfo.IsWindows)
// Workaround for mono not closing connections properly in certain situations.
AbortWebRequest(webRequest);
// The default messages for WebException on mono are pretty horrible.
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{
SecurityProtocolPolicy.DisableTls12();
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status);
}
httpWebResponse = (HttpWebResponse) e.Response;
if (httpWebResponse == null)
else if (e.ToString().Contains("TLS Support not"))
{
throw new TlsFailureException(webRequest, e);
}
else if (e.ToString().Contains("The authentication or decryption has failed."))
{
throw new TlsFailureException(webRequest, e);
}
else if (OsInfo.IsNotWindows)
{
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
}
else
{
throw;
}
}
}
byte[] data = null;
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null && responseStream != Stream.Null)
{
if (responseStream != null)
try
{
data = responseStream.ToBytes();
if (OsInfo.IsMonoRuntime && httpWebResponse.ContentEncoding == "gzip")
if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip")
{
using (var compressedStream = new MemoryStream(data))
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
@@ -111,17 +142,14 @@ namespace NzbDrone.Common.Http.Dispatchers
httpWebResponse.Headers.Remove("Content-Encoding");
}
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
}
}
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data,
httpWebResponse.StatusCode);
}
finally
{
webRequest = null;
(httpWebResponse as IDisposable)?.Dispose();
httpWebResponse = null;
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
}
protected virtual void AddProxy(HttpWebRequest webRequest, HttpRequest request)
@@ -181,5 +209,35 @@ 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)
.Write();
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -31,12 +31,19 @@ namespace NzbDrone.Common.Http
private readonly ICached<CookieContainer> _cookieContainerCache;
private readonly List<IHttpRequestInterceptor> _requestInterceptors;
private readonly IHttpDispatcher _httpDispatcher;
private readonly IUserAgentBuilder _userAgentBuilder;
public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, IHttpDispatcher httpDispatcher, Logger logger)
public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors,
ICacheManager cacheManager,
IRateLimitService rateLimitService,
IHttpDispatcher httpDispatcher,
IUserAgentBuilder userAgentBuilder,
Logger logger)
{
_requestInterceptors = requestInterceptors.ToList();
_rateLimitService = rateLimitService;
_httpDispatcher = httpDispatcher;
_userAgentBuilder = userAgentBuilder;
_logger = logger;
ServicePointManager.DefaultConnectionLimit = 12;
@@ -71,7 +78,7 @@ namespace NzbDrone.Common.Http
while (response.HasHttpRedirect);
}
if (response.HasHttpRedirect && !RuntimeInfoBase.IsProduction)
if (response.HasHttpRedirect && !RuntimeInfo.IsProduction)
{
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
}
@@ -143,19 +150,30 @@ namespace NzbDrone.Common.Http
if (!request.IgnorePersistentCookies)
{
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
sourceContainer.Add(persistentCookies);
sourceContainer.Add(persistentCookies);
}
if (request.Cookies.Count != 0)
{
foreach (var pair in request.Cookies)
{
var cookie = new Cookie(pair.Key, pair.Value, "/")
Cookie cookie;
if (pair.Value == null)
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
cookie = new Cookie(pair.Key, "", "/")
{
Expires = DateTime.Now.AddDays(-1)
};
}
else
{
cookie = new Cookie(pair.Key, pair.Value, "/")
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
}
sourceContainer.Add((Uri)request.Url, cookie);
@@ -178,7 +196,6 @@ namespace NzbDrone.Common.Http
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies);
}*/
@@ -226,13 +243,11 @@ namespace NzbDrone.Common.Http
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, UserAgentBuilder.UserAgent);
webClient.DownloadFile(url, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
var webClient = new GZipWebClient();
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.DownloadFile(url, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
catch (WebException e)
{
@@ -255,6 +270,7 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Get<T>(HttpRequest request) where T : new()
{
var response = Get(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
@@ -273,7 +289,16 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Post<T>(HttpRequest request) where T : new()
{
var response = Post(request);
CheckResponseContentType(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);
}
}
}
}

View File

@@ -7,13 +7,19 @@ namespace NzbDrone.Common.Http
public HttpRequest Request { get; private set; }
public HttpResponse Response { get; private set; }
public HttpException(HttpRequest request, HttpResponse response)
: base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url))
public HttpException(HttpRequest request, HttpResponse response, string message)
: base(message)
{
Request = request;
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)
: this(response.Request, response)
{
@@ -30,4 +36,4 @@ namespace NzbDrone.Common.Http
return base.ToString();
}
}
}
}

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Common.Http
IgnorePersistentCookies = false;
Cookies = new Dictionary<string, string>();
if (!RuntimeInfoBase.IsProduction)
if (!RuntimeInfo.IsProduction)
{
AllowAutoRedirect = false;
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
namespace NzbDrone.Common.Http
{
public class TlsFailureException : WebException
{
public TlsFailureException(WebRequest request, WebException innerException)
: base("Failed to establish secure https connection to '" + request.RequestUri + "', libcurl fallback might be unavailable.", innerException, WebExceptionStatus.SecureChannelFailure, innerException.Response)
{
}
}
}

View File

@@ -0,0 +1,14 @@
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}]")
{
}
}
}

View File

@@ -2,19 +2,39 @@ using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Http
{
public static class UserAgentBuilder
public interface IUserAgentBuilder
{
public static string UserAgent { get; private set; }
public static string UserAgentSimplified { get; private set; }
string GetUserAgent(bool simplified = false);
}
static UserAgentBuilder()
public class UserAgentBuilder : IUserAgentBuilder
{
private readonly string _userAgentSimplified;
private readonly string _userAgent;
public string GetUserAgent(bool simplified)
{
UserAgent = string.Format("Radarr/{0} ({1} {2})",
BuildInfo.Version,
OsInfo.Os, OsInfo.Version.ToString(2));
if (simplified)
{
return _userAgentSimplified;
}
UserAgentSimplified = string.Format("Radarr/{0}",
BuildInfo.Version.ToString(2));
return _userAgent;
}
public UserAgentBuilder(IOsInfo osInfo)
{
var osName = OsInfo.Os.ToString();
if (!string.IsNullOrWhiteSpace(osInfo.Name))
{
osName = osInfo.Name.ToLower();
}
var osVersion = osInfo.Version?.ToLower();
_userAgent = $"Radarr/{BuildInfo.Version} ({osName} {osVersion})";
_userAgentSimplified = $"Radarr/{BuildInfo.Version.ToString(2)}";
}
}
}

View File

@@ -1,89 +0,0 @@
using System;
using NLog;
using NLog.Common;
using NLog.Layouts;
using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptron;
using NzbDrone.Common.Exceptron.Configuration;
namespace NzbDrone.Common.Instrumentation
{
/// <summary>
/// <see cref="NLog"/> target for exceptron. Allows you to automatically report all
/// exceptions logged to Nlog/>
/// </summary>
[Target("Exceptron")]
public class ExceptronTarget : Target
{
/// <summary>
/// <see cref="ExceptronClient"/> instance that Nlog Target uses to report the exceptions.
/// </summary>
public IExceptronClient ExceptronClient { get; internal set; }
protected override void InitializeTarget()
{
var config = new ExceptronConfiguration
{
ApiKey = "d64e0a72845d495abc625af3a27cf5f5",
IncludeMachineName = true,
};
if (RuntimeInfoBase.IsProduction)
{
config.ApiKey = "82c0f66dd2d64d1480cc88b551c9bdd8";
}
ExceptronClient = new ExceptronClient(config, BuildInfo.Version);
}
/// <summary>
/// String that identifies the active user
/// </summary>
public Layout UserId { get; set; }
protected override void Write(LogEventInfo logEvent)
{
if (logEvent == null || logEvent.Exception == null || logEvent.Exception.ExceptronShouldIgnore()) return;
try
{
var exceptionData = new ExceptionData
{
Exception = logEvent.Exception,
Component = logEvent.LoggerName,
Message = logEvent.FormattedMessage,
};
if (UserId != null)
{
exceptionData.UserId = UserId.Render(logEvent);
}
if (logEvent.Level <= LogLevel.Info)
{
exceptionData.Severity = ExceptionSeverity.None;
}
else if (logEvent.Level <= LogLevel.Warn)
{
exceptionData.Severity = ExceptionSeverity.Warning;
}
else if (logEvent.Level <= LogLevel.Error)
{
exceptionData.Severity = ExceptionSeverity.Error;
}
else if (logEvent.Level <= LogLevel.Fatal)
{
exceptionData.Severity = ExceptionSeverity.Fatal;
}
ExceptronClient.SubmitException(exceptionData);
}
catch (Exception e)
{
InternalLogger.Warn("Unable to report exception. {0}", e);
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
@@ -35,7 +35,7 @@ namespace NzbDrone.Common.Instrumentation
return;
}
if (OsInfo.IsMonoRuntime)
if (PlatformInfo.IsMono)
{
if (exception is TypeInitializationException && exception.InnerException is DllNotFoundException ||
exception is DllNotFoundException)
@@ -51,4 +51,4 @@ namespace NzbDrone.Common.Instrumentation
Logger.Fatal(exception, "EPIC FAIL: " + exception.Message);
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using LogentriesNLog;
@@ -48,7 +48,7 @@ namespace NzbDrone.Common.Instrumentation
}
else
{
if (inConsole && (OsInfo.IsNotWindows || RuntimeInfoBase.IsUserInteractive))
if (inConsole && (OsInfo.IsNotWindows || RuntimeInfo.IsUserInteractive))
{
RegisterConsole();
}
@@ -152,16 +152,6 @@ namespace NzbDrone.Common.Instrumentation
LogManager.Configuration.LoggingRules.Add(loggingRule);
}
private static void RegisterExceptron()
{
var exceptronTarget = new ExceptronTarget();
var rule = new LoggingRule("*", LogLevel.Warn, exceptronTarget);
LogManager.Configuration.AddTarget("ExceptronTarget", exceptronTarget);
LogManager.Configuration.LoggingRules.Add(rule);
}
public static Logger GetLogger(Type obj)
{
return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));

View File

@@ -94,6 +94,10 @@
<Compile Include="Disk\RelativeFileSystemModel.cs" />
<Compile Include="Disk\FileSystemModel.cs" />
<Compile Include="Disk\FileSystemResult.cs" />
<Compile Include="EnvironmentInfo\IOperatingSystemVersionInfo.cs" />
<Compile Include="EnvironmentInfo\IOsVersionAdapter.cs" />
<Compile Include="EnvironmentInfo\IPlatformInfo.cs" />
<Compile Include="EnvironmentInfo\OsVersionModel.cs" />
<Compile Include="Extensions\DictionaryExtensions.cs" />
<Compile Include="Disk\GdiPlusInterop.cs" />
<Compile Include="Disk\OsPath.cs" />
@@ -125,13 +129,12 @@
<Compile Include="EnvironmentInfo\BuildInfo.cs" />
<Compile Include="EnvironmentInfo\OsInfo.cs" />
<Compile Include="EnvironmentInfo\IRuntimeInfo.cs" />
<Compile Include="EnvironmentInfo\RuntimeInfoBase.cs" />
<Compile Include="EnvironmentInfo\RuntimeInfo.cs" />
<Compile Include="EnvironmentInfo\StartupContext.cs" />
<Compile Include="Exceptions\NotParentException.cs" />
<Compile Include="Exceptions\NzbDroneException.cs" />
<Compile Include="Exceptron\Configuration\ExceptronConfiguration.cs" />
<Compile Include="Exceptron\ExceptionData.cs" />
<Compile Include="Exceptron\ExceptionExtentions.cs" />
<Compile Include="Exceptron\ExceptionSeverity.cs" />
<Compile Include="Exceptron\ExceptronApiException.cs" />
<Compile Include="Exceptron\ExceptronClient.cs" />
@@ -190,11 +193,12 @@
<Compile Include="Http\HttpRequestBuilder.cs" />
<Compile Include="Http\HttpRequestBuilderFactory.cs" />
<Compile Include="Http\Proxy\ProxyType.cs" />
<Compile Include="Http\TlsFailureException.cs" />
<Compile Include="Http\TooManyRequestsException.cs" />
<Compile Include="Extensions\IEnumerableExtensions.cs" />
<Compile Include="Http\UnexpectedHtmlContentException.cs" />
<Compile Include="Http\UserAgentBuilder.cs" />
<Compile Include="Instrumentation\CleanseLogMessage.cs" />
<Compile Include="Instrumentation\ExceptronTarget.cs" />
<Compile Include="Instrumentation\Extensions\LoggerProgressExtensions.cs" />
<Compile Include="Instrumentation\GlobalExceptionHandlers.cs" />
<Compile Include="Instrumentation\LogEventExtensions.cs" />

View File

@@ -108,7 +108,7 @@ namespace NzbDrone.Common.Processes
public Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action<string> onOutputDataReceived = null, Action<string> onErrorDataReceived = null)
{
if (OsInfo.IsMonoRuntime && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
args = GetMonoArgs(path, args);
path = "mono";
@@ -192,7 +192,7 @@ namespace NzbDrone.Common.Processes
public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null)
{
if (OsInfo.IsMonoRuntime && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
{
args = GetMonoArgs(path, args);
path = "mono";

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
@@ -37,8 +38,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences());
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences() { DhtEnabled = true });
Mocker.GetMock<IQBittorrentProxySelector>()
.Setup(s => s.GetProxy(It.IsAny<QBittorrentSettings>(), It.IsAny<bool>()))
.Returns(Mocker.GetMock<IQBittorrentProxy>().Object);
}
protected void GivenRedirectToMagnet()
@@ -154,7 +159,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
VerifyPaused(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
item.RemainingTime.Should().NotHaveValue();
}
[TestCase("pausedUP")]
@@ -162,6 +167,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("uploading")]
[TestCase("stalledUP")]
[TestCase("checkingUP")]
[TestCase("forcedUP")]
public void completed_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
@@ -184,6 +190,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("queuedDL")]
[TestCase("checkingDL")]
[TestCase("metaDL")]
public void queued_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
@@ -201,7 +208,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
VerifyQueued(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
item.RemainingTime.Should().NotHaveValue();
}
[Test]
@@ -243,7 +250,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
VerifyWarning(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
item.RemainingTime.Should().NotHaveValue();
}
[Test]
@@ -271,6 +278,35 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
id.Should().Be(expectedHash);
}
[Test]
public void Download_should_refuse_magnet_if_no_trackers_provided_and_dht_is_disabled()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences() { DhtEnabled = false });
var remoteMovie = CreateRemoteMovie();
remoteMovie.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR";
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteMovie));
}
[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 remoteMovie = CreateRemoteMovie();
remoteMovie.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp://abc";
Assert.DoesNotThrow(() => Subject.Download(remoteMovie));
Mocker.GetMock<IQBittorrentProxy>()
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]
public void Download_should_set_top_priority()
{
@@ -494,5 +530,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
item.Category.Should().Be(category);
}
[Test]
public void Test_should_force_api_version_check()
{
// Set TestConnection up to fail quick
Mocker.GetMock<IQBittorrentProxy>()
.Setup(v => v.GetApiVersion(It.IsAny<QBittorrentSettings>()))
.Returns(new Version(1, 0));
Subject.Test();
Mocker.GetMock<IQBittorrentProxySelector>()
.Verify(v => v.GetProxy(It.IsAny<QBittorrentSettings>(), true), Times.Once());
}
}
}

View File

@@ -1,9 +1,9 @@
using System.Collections.Specialized;
using System.Security.AccessControl;
using Moq;
using System;
using NUnit.Framework;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.TPL;
@@ -24,12 +24,16 @@ namespace NzbDrone.Core.Test.Framework
{
protected void UseRealHttp()
{
Mocker.GetMock<IPlatformInfo>().SetupGet(c => c.Version).Returns(new Version("3.0.0"));
Mocker.GetMock<IOsInfo>().SetupGet(c => c.Version).Returns("1.0.0");
Mocker.GetMock<IOsInfo>().SetupGet(c => c.Name).Returns("TestOS");
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>()));
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<NLog.Logger>()));
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger));
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
}

View File

@@ -1,4 +1,5 @@
using NUnit.Framework;
using System;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
@@ -8,17 +9,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[TestFixture]
public class MonoVersionCheckFixture : CoreTest<MonoVersionCheck>
{
[SetUp]
public void Setup()
{
MonoOnly();
}
private void GivenOutput(string version)
{
Mocker.GetMock<IRuntimeInfo>()
.SetupGet(s => s.RuntimeVersion)
.Returns(string.Format("{0} (tarball Wed Sep 25 16:35:44 CDT 2013)", version));
MonoOnly();
Mocker.GetMock<IPlatformInfo>()
.SetupGet(s => s.Version)
.Returns(new Version(version));
}
[TestCase("3.10")]

View File

@@ -18,7 +18,8 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
}
[TestCase("Prometheus", "Prometheus")]
[TestCase("The Man from U.N.C.L.E.", "The Man from U.N.C.L.E.")]
// TODO: TMDB does not like when we clean '.'
// [TestCase("The Man from U.N.C.L.E.", "The Man from U.N.C.L.E.")]
[TestCase("imdb:tt2527336", "Star Wars: The Last Jedi")]
[TestCase("imdb:tt2798920", "Annihilation")]
public void successful_search(string title, string expected)

View File

@@ -0,0 +1,32 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MovieTests.AlternativeTitleServiceTests
{
[TestFixture]
public class AlternativeTitleFixture : CoreTest
{
private AlternativeTitle CreateFakeTitle(SourceType source, int votes)
{
return Builder<AlternativeTitle>.CreateNew().With(t => t.SourceType = source).With(t => t.Votes = votes)
.Build();
}
[TestCase(SourceType.TMDB, -1, true)]
[TestCase(SourceType.TMDB, 1000, true)]
[TestCase(SourceType.Mappings, 0, false)]
[TestCase(SourceType.Mappings, 4, true)]
[TestCase(SourceType.Mappings, -1, false)]
[TestCase(SourceType.Indexer, 0, true)]
[TestCase(SourceType.User, 0, true)]
public void should_be_trusted(SourceType source, int votes, bool trusted)
{
var fakeTitle = CreateFakeTitle(source, votes);
fakeTitle.IsTrusted().Should().Be(trusted);
}
}
}

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MovieTests.AlternativeTitleServiceTests
{
[TestFixture]
public class AlternativeTitleServiceFixture : CoreTest<AlternativeTitleService>
{
private AlternativeTitle _title1;
private AlternativeTitle _title2;
private AlternativeTitle _title3;
private Movie _movie;
[SetUp]
public void Setup()
{
var titles = Builder<AlternativeTitle>.CreateListOfSize(3).All().With(t => t.MovieId = 0).Build();
_title1 = titles[0];
_title2 = titles[1];
_title3 = titles[2];
_movie = Builder<Movie>.CreateNew().With(m => m.CleanTitle = "myothertitle").With(m => m.Id = 1).Build();
}
private void GivenExistingTitles(params AlternativeTitle[] titles)
{
Mocker.GetMock<IAlternativeTitleRepository>().Setup(r => r.FindByMovieId(_movie.Id))
.Returns(titles.ToList());
}
[Test]
public void should_update_insert_remove_titles()
{
var titles = new List<AlternativeTitle> {_title2, _title3};
var updates = new List<AlternativeTitle> {_title2};
var deletes = new List<AlternativeTitle> {_title1};
var inserts = new List<AlternativeTitle> {_title3};
GivenExistingTitles(_title1, _title2);
Subject.UpdateTitles(titles, _movie);
Mocker.GetMock<IAlternativeTitleRepository>().Verify(r => r.InsertMany(inserts), Times.Once());
Mocker.GetMock<IAlternativeTitleRepository>().Verify(r => r.UpdateMany(updates), Times.Once());
Mocker.GetMock<IAlternativeTitleRepository>().Verify(r => r.DeleteMany(deletes), Times.Once());
}
[Test]
public void should_not_insert_duplicates()
{
GivenExistingTitles();
var titles = new List<AlternativeTitle> {_title1, _title1};
var inserts = new List<AlternativeTitle>{ _title1 };
Subject.UpdateTitles(titles, _movie);
Mocker.GetMock<IAlternativeTitleRepository>().Verify(r => r.InsertMany(inserts), Times.Once());
}
[Test]
public void should_not_insert_main_title()
{
GivenExistingTitles();
var titles = new List<AlternativeTitle>{_title1};
var movie = Builder<Movie>.CreateNew().With(m => m.CleanTitle = _title1.CleanTitle).Build();
Subject.UpdateTitles(titles, movie);
Mocker.GetMock<IAlternativeTitleRepository>().Verify(r => r.InsertMany(new List<AlternativeTitle>()), Times.Once());
}
[Test]
public void should_update_movie_id()
{
GivenExistingTitles();
var titles = new List<AlternativeTitle> {_title1, _title2};
Subject.UpdateTitles(titles, _movie);
_title1.MovieId.Should().Be(_movie.Id);
_title2.MovieId.Should().Be(_movie.Id);
}
[Test]
public void should_update_with_correct_id()
{
var existingTitle = Builder<AlternativeTitle>.CreateNew().With(t => t.Id = 2).Build();
GivenExistingTitles(existingTitle);
var updateTitle = existingTitle.JsonClone();
updateTitle.Id = 0;
Subject.UpdateTitles(new List<AlternativeTitle> {updateTitle}, _movie);
Mocker.GetMock<IAlternativeTitleRepository>().Verify(r => r.UpdateMany(It.Is<IList<AlternativeTitle>>(list => list.First().Id == existingTitle.Id)), Times.Once());
}
}
}

View File

@@ -296,6 +296,8 @@
<Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchMovieComparerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.cs" />
<Compile Include="MovieTests\AlternativeTitleServiceTests\AlternativeTitleFixture.cs" />
<Compile Include="MovieTests\AlternativeTitleServiceTests\AlternativeTitleServiceFixture.cs" />
<Compile Include="NetImport\CouchPotato\CouchPotatoParserFixture.cs" />
<Compile Include="NetImport\RSSImportFixture.cs" />
<Compile Include="NetImport\RSSImportParserFixture.cs" />

View File

@@ -26,6 +26,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL", Language.Mandarin)]
[TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)]
[TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL", Language.Russian)]
[TestCase("Castle.2009.S01E14.Ukrainian.HDTV.XviD-LOL", Language.Ukrainian)]
[TestCase("Castle.2009.S01E14.Ukr.HDTV.XviD-LOL", Language.Ukrainian)]
[TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL", Language.Polish)]
[TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)]
[TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL", Language.Swedish)]
@@ -68,6 +70,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("2 Broke Girls - S01E01 - Pilot.sub", Language.Unknown)]
[TestCase("2 Broke Girls - S01E01 - Pilot.eng.forced.sub", Language.English)]
[TestCase("2 Broke Girls - S01E01 - Pilot-eng-forced.sub", Language.English)]
[TestCase("2_Eng.srt", Language.English)]
[TestCase("3_English.srt", Language.English)]
[TestCase("Title.2000.1080p.BluRay.H264.AAC-RARBG.idx", Language.Unknown)]
[TestCase("Title.2000.1080p.BluRay.H264.AAC-RARBG.sub", Language.Unknown)]
public void should_parse_subtitle_language(string fileName, Language language)
{
var result = LanguageParser.ParseSubtitleLanguage(fileName);

View File

@@ -251,6 +251,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Contract.to.Kill.2016.REMUX.2160p.BluRay.AVC.DTS-HD.MA.5.1-iFT")]
[TestCase("27.Dresses.2008.REMUX.2160p.Bluray.AVC.DTS-HR.MA.5.1-LEGi0N")]
[TestCase("Los Vengadores (2012) [UHDRemux HDR HEVC 2160p][Dolby Atmos TrueHD 7 1 Eng DTS 5 1 Esp]")]
public void should_parse_remux2160p_quality(string title)
{
ParseAndVerifyQuality(title, Source.BLURAY, false, Resolution.R2160P, Modifier.REMUX);

View File

@@ -29,6 +29,16 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Middle.720p.HEVC.x265-MeGusta-Pre", "MeGusta")]
[TestCase("Haunted.Hayride.2018.720p.WEBRip.DDP5.1.x264-NTb-postbot", "NTb")]
[TestCase("Haunted.Hayride.2018.720p.WEBRip.DDP5.1.x264-NTb-xpost", "NTb")]
[TestCase("Men.in.Black.International.2019.BluRay.1080p.AVC.DTS-HD.MA5.1-CHDBits-RakuvArrow", "CHDBits")]
[TestCase("Aladdin.2019.1080p.BluRay.x264-SPARKS-WhiteRev", "SPARKS")]
[TestCase("Elvis.Presley.The.Searcher.2018.1080p.BluRay.x264-HANDJOB-BUYMORE", "HANDJOB")]
[TestCase("Kill.Bill.Vol.2.2004.1080p.BluRay.DTS.x264-CyTSuNee-AsRequested", "CyTSuNee")]
[TestCase("The.Good.Doctor.S02E17.Breakdown.1080p.AMZN.WEB-DL.DDP5.1.H.264-SiGMA-AlternativeToRequested", "SiGMA")]
[TestCase("Mandy.2018.NORDiC.1080p.BluRay.x264-EGEN-GEROV", "EGEN")]
[TestCase("TheEqualizer.2.2018.1080p.BluRay.DTS.X264-CMRG-Z0iDS3N", "CMRG")]
[TestCase("Ghosthouse.1988.720p.BluRay.x264-SADPANDA-Chamele0n", "SADPANDA")]
[TestCase("The.Walking.Dead.S08E08.1080p.BluRay.x264-ROVERS-4P", "ROVERS")]
[TestCase("Stranger.Things.S01E02.720p.BluRay.X264-REWARD-4Planet", "REWARD")]
//[TestCase("", "")]
public void should_parse_release_group(string title, string expected)
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Update;
@@ -10,6 +11,12 @@ namespace NzbDrone.Core.Test.UpdateTests
{
public class UpdatePackageProviderFixture : CoreTest<UpdatePackageProvider>
{
[SetUp]
public void Setup()
{
Mocker.GetMock<IPlatformInfo>().SetupGet(c => c.Version).Returns(new Version("9.9.9"));
}
[Test]
public void no_update_when_version_higher()
{

View File

@@ -17,6 +17,6 @@ namespace NzbDrone.Core.Analytics
_configFileProvider = configFileProvider;
}
public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfoBase.IsProduction;
public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfo.IsProduction;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NLog;
using NzbDrone.Common.EnsureThat;
@@ -373,7 +374,7 @@ namespace NzbDrone.Core.Configuration
public int FirstDayOfWeek
{
get { return GetValueInt("FirstDayOfWeek", (int)OsInfo.FirstDayOfWeek); }
get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); }
set { SetValue("FirstDayOfWeek", value); }
}

View File

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

View File

@@ -0,0 +1,27 @@
using System;
namespace NzbDrone.Core.Download.Clients
{
public class DownloadClientUnavailableException : DownloadClientException
{
public DownloadClientUnavailableException(string message, params object[] args)
: base(string.Format(message, args))
{
}
public DownloadClientUnavailableException(string message)
: base(message)
{
}
public DownloadClientUnavailableException(string message, Exception innerException, params object[] args)
: base(string.Format(message, args), innerException)
{
}
public DownloadClientUnavailableException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -18,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
{
private readonly IQBittorrentProxy _proxy;
private readonly IQBittorrentProxySelector _proxySelector;
public QBittorrent(IQBittorrentProxy proxy,
public QBittorrent(IQBittorrentProxySelector proxySelector,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
@@ -30,16 +30,23 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
_proxySelector = proxySelector;
}
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
protected override string AddFromMagnetLink(RemoteMovie remoteMovie, 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.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings);
Proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings);
}
var isRecentMovie = remoteMovie.Movie.IsRecentMovie;
@@ -47,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First ||
!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
SetInitialState(hash.ToLower());
@@ -57,13 +64,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, Byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
try
{
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings);
Proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings);
}
}
catch (Exception ex)
@@ -78,7 +85,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
if (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First ||
!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
}
catch (Exception ex)
@@ -100,8 +107,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
try
{
config = _proxy.GetConfig(Settings);
torrents = _proxy.GetTorrents(Settings);
config = Proxy.GetConfig(Settings);
torrents = Proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
@@ -113,17 +120,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
foreach (var torrent in torrents)
{
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper();
item.Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label;
item.Title = torrent.Name;
item.TotalSize = torrent.Size;
item.DownloadClient = Definition.Name;
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
item.RemainingTime = GetRemainingTime(torrent);
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
var item = new DownloadClientItem()
{
DownloadId = torrent.Hash.ToUpper(),
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
Title = torrent.Name,
TotalSize = torrent.Size,
DownloadClient = Definition.Name,
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
RemainingTime = GetRemainingTime(torrent),
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)),
};
// Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.CanMoveFiles = item.CanBeRemoved = (!config.MaxRatioEnabled || config.MaxRatio <= torrent.Ratio) && torrent.State == "pausedUP";
@@ -154,7 +161,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload
case "checkingUP": // torrent has finished downloading and is being checked
case "forcedUP": // torrent is beeing seeded by force
case "forcedUP": // torrent has finished downloading and is being forcibly seeded
item.Status = DownloadItemStatus.Completed;
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
break;
@@ -164,13 +171,26 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = "The download is stalled with no connections";
break;
case "metaDL": // torrent magnet is being downloaded
if (config.DhtEnabled)
{
item.Status = DownloadItemStatus.Queued;
}
else
{
item.Status = DownloadItemStatus.Warning;
item.Message = "qBittorrent cannot resolve magnet link with DHT disabled";
}
break;
case "moving": // torrent is being moved from a folder
case "downloading": // torrent is being downloaded and data is being transfered
item.Status = DownloadItemStatus.Downloading;
break;
default: // new status in API? default to downloading
item.Message = "Unknown download state: " + torrent.State;
_logger.Warn(item.Message);
_logger.Info(item.Message);
item.Status = DownloadItemStatus.Downloading;
break;
}
@@ -183,12 +203,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public override void RemoveItem(string hash, bool deleteData)
{
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
Proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
}
public override DownloadClientStatus GetStatus()
{
var config = _proxy.GetConfig(Settings);
var config = Proxy.GetConfig(Settings);
var destDir = new OsPath(config.SavePath);
@@ -211,8 +231,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
try
{
var version = _proxy.GetVersion(Settings);
if (version < 5)
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
if (version < Version.Parse("1.5"))
{
// API version 5 introduced the "save_path" property in /query/torrents
return new NzbDroneValidationFailure("Host", "Unsupported client version")
@@ -220,7 +240,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
};
}
else if (version < 6)
else if (version < Version.Parse("1.6"))
{
// API version 6 introduced support for labels
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
@@ -234,7 +254,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
else if (Settings.MovieCategory.IsNullOrWhiteSpace())
{
// warn if labels are supported, but category is not provided
return new NzbDroneValidationFailure("TvCategory", "Category is recommended")
return new NzbDroneValidationFailure("MovieCategory", "Category is recommended")
{
IsWarning = true,
DetailedDescription = "Radarr will not attempt to import completed downloads without a category."
@@ -242,7 +262,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
// 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)
{
return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
@@ -292,7 +312,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
try
{
var config = _proxy.GetConfig(Settings);
var config = Proxy.GetConfig(Settings);
if (!config.QueueingEnabled)
{
@@ -319,7 +339,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
try
{
_proxy.GetTorrents(Settings);
Proxy.GetTorrents(Settings);
}
catch (Exception ex)
{
@@ -337,13 +357,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
switch ((QBittorrentState)Settings.InitialState)
{
case QBittorrentState.ForceStart:
_proxy.SetForceStart(hash, true, Settings);
Proxy.SetForceStart(hash, true, Settings);
break;
case QBittorrentState.Start:
_proxy.ResumeTorrent(hash, Settings);
Proxy.ResumeTorrent(hash, Settings);
break;
case QBittorrentState.Pause:
_proxy.PauseTorrent(hash, Settings);
Proxy.PauseTorrent(hash, Settings);
break;
}
}
@@ -360,6 +380,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return null;
}
// qBittorrent sends eta=8640000 if unknown such as queued
if (torrent.Eta == 8640000)
{
return null;
}
return TimeSpan.FromSeconds((int) torrent.Eta);
}
}

View File

@@ -19,5 +19,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[JsonProperty(PropertyName = "queueing_enabled")]
public bool QueueingEnabled { get; set; } = true;
[JsonProperty(PropertyName = "dht")]
public bool DhtEnabled { get; set; } // DHT enabled (needed for more peers and magnet downloads)
}
}

View File

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

View File

@@ -11,41 +11,68 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
public interface IQBittorrentProxy
{
int GetVersion(QBittorrentSettings settings);
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void PauseTorrent(string hash, QBittorrentSettings settings);
void ResumeTorrent(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
}
public class QBittorrentProxy : IQBittorrentProxy
public class QBittorrentProxyV1 : IQBittorrentProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private readonly ICached<Dictionary<string, string>> _authCookieCache;
public QBittorrentProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
public QBittorrentProxyV1(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
}
public int GetVersion(QBittorrentSettings settings)
public bool IsApiSupported(QBittorrentSettings settings)
{
// We can do the api test without having to authenticate since v4.1 will return 404 on the request.
var request = BuildRequest(settings).Resource("/version/api");
var response = ProcessRequest<int>(request, settings);
request.SuppressHttpError = true;
try
{
var response = _httpClient.Execute(request.Build());
// Version request will return 404 if it doesn't exist.
if (response.StatusCode == HttpStatusCode.NotFound)
{
return false;
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
return true;
}
if (response.HasHttpError)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
}
return true;
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
public Version GetApiVersion(QBittorrentSettings settings)
{
// Version request does not require authentication and will return 404 if it doesn't exist.
var request = BuildRequest(settings).Resource("/version/api");
var response = Version.Parse("1." + ProcessRequest(request, settings));
return response;
}
public string GetVersion(QBittorrentSettings settings)
{
// Version request does not require authentication.
var request = BuildRequest(settings).Resource("/version/qbittorrent");
var response = ProcessRequest(request, settings).TrimStart('v');
return response;
}
@@ -60,10 +87,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/query/torrents")
.AddQueryParam("label", settings.MovieCategory)
.AddQueryParam("category", settings.MovieCategory);
var request = BuildRequest(settings).Resource("/query/torrents");
if (settings.MovieCategory.IsNotNullOrWhiteSpace())
{
request.AddQueryParam("label", settings.MovieCategory);
request.AddQueryParam("category", settings.MovieCategory);
}
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
return response;
@@ -80,6 +109,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.MovieCategory);
}
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.
@@ -100,6 +134,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.MovieCategory);
}
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.
@@ -112,9 +151,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
.Post()
.AddFormParameter("hashes", hash);
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
@@ -128,9 +167,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
ProcessRequest(setCategoryRequest, settings);
}
catch(DownloadClientException ex)
catch (DownloadClientException ex)
{
// if setCategory fails due to method not being found, then try older setLabel command for qbittorent < 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)
{
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
@@ -141,14 +180,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ProcessRequest(setLabelRequest, settings);
}
}
}
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/topPrio")
.Post()
.AddFormParameter("hashes", hash);
.Post()
.AddFormParameter("hashes", hash);
try
{
ProcessRequest(request, settings);
@@ -156,7 +195,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden?
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
{
return;
@@ -170,9 +208,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/command/pause")
.Post()
.AddFormParameter("hash", hash);
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
@@ -181,7 +218,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = BuildRequest(settings).Resource("/command/resume")
.Post()
.AddFormParameter("hash", hash);
ProcessRequest(request, settings);
}
@@ -190,17 +226,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var request = BuildRequest(settings).Resource("/command/setForceStart")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("value", enabled ? "true": "false");
.AddFormParameter("value", enabled ? "true" : "false");
ProcessRequest(request, settings);
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port);
requestBuilder.LogResponseContent = true;
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}
@@ -265,10 +301,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_authCookieCache.Remove(authKey);
var authLoginRequest = BuildRequest(settings).Resource("/login")
.Post()
.AddFormParameter("username", settings.Username ?? string.Empty)
.AddFormParameter("password", settings.Password ?? string.Empty)
.Build();
.Post()
.AddFormParameter("username", settings.Username ?? string.Empty)
.AddFormParameter("password", settings.Password ?? string.Empty)
.Build();
HttpResponse response;
try
@@ -287,7 +323,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
}
if (response.Content != "Ok.") // returns "Fails." on bad login

View File

@@ -0,0 +1,335 @@
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.MovieCategory.IsNotNullOrWhiteSpace())
{
request.AddQueryParam("category", settings.MovieCategory);
}
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
return response;
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post()
.AddFormParameter("urls", torrentUrl);
if (settings.MovieCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.MovieCategory);
}
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.MovieCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.MovieCategory);
}
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 MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/topPrio")
.Post()
.AddFormParameter("hashes", hash);
try
{
ProcessRequest(request, settings);
}
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
{
return;
}
throw;
}
}
public void PauseTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void ResumeTorrent(string hash, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
.Post()
.AddFormParameter("hashes", hash);
ProcessRequest(request, settings);
}
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("value", enabled ? "true" : "false");
ProcessRequest(request, settings);
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
{
LogResponseContent = true,
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
};
return requestBuilder;
}
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
where TResult : new()
{
var responseContent = ProcessRequest(requestBuilder, settings);
return Json.Deserialize<TResult>(responseContent);
}
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
{
AuthenticateClient(requestBuilder, settings);
var request = requestBuilder.Build();
request.LogResponseContent = true;
HttpResponse response;
try
{
response = _httpClient.Execute(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Debug("Authentication required, logging in.");
AuthenticateClient(requestBuilder, settings, true);
request = requestBuilder.Build();
response = _httpClient.Execute(request);
}
else
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
}
catch (WebException ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
return response.Content;
}
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
{
if (settings.Username.IsNullOrWhiteSpace() || settings.Password.IsNullOrWhiteSpace())
{
if (reauthenticate)
{
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}
return;
}
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
var cookies = _authCookieCache.Find(authKey);
if (cookies == null || reauthenticate)
{
_authCookieCache.Remove(authKey);
var authLoginRequest = BuildRequest(settings).Resource("/api/v2/auth/login")
.Post()
.AddFormParameter("username", settings.Username ?? string.Empty)
.AddFormParameter("password", settings.Password ?? string.Empty)
.Build();
HttpResponse response;
try
{
response = _httpClient.Execute(authLoginRequest);
}
catch (HttpException ex)
{
_logger.Debug("qbitTorrent authentication failed.");
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
}
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
catch (WebException ex)
{
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
}
if (response.Content != "Ok.") // returns "Fails." on bad login
{
_logger.Debug("qbitTorrent authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
}
_logger.Debug("qBittorrent authentication succeeded.");
cookies = response.GetCookies();
_authCookieCache.Set(authKey, cookies);
}
requestBuilder.SetCookies(cookies);
}
}
}

View File

@@ -93,11 +93,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
foreach (RTorrentTorrent torrent in torrents)
{
// Don't concern ourselves with categories other than specified
if (torrent.Category != Settings.MovieCategory) continue;
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && torrent.Category != Settings.MovieCategory) continue;
if (torrent.Path.StartsWith("."))
{
throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent.");
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
}
var item = new DownloadClientItem();

View File

@@ -66,15 +66,13 @@ namespace NzbDrone.Core.Extras
var sourcePath = localMovie.Path;
var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.AllDirectories).OrderByDescending(d => d).ToArray();
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim(' ', '.'))
.ToList();
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase));
foreach (var matchingFilename in matchingFilenames)
foreach (var matchingFilename in files)
{
var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e));

View File

@@ -88,11 +88,22 @@ namespace NzbDrone.Core.Extras.Subtitles
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
{
var language = LanguageParser.ParseSubtitleLanguage(path);
var suffix = GetSuffix(language, 1, false);
var subtitleFile = ImportFile(movie, movieFile, path, readOnly, extension, suffix);
subtitleFile.Language = language;
var subtitleFiles = _subtitleFileService.GetFilesByMovie(movie.Id);
var existingSrtSubs = subtitleFiles.Where(m => m.MovieFileId == movieFile.Id)
.Where(m => m.Language == language)
.Where(m => m.Extension == extension);
_subtitleFileService.Upsert(subtitleFile);
var suffix = GetSuffix(language, existingSrtSubs.Count() + 1, extension.EqualsIgnoreCase(".srt"));
var subtitleFile = new SubtitleFile();
if ((extension.EqualsIgnoreCase(".srt") && language != Language.Unknown) ||
!extension.EqualsIgnoreCase(".srt"))
{
subtitleFile = ImportFile(movie, movieFile, path, readOnly, extension, suffix);
subtitleFile.Language = language;
_subtitleFileService.Upsert(subtitleFile);
}
return subtitleFile;
}

View File

@@ -1,7 +1,6 @@
using System;
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
@@ -9,50 +8,43 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
public class MonoVersionCheck : HealthCheckBase
{
private readonly IRuntimeInfo _runtimeInfo;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
private static readonly Regex VersionRegex = new Regex(@"(?<=\W|^)(?<version>\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public MonoVersionCheck(IRuntimeInfo runtimeInfo, Logger logger)
public MonoVersionCheck(IPlatformInfo platformInfo, Logger logger)
{
_runtimeInfo = runtimeInfo;
_platformInfo = platformInfo;
_logger = logger;
}
public override HealthCheck Check()
{
if (OsInfo.IsWindows)
if (!PlatformInfo.IsMono)
{
return new HealthCheck(GetType());
}
var versionString = _runtimeInfo.RuntimeVersion;
var versionMatch = VersionRegex.Match(versionString);
var monoVersion = _platformInfo.Version;
if (versionMatch.Success)
if (monoVersion == new Version("3.4.0") && HasMonoBug18599())
{
var version = new Version(versionMatch.Groups["version"].Value);
if (version == new Version(3, 4, 0) && HasMonoBug18599())
{
_logger.Debug("mono version 3.4.0, checking for mono bug #18599 returned positive.");
return new HealthCheck(GetType(), HealthCheckResult.Error, "your mono version 3.4.0 has a critical bug, you should upgrade to a higher version");
}
if (version == new Version(4, 4, 0) || version == new Version(4, 4, 1))
{
_logger.Debug("mono version {0}", version);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"your mono version {version} has a bug that causes issues connecting to indexers/download clients");
}
if (version >= new Version(3, 10))
{
_logger.Debug("mono version is 3.10 or better: {0}", version.ToString());
return new HealthCheck(GetType());
}
_logger.Debug("Mono version 3.4.0, checking for Mono bug #18599 returned positive.");
return new HealthCheck(GetType(), HealthCheckResult.Error, "You are running an old and unsupported version of Mono with a known bug. You should upgrade to a higher version");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, "mono version is less than 3.10, upgrade for improved stability");
if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1"))
{
_logger.Debug("Mono version {0}", monoVersion);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version");
}
if (monoVersion >= new Version("3.10"))
{
_logger.Debug("Mono version is 3.10 or better: {0}", monoVersion);
return new HealthCheck(GetType());
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability.");
}
public override bool CheckOnConfigChange => false;
@@ -70,7 +62,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
return false;
}
var fieldInfo = numberFormatterType.GetField("userFormatProvider", BindingFlags.Static | BindingFlags.NonPublic);
var fieldInfo = numberFormatterType.GetField("userFormatProvider",
BindingFlags.Static | BindingFlags.NonPublic);
if (fieldInfo == null)
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -60,7 +61,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
requestBuilder.AddQueryParam("ranked", "0");
}
requestBuilder.AddQueryParam("category", "movies");
requestBuilder.AddQueryParam("category", string.Join(";", Settings.Categories.Distinct()));
requestBuilder.AddQueryParam("limit", "100");
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings));
requestBuilder.AddQueryParam("format", "json_extended");
@@ -98,7 +99,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
requestBuilder.AddQueryParam("ranked", "0");
}
requestBuilder.AddQueryParam("category", "movies");
requestBuilder.AddQueryParam("category", string.Join(";", Settings.Categories.Distinct()));
requestBuilder.AddQueryParam("limit", "100");
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings));
requestBuilder.AddQueryParam("format", "json_extended");

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser;
@@ -12,6 +12,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
public RarbgSettingsValidator()
{
RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.Categories).NotEmpty();
}
}
@@ -24,6 +25,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
BaseUrl = "https://torrentapi.org";
RankedOnly = false;
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
Categories = new[] { 14, 48, 17, 44, 45, 47, 50, 51, 52, 42, 46 };
}
[FieldDefinition(0, Label = "API URL", HelpText = "URL to Rarbg api, not the website.")]
@@ -44,6 +46,9 @@ namespace NzbDrone.Core.Indexers.Rarbg
[FieldDefinition(5, Type = FieldType.Tag, SelectOptions = typeof(IndexerFlags), Label = "Required Flags", HelpText = "What indexer flags are required?", HelpLink = "https://github.com/Radarr/Radarr/wiki/Indexer-Flags#1-required-flags", Advanced = true)]
public IEnumerable<int> RequiredFlags { get; set; }
[FieldDefinition(6, Type = FieldType.Textbox, Label = "Categories", HelpText = "Comma Separated list, you can retrieve the ID by checking the URL behind the category on the website (i.e. Movie/x264/1080 = 44)", HelpLink = "https://rarbgmirror.org/torrents.php?category=movies", Advanced = true)]
public IEnumerable<int> Categories { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -1,18 +1,14 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Lifecycle
{
public class ApplicationShutdownRequested : IEvent
{
public bool Restarting { get; set; }
public bool Restarting { get; }
public ApplicationShutdownRequested()
{
}
public ApplicationShutdownRequested(bool restarting)
public ApplicationShutdownRequested(bool restarting = false)
{
Restarting = restarting;
}
}
}
}

View File

@@ -21,8 +21,8 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
var qualityComparer = new QualityModelComparer(localMovie.Movie.Profile);
if (localMovie.Movie.MovieFile != null && qualityComparer.Compare(localMovie.Movie.MovieFile.Quality, localMovie.Quality) > 0)
{
_logger.Debug("This file isn't an upgrade for all episodes. Skipping {0}", localMovie.Path);
return Decision.Reject("Not an upgrade for existing episode file(s)");
_logger.Debug("This file isn't an upgrade for movie. Skipping {0}", localMovie.Path);
return Decision.Reject("Not an upgrade for existing movie file");
}
return Decision.Accept();

View File

@@ -189,7 +189,6 @@ namespace NzbDrone.Core.MediaFiles
catch (Exception ex)
{
ex.ExceptronIgnoreOnMono();
_logger.Warn(ex, "Unable to set date of file [" + filePath + "]");
}
}

View File

@@ -86,11 +86,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
// The dude abides, so should us, Lets be nice to TMDb
// var allowed = int.Parse(response.Headers.GetValues("X-RateLimit-Limit").First()); // get allowed
// var reset = long.Parse(response.Headers.GetValues("X-RateLimit-Reset").First()); // get time when it resets
var remaining = int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First());
if (remaining <= 5)
if (response.Headers.ContainsKey("X-RateLimit-Remaining"))
{
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies");
Thread.Sleep(5000);
var remaining = int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First());
if (remaining <= 5)
{
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies");
Thread.Sleep(5000);
}
}
var resource = response.Resource;
@@ -333,11 +336,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
// The dude abides, so should us, Lets be nice to TMDb
// var allowed = int.Parse(response.Headers.GetValues("X-RateLimit-Limit").First()); // get allowed
// var reset = long.Parse(response.Headers.GetValues("X-RateLimit-Reset").First()); // get time when it resets
var remaining = int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First());
if (remaining <= 5)
if (response.Headers.ContainsKey("X-RateLimit-Remaining"))
{
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies");
Thread.Sleep(5000);
var remaining = int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First());
if (remaining <= 5)
{
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies");
Thread.Sleep(5000);
}
}
var resources = response.Resource;

View File

@@ -17,30 +17,30 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
public int VoteCount { get; set; }
public Language Language { get; set; }
public LazyLoaded<Movie> Movie { get; set; }
public AlternativeTitle()
{
}
public AlternativeTitle(string title, SourceType sourceType = SourceType.TMDB, int sourceId = 0, Language language = Language.English)
{
Title = title;
CleanTitle = title.CleanSeriesTitle();
SourceType = sourceType;
SourceId = sourceId;
Language = language;
public AlternativeTitle()
{
}
public bool IsTrusted(int minVotes = 3)
public AlternativeTitle(string title, SourceType sourceType = SourceType.TMDB, int sourceId = 0, Language language = Language.English)
{
Title = title;
CleanTitle = title.CleanSeriesTitle();
SourceType = sourceType;
SourceId = sourceId;
Language = language;
}
public bool IsTrusted(int minVotes = 4)
{
switch (SourceType)
{
case SourceType.TMDB:
case SourceType.Mappings:
return Votes >= minVotes;
default:
return true;
}
}
}
public override bool Equals(object obj)

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Marr.Data;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;

View File

@@ -3,6 +3,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Messaging.Events;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies.Events;
namespace NzbDrone.Core.Movies.AlternativeTitles
@@ -13,7 +14,7 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie);
List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie);
AlternativeTitle GetById(int id);
void DeleteNotEnoughVotes(List<AlternativeTitle> mappingsTitles);
List<AlternativeTitle> UpdateTitles(List<AlternativeTitle> titles, Movie movie);
}
public class AlternativeTitleService : IAlternativeTitleService, IHandleAsync<MovieDeletedEvent>
@@ -63,11 +64,28 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
_titleRepo.Delete(title);
}
public void DeleteNotEnoughVotes(List<AlternativeTitle> mappingsTitles)
public List<AlternativeTitle> UpdateTitles(List<AlternativeTitle> titles, Movie movie)
{
var toRemove = mappingsTitles.Where(t => t.SourceType == SourceType.Mappings && t.Votes < 4);
var realT = _titleRepo.FindBySourceIds(toRemove.Select(t => t.SourceId).ToList());
_titleRepo.DeleteMany(realT);
int movieId = movie.Id;
// First update the movie ids so we can correlate them later.
titles.ForEach(t => t.MovieId = movieId);
// Then make sure none of them are the same as the main title.
titles = titles.Where(t => t.CleanTitle != movie.CleanTitle).ToList();
// Then make sure they are all distinct titles
titles = titles.DistinctBy(t => t.CleanTitle).ToList();
// Now find titles to delete, update and insert.
var existingTitles = _titleRepo.FindByMovieId(movieId);
var insert = titles.Where(t => !existingTitles.Contains(t));
var update = existingTitles.Where(t => titles.Contains(t));
var delete = existingTitles.Where(t => !titles.Contains(t));
_titleRepo.DeleteMany(delete.ToList());
_titleRepo.UpdateMany(update.ToList());
_titleRepo.InsertMany(insert.ToList());
return titles;
}
public void HandleAsync(MovieDeletedEvent message)

View File

@@ -108,26 +108,16 @@ namespace NzbDrone.Core.Movies
_logger.Warn(e, "Couldn't update movie path for " + movie.Path);
}
movieInfo.AlternativeTitles = movieInfo.AlternativeTitles.Where(t => t.CleanTitle != movie.CleanTitle)
.DistinctBy(t => t.CleanTitle)
.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles, t => t.CleanTitle, EqualityComparer<string>.Default).ToList();
try
{
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(movieInfo.AlternativeTitles, movie));
var mappings = _apiClient.AlternativeTitlesAndYearForMovie(movieInfo.TmdbId);
var mappingsTitles = mappings.Item1;
_titleService.DeleteNotEnoughVotes(mappingsTitles);
mappingsTitles = mappingsTitles.Where(t => t.IsTrusted()).ToList();
mappingsTitles = mappingsTitles.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles,
t => t.CleanTitle, EqualityComparer<string>.Default).ToList();
mappingsTitles = mappingsTitles.Where(t => t.Votes > 3).ToList();
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(mappingsTitles, movie));
movieInfo.AlternativeTitles.AddRange(mappingsTitles);
movie.AlternativeTitles = _titleService.UpdateTitles(movieInfo.AlternativeTitles, movie);
if (mappings.Item2 != null)
{

View File

@@ -150,6 +150,9 @@
<Compile Include="DecisionEngine\Specifications\CustomFormatAllowedByProfileSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\MaximumSizeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" />
<Compile Include="Download\Clients\DownloadClientUnavailableException.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxySelector.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV2.cs" />
<Compile Include="Download\InvalidNzbException.cs" />
<Compile Include="Download\NzbValidationService.cs" />
<Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcNfoDetector.cs" />
@@ -525,7 +528,7 @@
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentPriority.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxy.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV1.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentSettings.cs" />
<Compile Include="Download\Clients\QBittorrent\QBittorrentTorrent.cs" />
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
@@ -1336,4 +1339,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View File

@@ -31,6 +31,7 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("hu", "hun", Language.Hungarian),
new IsoLanguage("he", "heb", Language.Hebrew),
new IsoLanguage("cs", "ces", Language.Czech),
new IsoLanguage("ua", "ukr", Language.Ukrainian),
new IsoLanguage("an", "any", Language.Any)
};

View File

@@ -30,6 +30,7 @@ namespace NzbDrone.Core.Parser
Hungarian = 22,
Hebrew = 23,
Czech = 24,
Ukrainian = 25,
Any = -1,
}

View File

@@ -13,11 +13,10 @@ namespace NzbDrone.Core.Parser
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser));
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_|^)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR|VO|VFF|VFQ|VF2|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<czech>\b(?:CZ|SK)\b)",
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_|^)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR|VO|VFF|VFQ|VF2|TRUEFRENCH)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<czech>\b(?:CZ|SK)\b)|(?<ukrainian>\bukr\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})(?:[-_. ]forced)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex RarbgSubtitleLanguageRegex = new Regex("^[0-9]{1,2}_(?<iso_code>[A-Za-z]{2,3}).*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static List<Language> ParseLanguages(string title)
{
var lowerTitle = title.ToLower();
@@ -86,6 +85,9 @@ namespace NzbDrone.Core.Parser
if (lowerTitle.Contains("czech"))
languages.Add( Language.Czech);
if (lowerTitle.Contains("ukrainian"))
languages.Add(Language.Ukrainian);
var match = LanguageRegex.Match(title);
if (match.Groups["italian"].Captures.Cast<Capture>().Any())
@@ -118,6 +120,8 @@ namespace NzbDrone.Core.Parser
if (match.Groups["czech"].Success)
languages.Add( Language.Czech);
if (match.Groups["ukrainian"].Success)
languages.Add( Language.Ukrainian);
return languages.DistinctBy(l => (int)l).ToList();
}
@@ -148,13 +152,19 @@ namespace NzbDrone.Core.Parser
var simpleFilename = Path.GetFileNameWithoutExtension(fileName);
var languageMatch = SubtitleLanguageRegex.Match(simpleFilename);
if (!languageMatch.Success)
{
languageMatch = RarbgSubtitleLanguageRegex.Match(simpleFilename);
}
if (languageMatch.Success)
{
var isoCode = languageMatch.Groups["iso_code"].Value;
var isoLanguage = IsoLanguages.Find(isoCode);
var isoLanguage = IsoLanguages.Find(isoCode.ToLower());
return isoLanguage?.Language ?? Language.Unknown;
Logger.Debug("Parsed language: {0}", isoLanguage?.Language ?? Language.Unknown);
return isoLanguage?.Language ?? Language.Unknown;
}
#if !LIBRARY
Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName);

View File

@@ -112,7 +112,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample|Pre|postbot|xpost))+$",
private static readonly Regex CleanReleaseGroupRegex = new Regex(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet))+$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanTorrentSuffixRegex = new Regex(@"\[(?:ettv|rartv|rarbg|cttv)\]$",

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex HardcodedSubsRegex = new Regex(@"\b(?<hcsub>(\w+SUBS?)\b)|(?<hc>(HC|SUBBED))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private static readonly Regex RemuxRegex = new Regex(@"\b(?<remux>(BD)?Remux)\b",
private static readonly Regex RemuxRegex = new Regex(@"\b(?<remux>(BD|UHD)?Remux)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BRDISKRegex = new Regex(@"\b(COMPLETE|ISO|BDISO|BD25|BD50|BR.?DISK)\b",

View File

@@ -7,12 +7,10 @@ namespace NzbDrone.Core.Rest
{
public static RestClient BuildClient(string baseUrl)
{
var restClient = new RestClient(baseUrl);
restClient.UserAgent = string.Format("Radarr/{0} (RestSharp/{1}; {2}/{3})",
BuildInfo.Version,
restClient.GetType().Assembly.GetName().Version,
OsInfo.Os, OsInfo.Version.ToString(2));
var restClient = new RestClient(baseUrl)
{
UserAgent = $"Radarr/{BuildInfo.Version} ({OsInfo.Os})"
};
return restClient;
}

View File

@@ -1,4 +1,3 @@
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
@@ -14,16 +13,11 @@ namespace NzbDrone.Core.Update
private readonly IUpdatePackageProvider _updatePackageProvider;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger;
public CheckUpdateService(IUpdatePackageProvider updatePackageProvider,
IConfigFileProvider configFileProvider,
Logger logger)
IConfigFileProvider configFileProvider)
{
_updatePackageProvider = updatePackageProvider;
_configFileProvider = configFileProvider;
_logger = logger;
}
public UpdatePackage AvailableUpdate()
@@ -31,4 +25,4 @@ namespace NzbDrone.Core.Update
return _updatePackageProvider.GetLatestUpdate(_configFileProvider.Branch, BuildInfo.Version);
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo;
@@ -15,11 +15,13 @@ namespace NzbDrone.Core.Update
public class UpdatePackageProvider : IUpdatePackageProvider
{
private readonly IHttpClient _httpClient;
private readonly IPlatformInfo _platformInfo;
private readonly IHttpRequestBuilderFactory _requestBuilder;
public UpdatePackageProvider(IHttpClient httpClient, IRadarrCloudRequestBuilder requestBuilder)
public UpdatePackageProvider(IHttpClient httpClient, IRadarrCloudRequestBuilder requestBuilder, IPlatformInfo platformInfo)
{
_httpClient = httpClient;
_platformInfo = platformInfo;
_requestBuilder = requestBuilder.Services;
}
@@ -29,6 +31,7 @@ namespace NzbDrone.Core.Update
.Resource("/update/{branch}")
.AddQueryParam("version", currentVersion)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("runtimeVer", _platformInfo.Version)
.SetSegment("branch", branch)
.Build();
@@ -45,6 +48,7 @@ namespace NzbDrone.Core.Update
.Resource("/update/{branch}/changes")
.AddQueryParam("version", currentVersion)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("runtimeVer", _platformInfo.Version)
.SetSegment("branch", branch)
.Build();
@@ -53,4 +57,4 @@ namespace NzbDrone.Core.Update
return updates.Resource;
}
}
}
}

View File

@@ -20,6 +20,7 @@ namespace Radarr.Host.AccessControl
private readonly INetshProvider _netshProvider;
private readonly IConfigFileProvider _configFileProvider;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IOsInfo _osInfo;
private readonly Logger _logger;
public List<string> Urls
@@ -30,7 +31,7 @@ namespace Radarr.Host.AccessControl
}
}
private List<UrlAcl> InternalUrls { get; set; }
private List<UrlAcl> InternalUrls { get; }
private List<UrlAcl> RegisteredUrls { get; set; }
private static readonly Regex UrlAclRegex = new Regex(@"(?<scheme>https?)\:\/\/(?<address>.+?)\:(?<port>\d+)/(?<urlbase>.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -38,19 +39,26 @@ namespace Radarr.Host.AccessControl
public UrlAclAdapter(INetshProvider netshProvider,
IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo,
IOsInfo osInfo,
Logger logger)
{
_netshProvider = netshProvider;
_configFileProvider = configFileProvider;
_runtimeInfo = runtimeInfo;
_osInfo = osInfo;
_logger = logger;
InternalUrls = new List<UrlAcl>();
RegisteredUrls = GetRegisteredUrls();
RegisteredUrls = new List<UrlAcl>();
}
public void ConfigureUrls()
{
if (RegisteredUrls.Empty())
{
GetRegisteredUrls();
}
var localHostHttpUrls = BuildUrlAcls("http", "localhost", _configFileProvider.Port);
var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, _configFileProvider.Port);
@@ -105,7 +113,8 @@ namespace Radarr.Host.AccessControl
private void RefreshRegistration()
{
if (OsInfo.Version.Major < 6) return;
var osVersion = new Version(_osInfo.Version);
if (osVersion.Major < 6) return;
foreach (var urlAcl in InternalUrls)
{
@@ -124,19 +133,24 @@ namespace Radarr.Host.AccessControl
c.UrlBase == urlAcl.UrlBase);
}
private List<UrlAcl> GetRegisteredUrls()
private void GetRegisteredUrls()
{
if (OsInfo.IsNotWindows)
{
return new List<UrlAcl>();
return;
}
if (RegisteredUrls.Any())
{
return;
}
var arguments = string.Format("http show urlacl");
var output = _netshProvider.Run(arguments);
if (output == null || !output.Standard.Any()) return new List<UrlAcl>();
if (output == null || !output.Standard.Any()) return;
return output.Standard.Select(line =>
RegisteredUrls = output.Standard.Select(line =>
{
var match = UrlAclRegex.Match(line.Content);

View File

@@ -58,7 +58,7 @@ namespace Radarr.Host
//_cancelHandler = new CancelHandler();
}
_runtimeInfo.IsRunning = true;
_runtimeInfo.IsExiting = false;
DbFactory.RegisterDatabase(_container);
_hostController.StartServer();
@@ -87,7 +87,7 @@ namespace Radarr.Host
_logger.Info("Attempting to stop application.");
_hostController.StopServer();
_logger.Info("Application has finished stop routine.");
_runtimeInfo.IsRunning = false;
_runtimeInfo.IsExiting = true;
}
public void Handle(ApplicationShutdownRequested message)

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Nancy.Bootstrapper;
using NzbDrone.Api;
using NzbDrone.Common.Composition;
@@ -15,26 +15,15 @@ namespace Radarr.Host
var assemblies = new List<string>
{
"Radarr.Host",
"NzbDrone.Common",
"NzbDrone.Core",
"NzbDrone.Api",
"NzbDrone.SignalR"
};
if (OsInfo.IsWindows)
{
assemblies.Add("NzbDrone.Windows");
}
else
{
assemblies.Add("NzbDrone.Mono");
}
return new MainAppContainerBuilder(args, assemblies.ToArray()).Container;
return new MainAppContainerBuilder(args, assemblies).Container;
}
private MainAppContainerBuilder(StartupContext args, string[] assemblies)
private MainAppContainerBuilder(StartupContext args, List<string> assemblies)
: base(args, assemblies)
{
AutoRegisterImplementations<NzbDronePersistentConnection>();
@@ -43,4 +32,4 @@ namespace Radarr.Host
Container.Register<IHttpDispatcher, FallbackHttpDispatcher>();
}
}
}
}

View File

@@ -1,5 +1,6 @@
using NLog;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo;
namespace Radarr.Host
{
@@ -8,14 +9,16 @@ namespace Radarr.Host
private readonly INzbDroneServiceFactory _nzbDroneServiceFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IConsoleService _consoleService;
private readonly IRuntimeInfo _runtimeInfo;
private readonly Logger _logger;
public Router(INzbDroneServiceFactory nzbDroneServiceFactory, IServiceProvider serviceProvider,
IConsoleService consoleService, Logger logger)
IConsoleService consoleService, IRuntimeInfo runtimeInfo, Logger logger)
{
_nzbDroneServiceFactory = nzbDroneServiceFactory;
_serviceProvider = serviceProvider;
_consoleService = consoleService;
_runtimeInfo = runtimeInfo;
_logger = logger;
}
@@ -34,7 +37,7 @@ namespace Radarr.Host
case ApplicationModes.Interactive:
{
_logger.Debug("Console selected");
_logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected");
_nzbDroneServiceFactory.Start();
break;
}

View File

@@ -1,4 +1,4 @@
using System.Threading;
using System.Threading;
using NLog;
using NLog.Common;
using NzbDrone.Common.EnvironmentInfo;
@@ -28,7 +28,7 @@ namespace Radarr.Host
public void Spin()
{
while (_runtimeInfo.IsRunning)
while (!_runtimeInfo.IsExiting)
{
Thread.Sleep(1000);
}

View File

@@ -5,7 +5,6 @@ using NzbDrone.Api.Commands;
namespace NzbDrone.Integration.Test.ApiTests
{
[TestFixture]
[Ignore("Not ready to be used on this branch")]
public class CommandFixture : IntegrationTest
{
[Test]
@@ -16,4 +15,4 @@ namespace NzbDrone.Integration.Test.ApiTests
response.Id.Should().NotBe(0);
}
}
}
}

View File

@@ -3,41 +3,11 @@ using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Integration.Test.ApiTests
namespace NzbDrone.Integration.Test.ApiTests.WantedTests
{
[TestFixture]
public class WantedFixture : IntegrationTest
public class CutoffUnmetFixture : IntegrationTest
{
[Test, Order(0)]
public void missing_should_be_empty()
{
EnsureNoMovie(680, "Pulp Fiction");
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void missing_should_have_monitored_items()
{
EnsureMovie(680, "Pulp Fiction", true);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.Should().NotBeEmpty();
}
[Test, Order(1)]
public void missing_should_have_movie()
{
EnsureMovie(680, "Pulp Fiction", true);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.First().Title.Should().Be("Pulp Fiction");
}
[Test, Order(1)]
public void cutoff_should_have_monitored_items()
{
@@ -45,21 +15,11 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = EnsureMovie(680, "Pulp Fiction", true);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc");
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().NotBeEmpty();
}
[Test, Order(1)]
public void missing_should_not_have_unmonitored_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void cutoff_should_not_have_unmonitored_items()
{
@@ -67,7 +27,19 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = EnsureMovie(680, "Pulp Fiction", false);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc");
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void cutoff_should_not_have_released_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", true);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "status", "inCinemas");
result.Records.Should().BeEmpty();
}
@@ -84,16 +56,6 @@ namespace NzbDrone.Integration.Test.ApiTests
result.Records.First().Title.Should().Be("Pulp Fiction");
}
[Test, Order(2)]
public void missing_should_have_unmonitored_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "false");
result.Records.Should().NotBeEmpty();
}
[Test, Order(2)]
public void cutoff_should_have_unmonitored_items()
{
@@ -105,5 +67,17 @@ namespace NzbDrone.Integration.Test.ApiTests
result.Records.Should().NotBeEmpty();
}
[Test, Order(2)]
public void cutoff_should_have_released_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", false);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "status", "released");
result.Records.Should().NotBeEmpty();
}
}
}

View File

@@ -0,0 +1,81 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Integration.Test.ApiTests.WantedTests
{
[TestFixture]
public class MissingFixture : IntegrationTest
{
[Test, Order(0)]
public void missing_should_be_empty()
{
EnsureNoMovie(680, "Pulp Fiction");
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void missing_should_have_monitored_items()
{
EnsureMovie(680, "Pulp Fiction", true);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().NotBeEmpty();
}
[Test, Order(1)]
public void missing_should_have_movie()
{
EnsureMovie(680, "Pulp Fiction", true);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.First().Title.Should().Be("Pulp Fiction");
}
[Test, Order(1)]
public void missing_should_not_have_unmonitored_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void missing_should_not_have_released_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "status", "inCinemas");
result.Records.Should().BeEmpty();
}
[Test, Order(2)]
public void missing_should_have_unmonitored_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "false");
result.Records.Should().NotBeEmpty();
}
[Test, Order(2)]
public void missing_should_have_released_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "status", "released");
result.Records.Should().NotBeEmpty();
}
}
}

View File

@@ -235,6 +235,8 @@ namespace NzbDrone.Integration.Test
}
}
Commands.WaitAll();
return result;
}
@@ -254,9 +256,13 @@ namespace NzbDrone.Integration.Test
if (result.MovieFile == null)
{
var path = Path.Combine(MovieRootFolder, movie.Title, string.Format("{0} - {1}.mkv", movie.Title, quality.Name));
var path = Path.Combine(MovieRootFolder, movie.Title, string.Format("{0} ({1}) - {2}.strm", movie.Title, movie.Year, quality.Name));
Directory.CreateDirectory(Path.GetDirectoryName(path));
var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "ApiTests", "Files", "H264_sample.mp4");
//File.Copy(sourcePath, path);
File.WriteAllText(path, "Fake Movie");
Commands.PostAndWait(new CommandResource { Name = "refreshmovie", Body = new RefreshMovieCommand(movie.Id) });

View File

@@ -113,7 +113,8 @@
<Compile Include="ApiTests\MovieFileFixture.cs" />
<Compile Include="ApiTests\FileSystemFixture.cs" />
<Compile Include="ApiTests\MovieLookupFixture.cs" />
<Compile Include="ApiTests\WantedFixture.cs" />
<Compile Include="ApiTests\WantedTests\CutoffUnmetFixture.cs" />
<Compile Include="ApiTests\WantedTests\MissingFixture.cs" />
<Compile Include="Client\ClientBase.cs" />
<Compile Include="Client\IndexerClient.cs" />
<Compile Include="Client\DownloadClientClient.cs" />
@@ -180,20 +181,20 @@
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent Condition="('$(OS)' == 'Windows_NT')">
xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Mono.*" "$(TargetDir)"
xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Windows.*" "$(TargetDir)"
<PostBuildEvent Condition="('$(OS)' == 'Windows_NT')">
xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Mono.*" "$(TargetDir)"
xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Windows.*" "$(TargetDir)"
</PostBuildEvent>
<PostBuildEvent Condition="('$(OS)' != 'Windows_NT')">
cp -rv $(SolutionDir)\..\_output\NzbDrone.Mono.* $(TargetDir)
cp -rv $(SolutionDir)\..\_output\NzbDrone.Windows.* $(TargetDir)
<PostBuildEvent Condition="('$(OS)' != 'Windows_NT')">
cp -rv $(SolutionDir)\..\_output\NzbDrone.Mono.* $(TargetDir)
cp -rv $(SolutionDir)\..\_output\NzbDrone.Windows.* $(TargetDir)
</PostBuildEvent>
</PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,23 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Mono.EnvironmentInfo;
using NzbDrone.Test.Common;
namespace NzbDrone.Mono.Test.EnvironmentInfo
{
[TestFixture]
[Platform("Mono")]
public class MonoPlatformInfoFixture : TestBase<MonoPlatformInfo>
{
[Test]
public void should_get_framework_version()
{
Subject.Version.Major.Should().BeOneOf(4, 5);
if (Subject.Version.Major == 4)
{
Subject.Version.Minor.Should().BeOneOf(0, 5, 6);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Mono.Disk;
using NzbDrone.Mono.EnvironmentInfo.VersionAdapters;
using NzbDrone.Test.Common;
namespace NzbDrone.Mono.Test.EnvironmentInfo
{
[TestFixture]
[Platform("Mono")]
public class ReleaseFileVersionAdapterFixture : TestBase<ReleaseFileVersionAdapter>
{
[SetUp]
public void Setup()
{
Mocker.SetConstant<IDiskProvider>(Mocker.Resolve<DiskProvider>());
}
[Test]
public void should_get_version_info()
{
var info = Subject.Read();
info.FullName.Should().NotBeNullOrWhiteSpace();
info.Name.Should().NotBeNullOrWhiteSpace();
info.Version.Should().NotBeNullOrWhiteSpace();
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Mono.EnvironmentInfo.VersionAdapters;
using NzbDrone.Test.Common;
namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters
{
[TestFixture]
public class MacOsVersionAdapterFixture : TestBase<MacOsVersionAdapter>
{
[TestCase("10.8.0")]
[TestCase("10.8")]
[TestCase("10.8.1")]
[TestCase("10.11.20")]
public void should_get_version_info(string versionString)
{
var fileContent = File.ReadAllText(GetTestPath("Files/macOS/SystemVersion.plist")).Replace("10.0.0", versionString);
const string plistPath = "/System/Library/CoreServices/SystemVersion.plist";
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderExists("/System/Library/CoreServices/")).Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFiles("/System/Library/CoreServices/", SearchOption.TopDirectoryOnly))
.Returns(new[] { plistPath });
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.ReadAllText(plistPath))
.Returns(fileContent);
var versionName = Subject.Read();
versionName.Version.Should().Be(versionString);
versionName.Name.Should().Be("macOS");
versionName.FullName.Should().Be("macOS " + versionString);
}
[TestCase]
public void should_detect_server()
{
var fileContent = File.ReadAllText(GetTestPath("Files/macOS/SystemVersion.plist"));
const string plistPath = "/System/Library/CoreServices/ServerVersion.plist";
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderExists("/System/Library/CoreServices/")).Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFiles("/System/Library/CoreServices/", SearchOption.TopDirectoryOnly))
.Returns(new[] { plistPath });
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.ReadAllText(plistPath))
.Returns(fileContent);
var versionName = Subject.Read();
versionName.Name.Should().Be("macOS Server");
}
[TestCase]
public void should_return_null_if_folder_doesnt_exist()
{
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderExists("/System/Library/CoreServices/")).Returns(false);
Subject.Read().Should().BeNull();
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.GetFiles(It.IsAny<string>(), SearchOption.TopDirectoryOnly), Times.Never());
}
}
}

View File

@@ -0,0 +1,82 @@
using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Mono.Disk;
using NzbDrone.Mono.EnvironmentInfo.VersionAdapters;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories;
namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters
{
[TestFixture]
public class ReleaseFileVersionAdapterFixture : TestBase<ReleaseFileVersionAdapter>
{
[Test]
[IntegrationTest]
[Platform("Mono")]
public void should_get_version_info_from_actual_linux()
{
Mocker.SetConstant<IDiskProvider>(Mocker.Resolve<DiskProvider>());
var info = Subject.Read();
info.FullName.Should().NotBeNullOrWhiteSpace();
info.Name.Should().NotBeNullOrWhiteSpace();
info.Version.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void should_return_null_if_etc_doestn_exist()
{
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists("/etc/")).Returns(false);
Subject.Read().Should().BeNull();
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.GetFiles(It.IsAny<string>(), SearchOption.TopDirectoryOnly), Times.Never());
Subject.Read().Should().BeNull();
}
[Test]
public void should_return_null_if_release_file_doestn_exist()
{
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists("/etc/")).Returns(true);
Subject.Read().Should().BeNull();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFiles(It.IsAny<string>(), SearchOption.TopDirectoryOnly)).Returns(new string[0]);
Subject.Read().Should().BeNull();
}
[Test]
public void should_detect_version()
{
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists("/etc/")).Returns(true);
Subject.Read().Should().BeNull();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFiles(It.IsAny<string>(), SearchOption.TopDirectoryOnly)).Returns(new[]
{
"/etc/lsb-release",
"/etc/os-release"
});
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.ReadAllText("/etc/lsb-release"))
.Returns(File.ReadAllText(GetTestPath("Files/linux/lsb-release")));
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.ReadAllText("/etc/os-release"))
.Returns(File.ReadAllText(GetTestPath("Files/linux/os-release")));
var version = Subject.Read();
version.Should().NotBeNull();
version.Name.Should().Be("ubuntu");
version.Version.Should().Be("14.04");
version.FullName.Should().Be("Ubuntu 14.04.5 LTS");
}
}
}

View File

@@ -0,0 +1,4 @@
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.5 LTS"

View File

@@ -0,0 +1,9 @@
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

Some files were not shown because too many files have changed in this diff Show More