1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-30 18:25:57 -04: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: - unit_tests:
requires: requires:
- build - build
#- integration_tests: - integration_tests:
# requires: requires:
# - build - build
- publish_artifacts: - publish_artifacts:
requires: requires:
- build - build

9900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
@@ -23,7 +23,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
_logger = logger; _logger = logger;
// On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case. // 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) public void Register(IPipelines pipelines)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using Nancy.Bootstrapper; using Nancy.Bootstrapper;
using Nancy.Diagnostics; using Nancy.Diagnostics;
using NLog; using NLog;
@@ -24,9 +24,9 @@ namespace NzbDrone.Api
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) 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); DiagnosticsHook.Disable(pipelines);
} }

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ namespace NzbDrone.Common.Test
[Test] [Test]
public void IsProduction_should_return_false_when_run_within_nunit() 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] [Test]

View File

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

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\HttpRequestBuilderFixture.cs" />
<Compile Include="Http\HttpRequestFixture.cs" /> <Compile Include="Http\HttpRequestFixture.cs" />
<Compile Include="Http\HttpUriFixture.cs" /> <Compile Include="Http\HttpUriFixture.cs" />
<Compile Include="Http\UserAgentBuilderFixture.cs" />
<Compile Include="InstrumentationTests\CleanseLogMessageFixture.cs" /> <Compile Include="InstrumentationTests\CleanseLogMessageFixture.cs" />
<Compile Include="LevenshteinDistanceFixture.cs" /> <Compile Include="LevenshteinDistanceFixture.cs" />
<Compile Include="OsPathFixture.cs" /> <Compile Include="OsPathFixture.cs" />

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -13,12 +13,15 @@ namespace NzbDrone.Common.Composition
{ {
private readonly List<Type> _loadedTypes; 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>(); _loadedTypes = new List<Type>();
assemblies.Add(OsInfo.IsWindows ? "NzbDrone.Windows" : "NzbDrone.Mono");
assemblies.Add("NzbDrone.Common");
foreach (var assembly in assemblies) foreach (var assembly in assemblies)
{ {
_loadedTypes.AddRange(Assembly.Load(assembly).GetTypes()); _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)); 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 long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename); public abstract void InheritFolderPermissions(string filename);
public abstract void SetPermissions(string path, string mask, string user, string group); 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) public bool FileExists(string path)
{ {
Ensure.That(path, () => path).IsValidPath(); Ensure.That(path, () => path).IsValidPath();
return FileExists(path, OsInfo.PathStringComparison); return FileExists(path, PathStringComparison);
} }
public bool FileExists(string path, StringComparison stringComparison) public bool FileExists(string path, StringComparison stringComparison)

View File

@@ -101,12 +101,12 @@ namespace NzbDrone.Common.EnsureThat
if (param.Value.IsPathValid()) return param; 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 public interface IRuntimeInfo
{ {
bool IsUserInteractive { get; } bool IsUserInteractive { get; }
bool IsAdmin { get; } bool IsAdmin { get; }
bool IsWindowsService { get; } bool IsWindowsService { get; }
bool IsConsole { get; } bool IsWindowsTray { get; }
bool IsRunning { get; set; } bool IsExiting { get; set; }
bool RestartPending { get; set; } bool RestartPending { get; set; }
string ExecutingApplication { get; } string ExecutingApplication { get; }
string RuntimeVersion { get; }
} }
} }

View File

@@ -1,87 +1,95 @@
using System; using System;
using System.Diagnostics; using System.Collections.Generic;
using System.Globalization; using System.IO;
using System.Runtime.InteropServices; using System.Linq;
using NLog;
namespace NzbDrone.Common.EnvironmentInfo 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() static OsInfo()
{ {
var platform = (int)Environment.OSVersion.Platform; var platform = Environment.OSVersion.Platform;
Version = Environment.OSVersion.Version; switch (platform)
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)
{ {
Os = Os.Windows; case PlatformID.Win32NT:
PathStringComparison = StringComparison.OrdinalIgnoreCase; {
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 else
{ {
Os = IsOsx ? Os.Osx : Os.Linux; Name = Os.ToString();
FullName = Name;
PathStringComparison = StringComparison.Ordinal;
} }
} }
}
public static Version Version { get; private set; } public interface IOsInfo
public static bool IsMonoRuntime { get; private set; } {
public static bool IsNotWindows { get; private set; } string Version { get; }
public static bool IsLinux { get; private set; } string Name { get; }
public static bool IsOsx { get; private set; } string FullName { get; }
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 enum Os public enum Os
@@ -90,4 +98,4 @@ namespace NzbDrone.Common.EnvironmentInfo
Linux, Linux,
Osx 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.Diagnostics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@@ -9,11 +9,11 @@ using NzbDrone.Common.Processes;
namespace NzbDrone.Common.EnvironmentInfo namespace NzbDrone.Common.EnvironmentInfo
{ {
public abstract class RuntimeInfoBase : IRuntimeInfo public class RuntimeInfo : IRuntimeInfo
{ {
private readonly Logger _logger; private readonly Logger _logger;
public RuntimeInfoBase(IServiceProvider serviceProvider, Logger logger) public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
{ {
_logger = logger; _logger = logger;
@@ -28,10 +28,11 @@ namespace NzbDrone.Common.EnvironmentInfo
if (entry != null) if (entry != null)
{ {
ExecutingApplication = entry.Location; ExecutingApplication = entry.Location;
IsWindowsTray = entry.ManifestModule.Name == $"{ProcessProvider.NZB_DRONE_PROCESS_NAME}.exe";
} }
} }
static RuntimeInfoBase() static RuntimeInfo()
{ {
IsProduction = InternalIsProduction(); IsProduction = InternalIsProduction();
} }
@@ -59,31 +60,18 @@ namespace NzbDrone.Common.EnvironmentInfo
public bool IsWindowsService { get; private set; } public bool IsWindowsService { get; private set; }
public bool IsConsole public bool IsExiting { get; set; }
{
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 RestartPending { 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; }
public static bool IsProduction { get; private set; }
private static bool InternalIsProduction() private static bool InternalIsProduction()
{ {
if (BuildInfo.IsDebug || Debugger.IsAttached) return false; 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 try
{ {
@@ -99,21 +87,23 @@ namespace NzbDrone.Common.EnvironmentInfo
} }
try try
{ {
var currentAssmeblyLocation = typeof(RuntimeInfoBase).Assembly.Location; var currentAssmeblyLocation = typeof(RuntimeInfo).Assembly.Location;
if(currentAssmeblyLocation.ToLower().Contains("_output"))return false; if (currentAssmeblyLocation.ToLower().Contains("_output")) return false;
} }
catch catch
{ {
} }
string lowerCurrentDir = Directory.GetCurrentDirectory().ToLower(); var lowerCurrentDir = Directory.GetCurrentDirectory().ToLower();
if (lowerCurrentDir.Contains("teamcity")) return false; if (lowerCurrentDir.Contains("teamcity")) return false;
if (lowerCurrentDir.Contains("_output")) return false; if (lowerCurrentDir.Contains("_output")) return false;
return true; 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.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@@ -47,7 +48,7 @@ namespace NzbDrone.Common.Extensions
{ {
if (!comparison.HasValue) if (!comparison.HasValue)
{ {
comparison = OsInfo.PathStringComparison; comparison = DiskProviderBase.PathStringComparison;
} }
if (firstPath.Equals(secondPath, comparison.Value)) return true; if (firstPath.Equals(secondPath, comparison.Value)) return true;
@@ -93,7 +94,7 @@ namespace NzbDrone.Common.Extensions
while (child.Parent != null) while (child.Parent != null)
{ {
if (child.Parent.FullName.Equals(parent.FullName, OsInfo.PathStringComparison)) if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison))
{ {
return true; return true;
} }
@@ -275,4 +276,4 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE); 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 static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly Logger _logger; private readonly Logger _logger;
private const string _caBundleFileName = "curl-ca-bundle.crt"; private const string _caBundleFileName = "curl-ca-bundle.crt";
@@ -37,10 +38,11 @@ namespace NzbDrone.Common.Http.Dispatchers
_caBundleFilePath = _caBundleFileName; _caBundleFilePath = _caBundleFileName;
} }
} }
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, Logger logger) public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger)
{ {
_proxySettingsProvider = proxySettingsProvider; _proxySettingsProvider = proxySettingsProvider;
_userAgentBuilder = userAgentBuilder;
_logger = logger; _logger = logger;
} }
@@ -68,94 +70,93 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
using (Stream responseStream = new MemoryStream()) using (Stream responseStream = new MemoryStream())
using (Stream headerStream = 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; responseStream.Write(b, 0, s * n);
curlEasy.WriteFunction = (b, s, n, o) => 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); switch (result)
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) case CurlCode.SslCaCert:
{ case (CurlCode)77:
case CurlCode.SslCaCert: 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));
case (CurlCode)77: default:
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)); throw new WebException(string.Format("Curl Error {0} for Url {1}", 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) private string FixSetCookieHeader(string setCookie)
{ {
// fix up the date if it was malformed // 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 shortFormat = "ddd, dd-MMM-yy HH:mm:ss";
string longFormat = "ddd, dd-MMM-yyyy 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) private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, WebHeaderCollection webHeaderCollection)
{ {
byte[] bytes = null;
responseStream.Position = 0; responseStream.Position = 0;
if (responseStream.Length != 0) if (responseStream.Length != 0)
@@ -270,27 +270,20 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
if (encoding.IndexOf("gzip") != -1) if (encoding.IndexOf("gzip") != -1)
{ {
using (var zipStream = new GZipStream(responseStream, CompressionMode.Decompress)) responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
{
bytes = zipStream.ToBytes();
}
webHeaderCollection.Remove("Content-Encoding"); webHeaderCollection.Remove("Content-Encoding");
} }
else if (encoding.IndexOf("deflate") != -1) else if (encoding.IndexOf("deflate") != -1)
{ {
using (var deflateStream = new DeflateStream(responseStream, CompressionMode.Decompress)) responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);
{
bytes = deflateStream.ToBytes();
}
webHeaderCollection.Remove("Content-Encoding"); webHeaderCollection.Remove("Content-Encoding");
} }
} }
} }
if (bytes == null) bytes = responseStream.ToBytes(); return responseStream.ToBytes();
return bytes;
} }
} }

View File

@@ -10,21 +10,23 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
private readonly ManagedHttpDispatcher _managedDispatcher; private readonly ManagedHttpDispatcher _managedDispatcher;
private readonly CurlHttpDispatcher _curlDispatcher; private readonly CurlHttpDispatcher _curlDispatcher;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger; private readonly Logger _logger;
private readonly ICached<bool> _curlTLSFallbackCache; 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; _managedDispatcher = managedDispatcher;
_curlDispatcher = curlDispatcher; _curlDispatcher = curlDispatcher;
_platformInfo = platformInfo;
_curlTLSFallbackCache = cacheManager.GetCache<bool>(GetType(), "curlTLSFallback"); _curlTLSFallbackCache = cacheManager.GetCache<bool>(GetType(), "curlTLSFallback");
_logger = logger; _logger = logger;
} }
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) 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)) if (!_curlTLSFallbackCache.Find(request.Url.Host))
{ {

View File

@@ -2,9 +2,13 @@ using System;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Net; using System.Net;
using System.Reflection;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Security; using NzbDrone.Common.Security;
namespace NzbDrone.Common.Http.Dispatchers namespace NzbDrone.Common.Http.Dispatchers
@@ -13,55 +17,59 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy; private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly 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; _proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy; _createManagedWebProxy = createManagedWebProxy;
_userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger;
} }
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{ {
HttpWebResponse httpWebResponse = null; var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
HttpWebRequest webRequest = null;
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 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) if (request.ContentData != null)
{ {
webRequest.ContentLength = request.ContentData.Length; 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);
} }
else if (e.ToString().Contains("TLS Support not"))
httpWebResponse = (HttpWebResponse) e.Response; {
throw new TlsFailureException(webRequest, e);
if (httpWebResponse == null) }
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; 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(); data = responseStream.ToBytes();
if (OsInfo.IsMonoRuntime && httpWebResponse.ContentEncoding == "gzip") if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip")
{ {
using (var compressedStream = new MemoryStream(data)) using (var compressedStream = new MemoryStream(data))
using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress)) using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
@@ -111,17 +142,14 @@ namespace NzbDrone.Common.Http.Dispatchers
httpWebResponse.Headers.Remove("Content-Encoding"); 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, return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
httpWebResponse.StatusCode);
}
finally
{
webRequest = null;
(httpWebResponse as IDisposable)?.Dispose();
httpWebResponse = null;
}
} }
protected virtual void AddProxy(HttpWebRequest webRequest, HttpRequest request) 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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -31,12 +31,19 @@ namespace NzbDrone.Common.Http
private readonly ICached<CookieContainer> _cookieContainerCache; private readonly ICached<CookieContainer> _cookieContainerCache;
private readonly List<IHttpRequestInterceptor> _requestInterceptors; private readonly List<IHttpRequestInterceptor> _requestInterceptors;
private readonly IHttpDispatcher _httpDispatcher; 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(); _requestInterceptors = requestInterceptors.ToList();
_rateLimitService = rateLimitService; _rateLimitService = rateLimitService;
_httpDispatcher = httpDispatcher; _httpDispatcher = httpDispatcher;
_userAgentBuilder = userAgentBuilder;
_logger = logger; _logger = logger;
ServicePointManager.DefaultConnectionLimit = 12; ServicePointManager.DefaultConnectionLimit = 12;
@@ -71,7 +78,7 @@ namespace NzbDrone.Common.Http
while (response.HasHttpRedirect); 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"]); _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) if (!request.IgnorePersistentCookies)
{ {
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
sourceContainer.Add(persistentCookies); sourceContainer.Add(persistentCookies);
} }
if (request.Cookies.Count != 0) if (request.Cookies.Count != 0)
{ {
foreach (var pair in request.Cookies) 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. cookie = new Cookie(pair.Key, "", "/")
// See https://gist.github.com/ta264/7822b1424f72e5b4c961 {
Expires = DateTime.Now.AddHours(1) 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); sourceContainer.Add((Uri)request.Url, cookie);
@@ -178,7 +196,6 @@ namespace NzbDrone.Common.Http
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url); var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
cookieContainer.Add(persistentCookies); cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies); cookieContainer.Add(existingCookies);
}*/ }*/
@@ -226,13 +243,11 @@ namespace NzbDrone.Common.Http
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName); _logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
using (var webClient = new GZipWebClient()) var webClient = new GZipWebClient();
{ webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Headers.Add(HttpRequestHeader.UserAgent, UserAgentBuilder.UserAgent); webClient.DownloadFile(url, fileName);
webClient.DownloadFile(url, fileName); stopWatch.Stop();
stopWatch.Stop(); _logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
} }
catch (WebException e) catch (WebException e)
{ {
@@ -255,6 +270,7 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Get<T>(HttpRequest request) where T : new() public HttpResponse<T> Get<T>(HttpRequest request) where T : new()
{ {
var response = Get(request); var response = Get(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response); return new HttpResponse<T>(response);
} }
@@ -273,7 +289,16 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Post<T>(HttpRequest request) where T : new() public HttpResponse<T> Post<T>(HttpRequest request) where T : new()
{ {
var response = Post(request); var response = Post(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response); return new HttpResponse<T>(response);
} }
private void CheckResponseContentType(HttpResponse response)
{
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new UnexpectedHtmlContentException(response);
}
}
} }
} }

View File

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

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Common.Http
IgnorePersistentCookies = false; IgnorePersistentCookies = false;
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
if (!RuntimeInfoBase.IsProduction) if (!RuntimeInfo.IsProduction)
{ {
AllowAutoRedirect = false; 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 namespace NzbDrone.Common.Http
{ {
public static class UserAgentBuilder public interface IUserAgentBuilder
{ {
public static string UserAgent { get; private set; } string GetUserAgent(bool simplified = false);
public static string UserAgentSimplified { get; private set; } }
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})", if (simplified)
BuildInfo.Version, {
OsInfo.Os, OsInfo.Version.ToString(2)); return _userAgentSimplified;
}
UserAgentSimplified = string.Format("Radarr/{0}", return _userAgent;
BuildInfo.Version.ToString(2)); }
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 System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@@ -35,7 +35,7 @@ namespace NzbDrone.Common.Instrumentation
return; return;
} }
if (OsInfo.IsMonoRuntime) if (PlatformInfo.IsMono)
{ {
if (exception is TypeInitializationException && exception.InnerException is DllNotFoundException || if (exception is TypeInitializationException && exception.InnerException is DllNotFoundException ||
exception is DllNotFoundException) exception is DllNotFoundException)
@@ -51,4 +51,4 @@ namespace NzbDrone.Common.Instrumentation
Logger.Fatal(exception, "EPIC FAIL: " + exception.Message); Logger.Fatal(exception, "EPIC FAIL: " + exception.Message);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using LogentriesNLog; using LogentriesNLog;
@@ -48,7 +48,7 @@ namespace NzbDrone.Common.Instrumentation
} }
else else
{ {
if (inConsole && (OsInfo.IsNotWindows || RuntimeInfoBase.IsUserInteractive)) if (inConsole && (OsInfo.IsNotWindows || RuntimeInfo.IsUserInteractive))
{ {
RegisterConsole(); RegisterConsole();
} }
@@ -152,16 +152,6 @@ namespace NzbDrone.Common.Instrumentation
LogManager.Configuration.LoggingRules.Add(loggingRule); 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) public static Logger GetLogger(Type obj)
{ {
return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", "")); return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", ""));

View File

@@ -94,6 +94,10 @@
<Compile Include="Disk\RelativeFileSystemModel.cs" /> <Compile Include="Disk\RelativeFileSystemModel.cs" />
<Compile Include="Disk\FileSystemModel.cs" /> <Compile Include="Disk\FileSystemModel.cs" />
<Compile Include="Disk\FileSystemResult.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="Extensions\DictionaryExtensions.cs" />
<Compile Include="Disk\GdiPlusInterop.cs" /> <Compile Include="Disk\GdiPlusInterop.cs" />
<Compile Include="Disk\OsPath.cs" /> <Compile Include="Disk\OsPath.cs" />
@@ -125,13 +129,12 @@
<Compile Include="EnvironmentInfo\BuildInfo.cs" /> <Compile Include="EnvironmentInfo\BuildInfo.cs" />
<Compile Include="EnvironmentInfo\OsInfo.cs" /> <Compile Include="EnvironmentInfo\OsInfo.cs" />
<Compile Include="EnvironmentInfo\IRuntimeInfo.cs" /> <Compile Include="EnvironmentInfo\IRuntimeInfo.cs" />
<Compile Include="EnvironmentInfo\RuntimeInfoBase.cs" /> <Compile Include="EnvironmentInfo\RuntimeInfo.cs" />
<Compile Include="EnvironmentInfo\StartupContext.cs" /> <Compile Include="EnvironmentInfo\StartupContext.cs" />
<Compile Include="Exceptions\NotParentException.cs" /> <Compile Include="Exceptions\NotParentException.cs" />
<Compile Include="Exceptions\NzbDroneException.cs" /> <Compile Include="Exceptions\NzbDroneException.cs" />
<Compile Include="Exceptron\Configuration\ExceptronConfiguration.cs" /> <Compile Include="Exceptron\Configuration\ExceptronConfiguration.cs" />
<Compile Include="Exceptron\ExceptionData.cs" /> <Compile Include="Exceptron\ExceptionData.cs" />
<Compile Include="Exceptron\ExceptionExtentions.cs" />
<Compile Include="Exceptron\ExceptionSeverity.cs" /> <Compile Include="Exceptron\ExceptionSeverity.cs" />
<Compile Include="Exceptron\ExceptronApiException.cs" /> <Compile Include="Exceptron\ExceptronApiException.cs" />
<Compile Include="Exceptron\ExceptronClient.cs" /> <Compile Include="Exceptron\ExceptronClient.cs" />
@@ -190,11 +193,12 @@
<Compile Include="Http\HttpRequestBuilder.cs" /> <Compile Include="Http\HttpRequestBuilder.cs" />
<Compile Include="Http\HttpRequestBuilderFactory.cs" /> <Compile Include="Http\HttpRequestBuilderFactory.cs" />
<Compile Include="Http\Proxy\ProxyType.cs" /> <Compile Include="Http\Proxy\ProxyType.cs" />
<Compile Include="Http\TlsFailureException.cs" />
<Compile Include="Http\TooManyRequestsException.cs" /> <Compile Include="Http\TooManyRequestsException.cs" />
<Compile Include="Extensions\IEnumerableExtensions.cs" /> <Compile Include="Extensions\IEnumerableExtensions.cs" />
<Compile Include="Http\UnexpectedHtmlContentException.cs" />
<Compile Include="Http\UserAgentBuilder.cs" /> <Compile Include="Http\UserAgentBuilder.cs" />
<Compile Include="Instrumentation\CleanseLogMessage.cs" /> <Compile Include="Instrumentation\CleanseLogMessage.cs" />
<Compile Include="Instrumentation\ExceptronTarget.cs" />
<Compile Include="Instrumentation\Extensions\LoggerProgressExtensions.cs" /> <Compile Include="Instrumentation\Extensions\LoggerProgressExtensions.cs" />
<Compile Include="Instrumentation\GlobalExceptionHandlers.cs" /> <Compile Include="Instrumentation\GlobalExceptionHandlers.cs" />
<Compile Include="Instrumentation\LogEventExtensions.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) 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); args = GetMonoArgs(path, args);
path = "mono"; path = "mono";
@@ -192,7 +192,7 @@ namespace NzbDrone.Common.Processes
public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null) 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); args = GetMonoArgs(path, args);
path = "mono"; path = "mono";

View File

@@ -9,6 +9,7 @@ using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent; using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Core.Exceptions;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{ {
@@ -37,8 +38,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0])); .Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
Mocker.GetMock<IQBittorrentProxy>() Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>())) .Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences()); .Returns(new QBittorrentPreferences() { DhtEnabled = true });
Mocker.GetMock<IQBittorrentProxySelector>()
.Setup(s => s.GetProxy(It.IsAny<QBittorrentSettings>(), It.IsAny<bool>()))
.Returns(Mocker.GetMock<IQBittorrentProxy>().Object);
} }
protected void GivenRedirectToMagnet() protected void GivenRedirectToMagnet()
@@ -154,7 +159,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
VerifyPaused(item); VerifyPaused(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero); item.RemainingTime.Should().NotHaveValue();
} }
[TestCase("pausedUP")] [TestCase("pausedUP")]
@@ -162,6 +167,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("uploading")] [TestCase("uploading")]
[TestCase("stalledUP")] [TestCase("stalledUP")]
[TestCase("checkingUP")] [TestCase("checkingUP")]
[TestCase("forcedUP")]
public void completed_item_should_have_required_properties(string state) public void completed_item_should_have_required_properties(string state)
{ {
var torrent = new QBittorrentTorrent var torrent = new QBittorrentTorrent
@@ -184,6 +190,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("queuedDL")] [TestCase("queuedDL")]
[TestCase("checkingDL")] [TestCase("checkingDL")]
[TestCase("metaDL")]
public void queued_item_should_have_required_properties(string state) public void queued_item_should_have_required_properties(string state)
{ {
var torrent = new QBittorrentTorrent var torrent = new QBittorrentTorrent
@@ -201,7 +208,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
VerifyQueued(item); VerifyQueued(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero); item.RemainingTime.Should().NotHaveValue();
} }
[Test] [Test]
@@ -243,7 +250,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
VerifyWarning(item); VerifyWarning(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero); item.RemainingTime.Should().NotHaveValue();
} }
[Test] [Test]
@@ -271,6 +278,35 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
id.Should().Be(expectedHash); id.Should().Be(expectedHash);
} }
[Test]
public void Download_should_refuse_magnet_if_no_trackers_provided_and_dht_is_disabled()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences() { DhtEnabled = false });
var 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] [Test]
public void Download_should_set_top_priority() public void Download_should_set_top_priority()
{ {
@@ -494,5 +530,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single(); var item = Subject.GetItems().Single();
item.Category.Should().Be(category); item.Category.Should().Be(category);
} }
[Test]
public void 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 Moq;
using System;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
@@ -24,12 +24,16 @@ namespace NzbDrone.Core.Test.Framework
{ {
protected void UseRealHttp() 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<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>())); Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>())); Mocker.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<NLog.Logger>())); Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger)); Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), 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()); 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.Common.EnvironmentInfo;
using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -8,17 +9,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[TestFixture] [TestFixture]
public class MonoVersionCheckFixture : CoreTest<MonoVersionCheck> public class MonoVersionCheckFixture : CoreTest<MonoVersionCheck>
{ {
[SetUp]
public void Setup()
{
MonoOnly();
}
private void GivenOutput(string version) private void GivenOutput(string version)
{ {
Mocker.GetMock<IRuntimeInfo>() MonoOnly();
.SetupGet(s => s.RuntimeVersion) Mocker.GetMock<IPlatformInfo>()
.Returns(string.Format("{0} (tarball Wed Sep 25 16:35:44 CDT 2013)", version)); .SetupGet(s => s.Version)
.Returns(new Version(version));
} }
[TestCase("3.10")] [TestCase("3.10")]

View File

@@ -18,7 +18,8 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
} }
[TestCase("Prometheus", "Prometheus")] [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:tt2527336", "Star Wars: The Last Jedi")]
[TestCase("imdb:tt2798920", "Annihilation")] [TestCase("imdb:tt2798920", "Annihilation")]
public void successful_search(string title, string expected) 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\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchMovieComparerFixture.cs" /> <Compile Include="MetadataSource\SearchMovieComparerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.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\CouchPotato\CouchPotatoParserFixture.cs" />
<Compile Include="NetImport\RSSImportFixture.cs" /> <Compile Include="NetImport\RSSImportFixture.cs" />
<Compile Include="NetImport\RSSImportParserFixture.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.Mandarin.HDTV.XviD-LOL", Language.Mandarin)]
[TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)] [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)]
[TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL", Language.Russian)] [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.Polish.HDTV.XviD-LOL", Language.Polish)]
[TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)] [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)]
[TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL", Language.Swedish)] [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.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 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) public void should_parse_subtitle_language(string fileName, Language language)
{ {
var result = LanguageParser.ParseSubtitleLanguage(fileName); 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("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("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) public void should_parse_remux2160p_quality(string title)
{ {
ParseAndVerifyQuality(title, Source.BLURAY, false, Resolution.R2160P, Modifier.REMUX); 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("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-postbot", "NTb")]
[TestCase("Haunted.Hayride.2018.720p.WEBRip.DDP5.1.x264-NTb-xpost", "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("", "")] //[TestCase("", "")]
public void should_parse_release_group(string title, string expected) public void should_parse_release_group(string title, string expected)
{ {

View File

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

View File

@@ -17,6 +17,6 @@ namespace NzbDrone.Core.Analytics
_configFileProvider = configFileProvider; _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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
@@ -373,7 +374,7 @@ namespace NzbDrone.Core.Configuration
public int FirstDayOfWeek public int FirstDayOfWeek
{ {
get { return GetValueInt("FirstDayOfWeek", (int)OsInfo.FirstDayOfWeek); } get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); }
set { SetValue("FirstDayOfWeek", value); } set { SetValue("FirstDayOfWeek", value); }
} }

View File

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

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

View File

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

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

View File

@@ -66,15 +66,13 @@ namespace NzbDrone.Core.Extras
var sourcePath = localMovie.Path; var sourcePath = localMovie.Path;
var sourceFolder = _diskProvider.GetParentFolder(sourcePath); var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
var sourceFileName = Path.GetFileNameWithoutExtension(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) var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim(' ', '.')) .Select(e => e.Trim(' ', '.'))
.ToList(); .ToList();
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase)); foreach (var matchingFilename in files)
foreach (var matchingFilename in matchingFilenames)
{ {
var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e)); 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))) if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
{ {
var language = LanguageParser.ParseSubtitleLanguage(path); var language = LanguageParser.ParseSubtitleLanguage(path);
var suffix = GetSuffix(language, 1, false); var subtitleFiles = _subtitleFileService.GetFilesByMovie(movie.Id);
var subtitleFile = ImportFile(movie, movieFile, path, readOnly, extension, suffix); var existingSrtSubs = subtitleFiles.Where(m => m.MovieFileId == movieFile.Id)
subtitleFile.Language = language; .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; return subtitleFile;
} }

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@@ -9,50 +8,43 @@ namespace NzbDrone.Core.HealthCheck.Checks
{ {
public class MonoVersionCheck : HealthCheckBase public class MonoVersionCheck : HealthCheckBase
{ {
private readonly IRuntimeInfo _runtimeInfo; private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger; 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; _logger = logger;
} }
public override HealthCheck Check() public override HealthCheck Check()
{ {
if (OsInfo.IsWindows) if (!PlatformInfo.IsMono)
{ {
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
var versionString = _runtimeInfo.RuntimeVersion; var monoVersion = _platformInfo.Version;
var versionMatch = VersionRegex.Match(versionString);
if (versionMatch.Success) if (monoVersion == new Version("3.4.0") && HasMonoBug18599())
{ {
var version = new Version(versionMatch.Groups["version"].Value); _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");
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());
}
} }
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; public override bool CheckOnConfigChange => false;
@@ -70,7 +62,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
return false; return false;
} }
var fieldInfo = numberFormatterType.GetField("userFormatProvider", BindingFlags.Static | BindingFlags.NonPublic); var fieldInfo = numberFormatterType.GetField("userFormatProvider",
BindingFlags.Static | BindingFlags.NonPublic);
if (fieldInfo == null) if (fieldInfo == null)
{ {

View File

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

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@@ -12,6 +12,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
public RarbgSettingsValidator() public RarbgSettingsValidator()
{ {
RuleFor(c => c.BaseUrl).ValidRootUrl(); RuleFor(c => c.BaseUrl).ValidRootUrl();
RuleFor(c => c.Categories).NotEmpty();
} }
} }
@@ -24,6 +25,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
BaseUrl = "https://torrentapi.org"; BaseUrl = "https://torrentapi.org";
RankedOnly = false; RankedOnly = false;
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; 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.")] [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)] [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; } 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() public NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));

View File

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

View File

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

View File

@@ -189,7 +189,6 @@ namespace NzbDrone.Core.MediaFiles
catch (Exception ex) catch (Exception ex)
{ {
ex.ExceptronIgnoreOnMono();
_logger.Warn(ex, "Unable to set date of file [" + filePath + "]"); _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 // 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 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 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 (response.Headers.ContainsKey("X-RateLimit-Remaining"))
if (remaining <= 5)
{ {
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies"); var remaining = int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First());
Thread.Sleep(5000); if (remaining <= 5)
{
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies");
Thread.Sleep(5000);
}
} }
var resource = response.Resource; var resource = response.Resource;
@@ -333,11 +336,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
// The dude abides, so should us, Lets be nice to TMDb // 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 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 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 (response.Headers.ContainsKey("X-RateLimit-Remaining"))
if (remaining <= 5)
{ {
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies"); var remaining = int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First());
Thread.Sleep(5000); if (remaining <= 5)
{
_logger.Trace("Waiting 5 seconds to get information for the next 35 movies");
Thread.Sleep(5000);
}
} }
var resources = response.Resource; var resources = response.Resource;

View File

@@ -17,30 +17,30 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
public int VoteCount { get; set; } public int VoteCount { get; set; }
public Language Language { get; set; } public Language Language { get; set; }
public LazyLoaded<Movie> Movie { get; set; } public LazyLoaded<Movie> Movie { get; set; }
public AlternativeTitle() 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 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) switch (SourceType)
{ {
case SourceType.TMDB: case SourceType.Mappings:
return Votes >= minVotes; return Votes >= minVotes;
default: default:
return true; return true;
} }
} }
public override bool Equals(object obj) public override bool Equals(object obj)

View File

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

View File

@@ -3,6 +3,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies.Events; using NzbDrone.Core.Movies.Events;
namespace NzbDrone.Core.Movies.AlternativeTitles namespace NzbDrone.Core.Movies.AlternativeTitles
@@ -13,7 +14,7 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie); AlternativeTitle AddAltTitle(AlternativeTitle title, Movie movie);
List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie); List<AlternativeTitle> AddAltTitles(List<AlternativeTitle> titles, Movie movie);
AlternativeTitle GetById(int id); AlternativeTitle GetById(int id);
void DeleteNotEnoughVotes(List<AlternativeTitle> mappingsTitles); List<AlternativeTitle> UpdateTitles(List<AlternativeTitle> titles, Movie movie);
} }
public class AlternativeTitleService : IAlternativeTitleService, IHandleAsync<MovieDeletedEvent> public class AlternativeTitleService : IAlternativeTitleService, IHandleAsync<MovieDeletedEvent>
@@ -63,11 +64,28 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
_titleRepo.Delete(title); _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); int movieId = movie.Id;
var realT = _titleRepo.FindBySourceIds(toRemove.Select(t => t.SourceId).ToList()); // First update the movie ids so we can correlate them later.
_titleRepo.DeleteMany(realT); 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) 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); _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 try
{ {
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(movieInfo.AlternativeTitles, movie));
var mappings = _apiClient.AlternativeTitlesAndYearForMovie(movieInfo.TmdbId); var mappings = _apiClient.AlternativeTitlesAndYearForMovie(movieInfo.TmdbId);
var mappingsTitles = mappings.Item1; var mappingsTitles = mappings.Item1;
_titleService.DeleteNotEnoughVotes(mappingsTitles); mappingsTitles = mappingsTitles.Where(t => t.IsTrusted()).ToList();
mappingsTitles = mappingsTitles.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles, movieInfo.AlternativeTitles.AddRange(mappingsTitles);
t => t.CleanTitle, EqualityComparer<string>.Default).ToList();
movie.AlternativeTitles = _titleService.UpdateTitles(movieInfo.AlternativeTitles, movie);
mappingsTitles = mappingsTitles.Where(t => t.Votes > 3).ToList();
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(mappingsTitles, movie));
if (mappings.Item2 != null) if (mappings.Item2 != null)
{ {

View File

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

View File

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

View File

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

View File

@@ -13,11 +13,10 @@ namespace NzbDrone.Core.Parser
{ {
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser)); 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); 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 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) public static List<Language> ParseLanguages(string title)
{ {
var lowerTitle = title.ToLower(); var lowerTitle = title.ToLower();
@@ -86,6 +85,9 @@ namespace NzbDrone.Core.Parser
if (lowerTitle.Contains("czech")) if (lowerTitle.Contains("czech"))
languages.Add( Language.Czech); languages.Add( Language.Czech);
if (lowerTitle.Contains("ukrainian"))
languages.Add(Language.Ukrainian);
var match = LanguageRegex.Match(title); var match = LanguageRegex.Match(title);
if (match.Groups["italian"].Captures.Cast<Capture>().Any()) if (match.Groups["italian"].Captures.Cast<Capture>().Any())
@@ -118,6 +120,8 @@ namespace NzbDrone.Core.Parser
if (match.Groups["czech"].Success) if (match.Groups["czech"].Success)
languages.Add( Language.Czech); languages.Add( Language.Czech);
if (match.Groups["ukrainian"].Success)
languages.Add( Language.Ukrainian);
return languages.DistinctBy(l => (int)l).ToList(); return languages.DistinctBy(l => (int)l).ToList();
} }
@@ -148,13 +152,19 @@ namespace NzbDrone.Core.Parser
var simpleFilename = Path.GetFileNameWithoutExtension(fileName); var simpleFilename = Path.GetFileNameWithoutExtension(fileName);
var languageMatch = SubtitleLanguageRegex.Match(simpleFilename); var languageMatch = SubtitleLanguageRegex.Match(simpleFilename);
if (!languageMatch.Success)
{
languageMatch = RarbgSubtitleLanguageRegex.Match(simpleFilename);
}
if (languageMatch.Success) if (languageMatch.Success)
{ {
var isoCode = languageMatch.Groups["iso_code"].Value; 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 #if !LIBRARY
Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName); 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]))(?=[_.-])", 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); 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); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanTorrentSuffixRegex = new Regex(@"\[(?:ettv|rartv|rarbg|cttv)\]$", 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 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); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex BRDISKRegex = new Regex(@"\b(COMPLETE|ISO|BDISO|BD25|BD50|BR.?DISK)\b", 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) public static RestClient BuildClient(string baseUrl)
{ {
var restClient = new RestClient(baseUrl); var restClient = new RestClient(baseUrl)
{
restClient.UserAgent = string.Format("Radarr/{0} (RestSharp/{1}; {2}/{3})", UserAgent = $"Radarr/{BuildInfo.Version} ({OsInfo.Os})"
BuildInfo.Version, };
restClient.GetType().Assembly.GetName().Version,
OsInfo.Os, OsInfo.Version.ToString(2));
return restClient; return restClient;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,41 +3,11 @@ using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests.WantedTests
{ {
[TestFixture] [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)] [Test, Order(1)]
public void cutoff_should_have_monitored_items() public void cutoff_should_have_monitored_items()
{ {
@@ -45,21 +15,11 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = EnsureMovie(680, "Pulp Fiction", true); var movie = EnsureMovie(680, "Pulp Fiction", true);
EnsureMovieFile(movie, Quality.SDTV); 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(); 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)] [Test, Order(1)]
public void cutoff_should_not_have_unmonitored_items() public void cutoff_should_not_have_unmonitored_items()
{ {
@@ -67,7 +27,19 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = EnsureMovie(680, "Pulp Fiction", false); var movie = EnsureMovie(680, "Pulp Fiction", false);
EnsureMovieFile(movie, Quality.SDTV); 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(); result.Records.Should().BeEmpty();
} }
@@ -84,16 +56,6 @@ namespace NzbDrone.Integration.Test.ApiTests
result.Records.First().Title.Should().Be("Pulp Fiction"); 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)] [Test, Order(2)]
public void cutoff_should_have_unmonitored_items() public void cutoff_should_have_unmonitored_items()
{ {
@@ -105,5 +67,17 @@ namespace NzbDrone.Integration.Test.ApiTests
result.Records.Should().NotBeEmpty(); 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; return result;
} }
@@ -254,9 +256,13 @@ namespace NzbDrone.Integration.Test
if (result.MovieFile == null) 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)); 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"); File.WriteAllText(path, "Fake Movie");
Commands.PostAndWait(new CommandResource { Name = "refreshmovie", Body = new RefreshMovieCommand(movie.Id) }); 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\MovieFileFixture.cs" />
<Compile Include="ApiTests\FileSystemFixture.cs" /> <Compile Include="ApiTests\FileSystemFixture.cs" />
<Compile Include="ApiTests\MovieLookupFixture.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\ClientBase.cs" />
<Compile Include="Client\IndexerClient.cs" /> <Compile Include="Client\IndexerClient.cs" />
<Compile Include="Client\DownloadClientClient.cs" /> <Compile Include="Client\DownloadClientClient.cs" />
@@ -180,20 +181,20 @@
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<PostBuildEvent Condition="('$(OS)' == 'Windows_NT')"> <PostBuildEvent Condition="('$(OS)' == 'Windows_NT')">
xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Mono.*" "$(TargetDir)" xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Mono.*" "$(TargetDir)"
xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Windows.*" "$(TargetDir)" xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Windows.*" "$(TargetDir)"
</PostBuildEvent> </PostBuildEvent>
<PostBuildEvent Condition="('$(OS)' != 'Windows_NT')"> <PostBuildEvent Condition="('$(OS)' != 'Windows_NT')">
cp -rv $(SolutionDir)\..\_output\NzbDrone.Mono.* $(TargetDir) cp -rv $(SolutionDir)\..\_output\NzbDrone.Mono.* $(TargetDir)
cp -rv $(SolutionDir)\..\_output\NzbDrone.Windows.* $(TargetDir) cp -rv $(SolutionDir)\..\_output\NzbDrone.Windows.* $(TargetDir)
</PostBuildEvent> </PostBuildEvent>
</PropertyGroup> </PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">
</Target> </Target>
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
</Project> </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