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

Compare commits

..

79 Commits

Author SHA1 Message Date
Taloth Saldono 63853494f8 Added active detection for updatecheck so we know which os/runtime versions don't need to be supported anymore. 2020-03-13 14:53:35 +01:00
Taloth Saldono 6593575850 Fixed: Don't auto-search newly added episodes on tvdb that aired more than 2 weeks ago
Fixed: Don't monitor newly added old episodes on tvdb if series was previously empty
2020-03-13 13:55:48 +01:00
Taloth Saldono bbf74a8835 Fixed: Workaround for mono 5.16+ bug preventing the closure of sockets on timeouts (Jackett connections)
ref #2802
2019-07-02 21:00:02 +02:00
Taloth Saldono 1fc2866032 Fixed: Include all download items if no category is specified in rtorrent.
closes #3002
2019-03-24 15:27:41 +01:00
Taloth Saldono eb2e7b9c79 Continue Test in case of validation warnings. 2019-03-24 15:22:50 +01:00
Taloth Saldono cab900f656 Don't skip magnet links with included trackers if dht is disabled. 2019-03-24 14:58:13 +01:00
Taloth Saldono e2b91e5dc4 Fixed: Detecting if qbittorrent seeding time limit has been reached 2019-03-23 22:58:43 +01:00
Taloth Saldono e52fcf843c Handle Deluge v2 beta breaking change in their api.
closes #2412
2019-03-03 23:10:28 +01:00
Taloth Saldono 08ba273089 fixed qbittorrent tests failing due to incorrect test setup. And http tests failed due to httpbin changing their output. 2019-03-03 21:19:25 +01:00
Taloth Saldono faa2d632e5 New: Indexer Seed Limit settings applied to new downloads for qBittorrent
closes #2607
2019-03-03 20:25:31 +01:00
Taloth Saldono 1b939ebf4b Fixed: Magnet Link progress visualisation and adding magnet links if dht is disabled in qBittorrent 2019-03-03 19:29:25 +01:00
Mark Bebbington aa46216117 Fixed: qBittorrent api v2 support (qbit v4.1+)
fixes #2887
closes #2951
ref #2945
2019-03-03 19:26:50 +01:00
Mark McDowall c3c6b3d166 Fixed: Importing completed downloads from NZBGet with post processing script failing
Fixes #2919

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

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

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