1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

53 Commits

Author SHA1 Message Date
Mark McDowall
7e9f0d0522 Updated analytics help text 2017-02-21 11:18:29 -08:00
Taloth Saldono
1f8bd8e1e9 Fixed typo in DL station hint text. 2017-02-21 18:19:55 +01:00
Taloth Saldono
2855090005 Fixed: Removed Womble indexer. 2017-02-21 17:03:10 +01:00
Taloth Saldono
060b9f6fd1 Fixed: Updated BTN api url. 2017-02-21 16:40:20 +01:00
margaale
9304547c95 Test if the OutputPath specified by TvDirectory/TvCategory exists. 2017-02-21 16:40:16 +01:00
margaale
c56c83e169 New: Added support for nzb downloads in Synology Download Station. 2017-02-20 18:57:11 +01:00
Mark McDowall
c6fa883662 Fixed: Saving nyaa settings
Fixes #1687
2017-02-16 09:19:28 -08:00
Mark McDowall
4043d07ab1 Verify LimeTorrents parsing 2017-02-15 22:30:03 -08:00
Mark McDowall
8af3348e7f Fixed: Slow loading root folders caused them to never appear 2017-02-15 22:30:03 -08:00
Taloth Saldono
49d0d4c357 Renamed DownloadStation implementation to TorrentDownloadStation. 2017-02-15 21:32:25 +01:00
Taloth Saldono
47b1157b96 Fixed: Permanently removed kickass rss/api implementation. 2017-02-15 21:32:21 +01:00
Taloth Saldono
adc79f0eba Added more sensible error for BTN html response. 2017-02-15 20:40:32 +01:00
Taloth Saldono
6b117427f8 Fixed double question mark in log. 2017-02-15 20:40:32 +01:00
Mark McDowall
7884dd9a39 New: Added omgwtfnzbs Newznab prefix 2017-02-13 22:46:26 -08:00
Marcelo Castagna
45d8b1e2ad Fixed: Delete data when removing torrent from Download Station
fixes #1676
2017-02-13 20:17:52 +01:00
Marcelo Castagna
cf306f4aba Throw exception with error message return by diskstation (#1672) 2017-02-12 20:20:16 +01:00
Mark McDowall
d7aa23388e New: Update Media info for Windows/macOS to 0.7.92.1 2017-02-11 16:29:49 -08:00
margaale
82a99b7f80 New: Added support for Synology Download Station as torrent client. 2017-02-11 21:06:23 +01:00
Taloth Saldono
2f6d9e191e Fixed: Ignore .nfs* files during copy actions since those files are special NFS files that should never be touched.
fixes #1552
2017-02-09 19:33:28 +01:00
Taloth Saldono
0782a15979 Remove backslashes from BTN release titles.
fixes #1075
2017-02-09 19:33:28 +01:00
vertigo235
ddd119a4eb New: Add paused option for NZBGet
Closes #346
2017-02-08 20:36:39 -08:00
Taloth Saldono
d4788b4cae Added tests for edge-case.
closes #1147
2017-02-08 22:10:30 +01:00
Taloth Saldono
812999423b Fixed: Don't try to show diskspace usage non-existing drives.
fixes #1639
2017-02-07 23:06:14 +01:00
Taloth Saldono
657730f4d2 Fixed: /var/lib/docker no longer shows up in DiskSpace. Caused warnings if the user used docker with zfs storage driver.
fixes #1663
2017-02-07 22:44:31 +01:00
Taloth Saldono
0255eb3aca Fixed: Increased timeout when waiting for rtorrent to finish adding torrent.
fixes #1665
2017-02-07 22:36:47 +01:00
Mark McDowall
fc15daa37e New: Improve parsing of audio channels from MediaInfo output 2017-02-04 22:04:12 -08:00
Mark McDowall
10264a5bfb New: Ensure folders are sorted alphabetically when importing
Closes #294
2017-02-04 22:04:12 -08:00
Mark McDowall
ef044f1ff5 Update README.md 2017-01-27 20:56:07 -08:00
Mark McDowall
ef03e9e9a7 Fixed: Proper port validation for download clients and connections
Closes #1642
2017-01-26 22:35:16 -08:00
Mark McDowall
3bd7c09acf Strip 2160p from titles before parsing 2017-01-23 23:53:15 -08:00
Keivan Beigi
fbd2f8dea4 Fixed: Growl download notification title 2017-01-22 13:07:21 -08:00
Keivan Beigi
15e07f72d4 Better Runtime names 2017-01-20 20:54:04 -08:00
Keivan Beigi
f25bfe9d28 don't log migrations during regular DB tests 2017-01-20 20:33:10 -08:00
Keivan Beigi
d5e720c404 include os name, runtime name in version tag for sentry 2017-01-20 20:16:34 -08:00
Keivan Beigi
c9a8ebc2e6 Create anonymous hash to detect issue duplication 2017-01-20 20:15:49 -08:00
Mark McDowall
5e7e816c03 AsOsAgnostic paths for root folder tests 2017-01-20 09:02:36 -08:00
vertigo235
f56076a135 Fixed: Pushover silent priority 2017-01-19 23:51:58 -08:00
Mark McDowall
54dd527f01 Exclude .grab and Plex Version folders 2017-01-19 01:38:37 -08:00
Mark McDowall
c6eb19c04d Exclude .grab and Plex Version folders
New: Ignore .grab folder (Plex DVR)
New: Ignore Plex Versions folder (Media Optimizer)
Closes #1610
2017-01-18 20:15:32 -08:00
Mitchell Cash
38b65ba27d Cleanup README (#1622) 2017-01-18 12:31:20 -08:00
Keivan Beigi
a2a49ce934 Revert "New: Upgraded SQLite binares for macOS"
This reverts commit 8d91f18823.
2017-01-18 10:04:36 -08:00
Keivan Beigi
047d5a4388 Revert "New: Upgraded SQLite binaries for Windows (3.16.0)"
This reverts commit 111e401a2c.
2017-01-18 10:04:26 -08:00
Keivan Beigi
aae69ff49a Revert "Upgraded System.Data.SQLite to 1.0.104.0"
This reverts commit 01e2f4e7e5.
2017-01-18 10:04:07 -08:00
Sander Ploegsma
da451cfe03 Option to convert ical feed items to all-day events 2017-01-17 22:36:48 +01:00
Keivan Beigi
01e2f4e7e5 Upgraded System.Data.SQLite to 1.0.104.0 2017-01-17 11:47:46 -08:00
Keivan Beigi
8aacc61c50 New: Switched nyaa.se to HTTPS 2017-01-17 11:47:46 -08:00
Keivan Beigi
111e401a2c New: Upgraded SQLite binaries for Windows (3.16.0) 2017-01-17 11:47:45 -08:00
Keivan
8d91f18823 New: Upgraded SQLite binares for macOS
Upgraded from 3.8.1 to 3.9.1
2017-01-17 11:47:45 -08:00
Keivan Beigi
cea6469ab8 Use nameof 2017-01-17 11:47:06 -08:00
Mark McDowall
ced7a7dce2 New: Prefer anime batch releases over single episode releases 2017-01-14 12:28:22 -08:00
Mitchell Cash
20a2cfe260 Use DOGnzb name as the default rather than the URL 2017-01-14 08:45:29 -08:00
Drew Freyling
5b0a285b84 New: Reduced image file sizes 2017-01-12 13:10:19 -08:00
Mark McDowall
68ea8a551c Fixed: Parsing of SABnzbd develop version 2017-01-12 00:38:56 -08:00
144 changed files with 4041 additions and 1133 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 34 KiB

52
README.md Normal file
View File

@@ -0,0 +1,52 @@
# Sonarr
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
## Major Features Include:
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new episodes
* Can scan your existing library and download any missing episodes
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Fully configurable episode renaming
* Full integration with SABnzbd and NZBGet
* Full integration with Kodi, Plex (notification, library update, metadata)
* Full support for specials and multi-episode releases
* And a beautiful UI
## Configuring Development Environment:
### Requirements
* Visual Studio 2015 (https://www.visualstudio.com/vs/)
* [Git](https://git-scm.com/downloads)
* [NodeJS](https://nodejs.org/download/)
### Setup
* Make sure all the required software mentioned above are installed.
* Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
* Grab the submodules `git submodule init && git submodule update`
* Install the required Node Packages `npm install`
* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
*Please note gulp must be running at all times while you are working with Sonarr client source files.*
### Development
* Open `NzbDrone.sln` in Visual Studio
* Make sure `NzbDrone.Console` is set as the startup project
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2017
### Sponsors
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [ReSharper](http://www.jetbrains.com/resharper/)
* [WebStorm](http://www.jetbrains.com/webstorm/)
* [TeamCity](http://www.jetbrains.com/teamcity/)

View File

@@ -1,53 +0,0 @@
# Sonarr #
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
## Major Features Include: ##
* Support for major platforms: Windows, Linux, OSX, Raspberry Pi, etc.
* Automatically detects new episodes
* Can scan your existing library and download any missing episodes
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Fully configurable episode renaming
* Full integration with SABNzbd and NzbGet
* Full integration with XBMC, Plex (notification, library update, metadata)
* Full support for specials and multi-episode releases
* And a beautiful UI
## Configuring Development Environment: ##
### Requirements ###
- Visual Studio 2015 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
- [Git](http://git-scm.com/downloads)
- [NodeJS](http://nodejs.org/download/)
### Setup ###
- Make sure all the required software mentioned above are installed.
- Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
- Grab the submodules `git submodule init && git submodule update`
- install the required Node Packages `npm install`
- start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
*Please note gulp must be running at all times while you are working with Sonarr client source files.*
### Development ###
- Open `NzbDrone.sln` in Visual Studio
- Make sure `NzbDrone.Console` is set as the startup project
### License ###
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
Copyright 2010-2016
### Sponsors ###
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
- [ReSharper](http://www.jetbrains.com/resharper/)
- [WebStorm](http://www.jetbrains.com/webstorm/)
- [TeamCity](http://www.jetbrains.com/teamcity/)

View File

@@ -37,6 +37,7 @@ namespace NzbDrone.Api.Calendar
var end = DateTime.Today.AddDays(futureDays);
var unmonitored = false;
var premiersOnly = false;
var asAllDay = false;
var tags = new List<int>();
// TODO: Remove start/end parameters in v3, they don't work well for iCal
@@ -46,6 +47,7 @@ namespace NzbDrone.Api.Calendar
var queryFutureDays = Request.Query.FutureDays;
var queryUnmonitored = Request.Query.Unmonitored;
var queryPremiersOnly = Request.Query.PremiersOnly;
var queryAsAllDay = Request.Query.AsAllDay;
var queryTags = Request.Query.Tags;
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
@@ -73,6 +75,11 @@ namespace NzbDrone.Api.Calendar
premiersOnly = bool.Parse(queryPremiersOnly.Value);
}
if (queryAsAllDay.HasValue)
{
asAllDay = bool.Parse(queryAsAllDay.Value);
}
if (queryTags.HasValue)
{
var tagInput = (string)queryTags.Value.ToString();
@@ -102,11 +109,19 @@ namespace NzbDrone.Api.Calendar
var occurrence = calendar.Create<Event>();
occurrence.Uid = "NzbDrone_episode_" + episode.Id;
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true };
occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true };
occurrence.Description = episode.Overview;
occurrence.Categories = new List<string>() { episode.Series.Network };
if (asAllDay)
{
occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = false };
}
else
{
occurrence.Start = new CalDateTime(episode.AirDateUtc.Value) { HasTime = true };
occurrence.End = new CalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true };
}
switch (episode.Series.SeriesType)
{
case SeriesTypes.Daily:

View File

@@ -73,14 +73,14 @@ namespace NzbDrone.Api.ClientSchema
if (propertyInfo.PropertyType == typeof(int))
{
var value = Convert.ToInt32(field.Value);
propertyInfo.SetValue(target, value, null);
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyInfo.PropertyType == typeof(long))
{
var value = Convert.ToInt64(field.Value);
propertyInfo.SetValue(target, value, null);
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyInfo.PropertyType == typeof(int?))

View File

@@ -39,7 +39,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
context.Items["ApiRequestStartTime"] = DateTime.UtcNow;
var reqPath = GetRequestPathAndQuery(context.Request);
_loggerHttp.Trace("Req: {0} [{1}] {2}", id, context.Request.Method, reqPath);
return null;
@@ -80,7 +80,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
{
if (request.Url.Query.IsNotNullOrWhiteSpace())
{
return string.Concat(request.Url.Path, "?", request.Url.Query);
return string.Concat(request.Url.Path, request.Url.Query);
}
else
{
@@ -88,4 +88,4 @@ namespace NzbDrone.Api.Extensions.Pipelines
}
}
}
}
}

View File

@@ -16,6 +16,7 @@ namespace NzbDrone.Common.Test.DiskTests
private readonly string _targetPath = @"C:\target\my.video.mkv".AsOsAgnostic();
private readonly string _backupPath = @"C:\source\my.video.mkv.backup~".AsOsAgnostic();
private readonly string _tempTargetPath = @"C:\target\my.video.mkv.partial~".AsOsAgnostic();
private readonly string _nfsFile = ".nfs01231232";
[SetUp]
public void SetUp()
@@ -642,6 +643,21 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(source.FullName, destination.FullName);
}
[Test]
public void CopyFolder_should_ignore_nfs_temp_file()
{
WithRealDiskProvider();
var source = GetFilledTempFolder();
File.WriteAllText(Path.Combine(source.FullName, _nfsFile), "SubFile1");
var destination = new DirectoryInfo(GetTempFilePath());
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy);
File.Exists(Path.Combine(destination.FullName, _nfsFile)).Should().BeFalse();
}
[Test]
public void MoveFolder_should_move_folder()
@@ -704,6 +720,26 @@ namespace NzbDrone.Common.Test.DiskTests
destination.GetFileSystemInfos().Should().BeEmpty();
}
[Test]
public void MirrorFolder_should_not_remove_nfs_files()
{
WithRealDiskProvider();
var original = GetFilledTempFolder();
var source = new DirectoryInfo(GetTempFilePath());
var destination = new DirectoryInfo(GetTempFilePath());
source.Create();
Subject.TransferFolder(original.FullName, destination.FullName, TransferMode.Copy);
File.WriteAllText(Path.Combine(destination.FullName, _nfsFile), "SubFile1");
var count = Subject.MirrorFolder(source.FullName, destination.FullName);
count.Should().Equals(0);
destination.GetFileSystemInfos().Should().HaveCount(1);
}
[Test]
public void MirrorFolder_should_add_new_files()
{
@@ -721,6 +757,24 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void MirrorFolder_should_ignore_nfs_temp_file()
{
WithRealDiskProvider();
var source = GetFilledTempFolder();
File.WriteAllText(Path.Combine(source.FullName, _nfsFile), "SubFile1");
var destination = new DirectoryInfo(GetTempFilePath());
var count = Subject.MirrorFolder(source.FullName, destination.FullName);
count.Should().Equals(3);
File.Exists(Path.Combine(destination.FullName, _nfsFile)).Should().BeFalse();
}
[Test]
public void MirrorFolder_should_not_touch_equivalent_files()
{

View File

@@ -0,0 +1,21 @@
using FluentAssertions;
using NUnit.Framework;
namespace NzbDrone.Common.Test
{
[TestFixture]
public class HashUtilFixture
{
[Test]
public void should_create_anon_id()
{
HashUtil.AnonymousToken().Should().NotBeNullOrEmpty();
}
[Test]
public void should_create_the_same_id()
{
HashUtil.AnonymousToken().Should().Be(HashUtil.AnonymousToken());
}
}
}

View File

@@ -84,6 +84,7 @@
<Compile Include="ExtensionTests\IEnumerableExtensionTests\ExceptByFixture.cs" />
<Compile Include="ExtensionTests\IEnumerableExtensionTests\IntersectByFixture.cs" />
<Compile Include="ExtensionTests\Int64ExtensionFixture.cs" />
<Compile Include="HashUtilFixture.cs" />
<Compile Include="Http\HttpClientFixture.cs" />
<Compile Include="Http\HttpHeaderFixture.cs" />
<Compile Include="Http\HttpRequestBuilderFixture.cs" />

View File

@@ -64,11 +64,15 @@ namespace NzbDrone.Common.Disk
foreach (var subDir in _diskProvider.GetDirectoryInfos(sourcePath))
{
if (ShouldIgnore(subDir)) continue;
result &= TransferFolder(subDir.FullName, Path.Combine(targetPath, subDir.Name), mode, verificationMode);
}
foreach (var sourceFile in _diskProvider.GetFileInfos(sourcePath))
{
if (ShouldIgnore(sourceFile)) continue;
var destFile = Path.Combine(targetPath, sourceFile.Name);
result &= TransferFile(sourceFile.FullName, destFile, mode, true, verificationMode);
@@ -101,11 +105,15 @@ namespace NzbDrone.Common.Disk
foreach (var subDir in targetFolders.Where(v => !sourceFolders.Any(d => d.Name == v.Name)))
{
if (ShouldIgnore(subDir)) continue;
_diskProvider.DeleteFolder(subDir.FullName, true);
}
foreach (var subDir in sourceFolders)
{
if (ShouldIgnore(subDir)) continue;
filesCopied += MirrorFolder(subDir.FullName, Path.Combine(targetPath, subDir.Name));
}
@@ -114,11 +122,15 @@ namespace NzbDrone.Common.Disk
foreach (var targetFile in targetFiles.Where(v => !sourceFiles.Any(d => d.Name == v.Name)))
{
if (ShouldIgnore(targetFile)) continue;
_diskProvider.DeleteFile(targetFile.FullName);
}
foreach (var sourceFile in sourceFiles)
{
if (ShouldIgnore(sourceFile)) continue;
var targetFile = Path.Combine(targetPath, sourceFile.Name);
if (CompareFiles(sourceFile.FullName, targetFile))
@@ -564,5 +576,27 @@ namespace NzbDrone.Common.Disk
throw;
}
}
private bool ShouldIgnore(DirectoryInfo folder)
{
if (folder.Name.StartsWith(".nfs"))
{
_logger.Trace("Ignoring folder {0}", folder.FullName);
return true;
}
return false;
}
private bool ShouldIgnore(FileInfo file)
{
if (file.Name.StartsWith(".nfs"))
{
_logger.Trace("Ignoring file {0}", file.FullName);
return true;
}
return false;
}
}
}

View File

@@ -32,6 +32,19 @@ namespace NzbDrone.Common.EnvironmentInfo
public static bool IsMono => Platform == PlatformType.Mono;
public static bool IsDotNet => Platform == PlatformType.DotNet;
public static string PlatformName
{
get
{
if (IsDotNet)
{
return ".NET";
}
return "Mono";
}
}
public abstract Version Version { get; }
}
}

View File

@@ -14,8 +14,8 @@ namespace NzbDrone.Common.Extensions
public static Dictionary<T1, T2> Merge<T1, T2>(this Dictionary<T1, T2> first, Dictionary<T1, T2> second)
{
if (first == null) throw new ArgumentNullException("first");
if (second == null) throw new ArgumentNullException("second");
if (first == null) throw new ArgumentNullException(nameof(first));
if (second == null) throw new ArgumentNullException(nameof(second));
var merged = new Dictionary<T1, T2>();
first.ToList().ForEach(kv => merged[kv.Key] = kv.Value);

View File

@@ -24,7 +24,13 @@ namespace NzbDrone.Common
}
}
}
return string.Format("{0:x8}", mCrc);
return $"{mCrc:x8}";
}
public static string AnonymousToken()
{
var seed = $"{Environment.ProcessorCount}_{Environment.OSVersion.Platform}_{Environment.MachineName}_{Environment.UserName}";
return HashUtil.CalculateCrc(seed);
}
}
}

View File

@@ -1,4 +1,3 @@
using System;
using SharpRaven.Data;
namespace NzbDrone.Common.Instrumentation.Sentry
@@ -7,7 +6,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
public SentryUser Create()
{
return new SentryUser(Environment.MachineName);
return new SentryUser(HashUtil.AnonymousToken());
}
}
}

View File

@@ -37,13 +37,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
Compression = true,
Environment = RuntimeInfo.IsProduction ? "production" : "development",
Release = BuildInfo.Release
Release = BuildInfo.Release,
ErrorOnCapture = OnError
};
_client.ErrorOnCapture = OnError;
_client.Tags.Add("osfamily", OsInfo.Os.ToString());
_client.Tags.Add("runtime", PlatformInfo.Platform.ToString().ToLower());
_client.Tags.Add("runtime", PlatformInfo.PlatformName);
_client.Tags.Add("culture", Thread.CurrentThread.CurrentCulture.Name);
_client.Tags.Add("branch", BuildInfo.Branch);
_client.Tags.Add("version", BuildInfo.Version.ToString());
@@ -135,10 +135,14 @@ namespace NzbDrone.Common.Instrumentation.Sentry
sentryEvent.Fingerprint.Add(logEvent.Exception.GetType().FullName);
}
var osName = Environment.GetEnvironmentVariable("OS_NAME");
var osVersion = Environment.GetEnvironmentVariable("OS_VERSION");
var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION");
sentryEvent.Tags.Add("os_name", Environment.GetEnvironmentVariable("OS_NAME"));
sentryEvent.Tags.Add("os_version", Environment.GetEnvironmentVariable("OS_VERSION"));
sentryEvent.Tags.Add("runtime_version", Environment.GetEnvironmentVariable("RUNTIME_VERSION"));
sentryEvent.Tags.Add("os_name", osName);
sentryEvent.Tags.Add("os_version", $"{osName} {osVersion}");
sentryEvent.Tags.Add("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}");
_client.Capture(sentryEvent);
}

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Common.TPL
/// <param name="maxDegreeOfParallelism">The maximum degree of parallelism provided by this scheduler.</param>
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
{
if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism");
if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
_maxDegreeOfParallelism = maxDegreeOfParallelism;
}

View File

@@ -0,0 +1,62 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class update_btn_url_migration_fixture : MigrationTest<update_btn_url>
{
[TestCase("http://api.btnapps.net")]
[TestCase("https://api.btnapps.net")]
[TestCase("http://api.btnapps.net/")]
[TestCase("https://api.btnapps.net/")]
public void should_replace_old_url(string oldUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "btn_old_url",
Implementation = "BroadcastheNet",
Settings = new BroadcastheNetSettings106
{
BaseUrl = oldUrl
}.ToJson(),
ConfigContract = "BroadcastheNetSettings"
});
});
var items = db.Query<IndexerDefinition90>("SELECT * FROM Indexers");
items.Should().HaveCount(1);
items.First().Settings.ToObject<BroadcastheNetSettings106>().BaseUrl.Should().Contain("api.broadcasthe.net");
}
[Test]
public void should_not_replace_other_indexers()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "not_btn",
Implementation = "NotBroadcastheNet",
Settings = new BroadcastheNetSettings106
{
BaseUrl = "http://api.btnapps.net",
}.ToJson(),
ConfigContract = "BroadcastheNetSettings"
});
});
var items = db.Query<IndexerDefinition90>("SELECT * FROM Indexers");
items.Should().HaveCount(1);
items.First().Settings.ToObject<BroadcastheNetSettings106>().BaseUrl.Should().Be("http://api.btnapps.net");
}
}
}

View File

@@ -217,6 +217,37 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
qualifiedReports.First().RemoteEpisode.ParsedEpisodeInfo.FullSeason.Should().BeTrue();
}
[Test]
public void should_prefer_multiepisode_over_single_episode_for_anime()
{
var remoteEpisode1 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1), GivenEpisode(2) }, new QualityModel(Quality.HDTV720p));
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p));
remoteEpisode1.Series.SeriesType = SeriesTypes.Anime;
remoteEpisode2.Series.SeriesType = SeriesTypes.Anime;
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteEpisode1));
decisions.Add(new DownloadDecision(remoteEpisode2));
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteEpisode.Episodes.Count.Should().Be(remoteEpisode1.Episodes.Count);
}
[Test]
public void should_prefer_single_episode_over_multi_episode_for_non_anime()
{
var remoteEpisode1 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1), GivenEpisode(2) }, new QualityModel(Quality.HDTV720p));
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p));
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteEpisode1));
decisions.Add(new DownloadDecision(remoteEpisode2));
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteEpisode.Episodes.Count.Should().Be(remoteEpisode2.Episodes.Count);
}
[Test]
public void should_prefer_releases_with_more_seeders()
{

View File

@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
};
}
[Test, TestCaseSource("AllowedTestCases")]
[Test, TestCaseSource(nameof(AllowedTestCases))]
public void should_allow_if_quality_is_defined_in_profile(Quality qualityType)
{
remoteEpisode.ParsedEpisodeInfo.Quality.Quality = qualityType;
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test, TestCaseSource("DeniedTestCases")]
[Test, TestCaseSource(nameof(DeniedTestCases))]
public void should_not_allow_if_quality_is_not_defined_in_profile(Quality qualityType)
{
remoteEpisode.ParsedEpisodeInfo.Quality.Quality = qualityType;

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Returns(autoDownloadPropers);
}
[Test, TestCaseSource("IsUpgradeTestCases")]
[Test, TestCaseSource(nameof(IsUpgradeTestCases))]
public void IsUpgradeTest(Quality current, int currentVersion, Quality newQuality, int newVersion, Quality cutoff, bool expected)
{
GivenAutoDownloadPropers(true);

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DiskSpace;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.DiskSpace
{
[TestFixture]
public class DiskSpaceServiceFixture : CoreTest<DiskSpaceService>
{
private string _seriesFolder;
private string _seriesFolder2;
private string _droneFactoryFolder;
[SetUp]
public void SetUp()
{
_seriesFolder = @"G:\fasdlfsdf\series".AsOsAgnostic();
_seriesFolder2 = @"G:\fasdlfsdf\series2".AsOsAgnostic();
_droneFactoryFolder = @"G:\dronefactory".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetMounts())
.Returns(new List<IMount>());
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetPathRoot(It.IsAny<string>()))
.Returns(@"G:\".AsOsAgnostic());
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetAvailableSpace(It.IsAny<string>()))
.Returns(0);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetTotalSize(It.IsAny<string>()))
.Returns(0);
GivenSeries();
}
private void GivenSeries(params Series[] series)
{
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetAllSeries())
.Returns(series.ToList());
}
private void GivenExistingFolder(string folder)
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FolderExists(folder))
.Returns(true);
}
[Test]
public void should_check_diskspace_for_series_folders()
{
GivenSeries(new Series { Path = _seriesFolder });
GivenExistingFolder(_seriesFolder);
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().NotBeEmpty();
}
[Test]
public void should_check_diskspace_for_same_root_folder_only_once()
{
GivenSeries(new Series { Path = _seriesFolder }, new Series { Path = _seriesFolder2 });
GivenExistingFolder(_seriesFolder);
GivenExistingFolder(_seriesFolder2);
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().HaveCount(1);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Once());
}
[Test]
public void should_not_check_diskspace_for_missing_series_folders()
{
GivenSeries(new Series { Path = _seriesFolder });
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().BeEmpty();
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Never());
}
[Test]
public void should_check_diskspace_for_dronefactory_folder()
{
Mocker.GetMock<IConfigService>()
.SetupGet(v => v.DownloadedEpisodesFolder)
.Returns(_droneFactoryFolder);
GivenExistingFolder(_droneFactoryFolder);
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().NotBeEmpty();
}
[Test]
public void should_not_check_diskspace_for_missing_dronefactory_folder()
{
Mocker.GetMock<IConfigService>()
.SetupGet(v => v.DownloadedEpisodesFolder)
.Returns(_droneFactoryFolder);
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().BeEmpty();
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Never());
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.DownloadStation;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
{
[TestFixture]
public class SerialNumberProviderFixture : CoreTest<SerialNumberProvider>
{
protected DownloadStationSettings _settings;
[SetUp]
protected void Setup()
{
_settings = new DownloadStationSettings();
}
private void GivenValidResponse()
{
Mocker.GetMock<IDSMInfoProxy>()
.Setup(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
.Returns("serial");
}
private void GivenInvalidResponse()
{
Mocker.GetMock<IDSMInfoProxy>()
.Setup(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
.Throws(new DownloadClientException("Serial response invalid"));
}
[Test]
public void should_return_hashedserialnumber()
{
GivenValidResponse();
var serial = Subject.GetSerialNumber(_settings);
// This hash should remain the same for 'serial', so don't update the test if you change HashConverter, fix the code instead.
serial.Should().Be("50DE66B735D30738618568294742FCF1DFA52A47");
Mocker.GetMock<IDSMInfoProxy>()
.Verify(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void should_cache_serialnumber()
{
GivenValidResponse();
var serial1 = Subject.GetSerialNumber(_settings);
var serial2 = Subject.GetSerialNumber(_settings);
serial2.Should().Be(serial1);
Mocker.GetMock<IDSMInfoProxy>()
.Verify(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void should_throw_if_serial_number_unavailable()
{
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetSerialNumber(_settings));
ExceptionVerification.ExpectedWarns(1);
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.DownloadStation;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
{
[TestFixture]
public class SharedFolderResolverFixture : CoreTest<SharedFolderResolver>
{
protected string _serialNumber = "SERIALNUMBER";
protected OsPath _sharedFolder;
protected OsPath _physicalPath;
protected DownloadStationSettings _settings;
[SetUp]
protected void Setup()
{
_sharedFolder = new OsPath("/myFolder");
_physicalPath = new OsPath("/mnt/sda1/folder");
_settings = new DownloadStationSettings();
Mocker.GetMock<IFileStationProxy>()
.Setup(f => f.GetSharedFolderMapping(It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
.Throws(new DownloadClientException("There is no shared folder"));
Mocker.GetMock<IFileStationProxy>()
.Setup(f => f.GetSharedFolderMapping(_sharedFolder.FullPath, It.IsAny<DownloadStationSettings>()))
.Returns(new SharedFolderMapping(_sharedFolder.FullPath, _physicalPath.FullPath));
}
[Test]
public void should_throw_when_cannot_resolve_shared_folder()
{
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.RemapToFullPath(new OsPath("/unknownFolder"), _settings, _serialNumber));
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_valid_sharedfolder()
{
var mapping = Subject.RemapToFullPath(_sharedFolder, _settings, "abc");
mapping.Should().Be(_physicalPath);
Mocker.GetMock<IFileStationProxy>()
.Verify(f => f.GetSharedFolderMapping(It.IsAny<string>(), It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void should_cache_mapping()
{
Subject.RemapToFullPath(_sharedFolder, _settings, "abc");
Subject.RemapToFullPath(_sharedFolder, _settings, "abc");
Mocker.GetMock<IFileStationProxy>()
.Verify(f => f.GetSharedFolderMapping(It.IsAny<string>(), It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void should_remap_subfolder()
{
var mapping = Subject.RemapToFullPath(_sharedFolder + "sub", _settings, "abc");
mapping.Should().Be(_physicalPath + "sub");
}
}
}

View File

@@ -0,0 +1,600 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.DownloadStation;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
{
[TestFixture]
public class TorrentDownloadStationFixture : DownloadClientFixtureBase<TorrentDownloadStation>
{
protected DownloadStationSettings _settings;
protected DownloadStationTask _queued;
protected DownloadStationTask _downloading;
protected DownloadStationTask _failed;
protected DownloadStationTask _completed;
protected DownloadStationTask _seeding;
protected DownloadStationTask _magnet;
protected DownloadStationTask _singleFile;
protected DownloadStationTask _multipleFiles;
protected DownloadStationTask _singleFileCompleted;
protected DownloadStationTask _multipleFilesCompleted;
protected string _serialNumber = "SERIALNUMBER";
protected string _category = "sonarr";
protected string _tvDirectory = @"video/Series";
protected string _defaultDestination = "somepath";
protected OsPath _physicalPath = new OsPath("/mnt/sdb1/mydata");
protected Dictionary<string, object> _downloadStationConfigItems;
protected string DownloadURL => "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download";
[SetUp]
public void Setup()
{
_settings = new DownloadStationSettings()
{
Host = "127.0.0.1",
Port = 5000,
Username = "admin",
Password = "pass"
};
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = _settings;
_queued = new DownloadStationTask()
{
Id = "id1",
Size = 1000,
Status = DownloadStationTaskStatus.Waiting,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "0"},
{ "speed_download", "0" }
}
}
};
_completed = new DownloadStationTask()
{
Id = "id2",
Size = 1000,
Status = DownloadStationTaskStatus.Finished,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
},
}
};
_seeding = new DownloadStationTask()
{
Id = "id2",
Size = 1000,
Status = DownloadStationTaskStatus.Seeding,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
}
}
};
_downloading = new DownloadStationTask()
{
Id = "id3",
Size = 1000,
Status = DownloadStationTaskStatus.Downloading,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "100"},
{ "speed_download", "50" }
}
}
};
_failed = new DownloadStationTask()
{
Id = "id4",
Size = 1000,
Status = DownloadStationTaskStatus.Error,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "10"},
{ "speed_download", "0" }
}
}
};
_singleFile = new DownloadStationTask()
{
Id = "id5",
Size = 1000,
Status = DownloadStationTaskStatus.Seeding,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "a.mkv",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
}
}
};
_multipleFiles = new DownloadStationTask()
{
Id = "id6",
Size = 1000,
Status = DownloadStationTaskStatus.Seeding,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
}
}
};
_singleFileCompleted = new DownloadStationTask()
{
Id = "id6",
Size = 1000,
Status = DownloadStationTaskStatus.Finished,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "a.mkv",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
}
}
};
_multipleFilesCompleted = new DownloadStationTask()
{
Id = "id6",
Size = 1000,
Status = DownloadStationTaskStatus.Finished,
Type = DownloadStationTaskType.BT,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", DownloadURL }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
}
}
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0]));
_downloadStationConfigItems = new Dictionary<string, object>
{
{ "default_destination", _defaultDestination },
};
Mocker.GetMock<IDownloadStationProxy>()
.Setup(v => v.GetConfig(It.IsAny<DownloadStationSettings>()))
.Returns(_downloadStationConfigItems);
}
protected void GivenSharedFolder()
{
Mocker.GetMock<ISharedFolderResolver>()
.Setup(s => s.RemapToFullPath(It.IsAny<OsPath>(), It.IsAny<DownloadStationSettings>(), It.IsAny<string>()))
.Returns<OsPath, DownloadStationSettings, string>((path, setttings, serial) => _physicalPath);
}
protected void GivenSerialNumber()
{
Mocker.GetMock<ISerialNumberProvider>()
.Setup(s => s.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
.Returns(_serialNumber);
}
protected void GivenTvCategory()
{
_settings.TvCategory = _category;
}
protected void GivenTvDirectory()
{
_settings.TvDirectory = _tvDirectory;
}
protected virtual void GivenTorrents(List<DownloadStationTask> torrents)
{
if (torrents == null)
{
torrents = new List<DownloadStationTask>();
}
Mocker.GetMock<IDownloadStationProxy>()
.Setup(s => s.GetTasks(DownloadStationTaskType.BT, It.IsAny<DownloadStationSettings>()))
.Returns(torrents);
}
protected void PrepareClientToReturnQueuedItem()
{
GivenTorrents(new List<DownloadStationTask>
{
_queued
});
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
Mocker.GetMock<IDownloadStationProxy>()
.Setup(s => s.AddTaskFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
.Callback(PrepareClientToReturnQueuedItem);
Mocker.GetMock<IDownloadStationProxy>()
.Setup(s => s.AddTaskFromData(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
.Callback(PrepareClientToReturnQueuedItem);
}
protected override RemoteEpisode CreateRemoteEpisode()
{
var episode = base.CreateRemoteEpisode();
episode.Release.DownloadUrl = DownloadURL;
return episode;
}
protected int GivenAllKindOfTasks()
{
var tasks = new List<DownloadStationTask>() { _queued, _completed, _failed, _downloading, _seeding };
Mocker.GetMock<IDownloadStationProxy>()
.Setup(d => d.GetTasks(DownloadStationTaskType.BT, _settings))
.Returns(tasks);
return tasks.Count;
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
{
GivenSerialNumber();
GivenTvDirectory();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), _tvDirectory, It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void Download_with_category_should_force_directory()
{
GivenSerialNumber();
GivenTvCategory();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), $"{_defaultDestination}/{_category}", It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSerialNumber();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void GetItems_should_ignore_downloads_in_wrong_folder()
{
_settings.TvDirectory = @"/shared/folder/sub";
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask> { _completed });
Subject.GetItems().Should().BeEmpty();
}
[Test]
public void GetItems_should_throw_if_shared_folder_resolve_fails()
{
Mocker.GetMock<ISharedFolderResolver>()
.Setup(s => s.RemapToFullPath(It.IsAny<OsPath>(), It.IsAny<DownloadStationSettings>(), It.IsAny<string>()))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
GivenSerialNumber();
GivenAllKindOfTasks();
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetItems());
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void GetItems_should_throw_if_serial_number_unavailable()
{
Mocker.GetMock<ISerialNumberProvider>()
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
GivenSharedFolder();
GivenAllKindOfTasks();
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetItems());
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void Download_should_throw_and_not_add_task_if_cannot_get_serial_number()
{
var remoteEpisode = CreateRemoteEpisode();
Mocker.GetMock<ISerialNumberProvider>()
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteEpisode));
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, _settings), Times.Never());
}
[Test]
public void GetItems_should_set_outputPath_to_base_folder_when_single_file_non_finished_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask>() { _singleFile });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().OutputPath.Should().Be(_physicalPath + _singleFile.Title);
}
[Test]
public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_non_finished_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask>() { _multipleFiles });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().OutputPath.Should().Be(_physicalPath + _multipleFiles.Title);
}
[Test]
public void GetItems_should_set_outputPath_to_base_folder_when_single_file_finished_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask>() { _singleFileCompleted });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().OutputPath.Should().Be(_physicalPath + _singleFileCompleted.Title);
}
[Test]
public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_finished_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask>() { _multipleFilesCompleted });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().OutputPath.Should().Be($"{_physicalPath}/{_multipleFiles.Title}");
}
[Test]
public void GetItems_should_not_map_outputpath_for_queued_or_downloading_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask>
{
_queued, _downloading
});
var items = Subject.GetItems();
items.Should().HaveCount(2);
items.Should().OnlyContain(v => v.OutputPath.IsEmpty);
}
[Test]
public void GetItems_should_map_outputpath_for_completed_or_failed_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenTorrents(new List<DownloadStationTask>
{
_completed, _failed, _seeding
});
var items = Subject.GetItems();
items.Should().HaveCount(3);
items.Should().OnlyContain(v => !v.OutputPath.IsEmpty);
}
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)]
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)]
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed, true)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)]
public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected)
{
GivenSerialNumber();
GivenSharedFolder();
_queued.Status = apiStatus;
GivenTorrents(new List<DownloadStationTask>() { _queued });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().IsReadOnly.Should().Be(readOnlyExpected);
}
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Error, DownloadItemStatus.Failed)]
[TestCase(DownloadStationTaskStatus.Extracting, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
GivenSerialNumber();
GivenSharedFolder();
_queued.Status = apiStatus;
GivenTorrents(new List<DownloadStationTask>() { _queued });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().Status.Should().Be(expectedItemStatus);
}
}
}

View File

@@ -0,0 +1,442 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.DownloadStation;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
using NzbDrone.Core.Organizer;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
{
[TestFixture]
public class UsenetDownloadStationFixture : DownloadClientFixtureBase<UsenetDownloadStation>
{
protected DownloadStationSettings _settings;
protected DownloadStationTask _queued;
protected DownloadStationTask _downloading;
protected DownloadStationTask _failed;
protected DownloadStationTask _completed;
protected DownloadStationTask _seeding;
protected string _serialNumber = "SERIALNUMBER";
protected string _category = "sonarr";
protected string _tvDirectory = @"video/Series";
protected string _defaultDestination = "somepath";
protected OsPath _physicalPath = new OsPath("/mnt/sdb1/mydata");
protected RemoteEpisode _remoteEpisode;
protected Dictionary<string, object> _downloadStationConfigItems;
[SetUp]
public void Setup()
{
_remoteEpisode = CreateRemoteEpisode();
_settings = new DownloadStationSettings()
{
Host = "127.0.0.1",
Port = 5000,
Username = "admin",
Password = "pass"
};
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = _settings;
_queued = new DownloadStationTask()
{
Id = "id1",
Size = 1000,
Status = DownloadStationTaskStatus.Waiting,
Type = DownloadStationTaskType.NZB,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "0"},
{ "speed_download", "0" }
}
}
};
_completed = new DownloadStationTask()
{
Id = "id2",
Size = 1000,
Status = DownloadStationTaskStatus.Finished,
Type = DownloadStationTaskType.NZB,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
},
}
};
_seeding = new DownloadStationTask()
{
Id = "id2",
Size = 1000,
Status = DownloadStationTaskStatus.Seeding,
Type = DownloadStationTaskType.NZB,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "speed_download", "0" }
}
}
};
_downloading = new DownloadStationTask()
{
Id = "id3",
Size = 1000,
Status = DownloadStationTaskStatus.Downloading,
Type = DownloadStationTaskType.NZB,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "100"},
{ "speed_download", "50" }
}
}
};
_failed = new DownloadStationTask()
{
Id = "id4",
Size = 1000,
Status = DownloadStationTaskStatus.Error,
Type = DownloadStationTaskType.NZB,
Username = "admin",
Title = "title",
Additional = new DownloadStationTaskAdditional
{
Detail = new Dictionary<string, string>
{
{ "destination","shared/folder" },
{ "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" }
},
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "10"},
{ "speed_download", "0" }
}
}
};
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0]));
_downloadStationConfigItems = new Dictionary<string, object>
{
{ "default_destination", _defaultDestination },
};
Mocker.GetMock<IDownloadStationProxy>()
.Setup(v => v.GetConfig(It.IsAny<DownloadStationSettings>()))
.Returns(_downloadStationConfigItems);
}
protected void GivenSharedFolder()
{
Mocker.GetMock<ISharedFolderResolver>()
.Setup(s => s.RemapToFullPath(It.IsAny<OsPath>(), It.IsAny<DownloadStationSettings>(), It.IsAny<string>()))
.Returns<OsPath, DownloadStationSettings, string>((path, setttings, serial) => _physicalPath);
}
protected void GivenSerialNumber()
{
Mocker.GetMock<ISerialNumberProvider>()
.Setup(s => s.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
.Returns(_serialNumber);
}
protected void GivenTvCategory()
{
_settings.TvCategory = _category;
}
protected void GivenTvDirectory()
{
_settings.TvDirectory = _tvDirectory;
}
protected virtual void GivenNZBs(List<DownloadStationTask> nzbs)
{
if (nzbs == null)
{
nzbs = new List<DownloadStationTask>();
}
Mocker.GetMock<IDownloadStationProxy>()
.Setup(s => s.GetTasks(DownloadStationTaskType.NZB, It.IsAny<DownloadStationSettings>()))
.Returns(nzbs);
}
protected void PrepareClientToReturnQueuedItem()
{
GivenNZBs(new List<DownloadStationTask>
{
_queued
});
}
protected void GivenSuccessfulDownload()
{/*
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
*/
Mocker.GetMock<IDownloadStationProxy>()
.Setup(s => s.AddTaskFromData(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
.Callback(PrepareClientToReturnQueuedItem);
}
protected void GivenAllKindOfTasks()
{
var tasks = new List<DownloadStationTask>() { _queued, _completed, _failed, _downloading, _seeding };
Mocker.GetMock<IDownloadStationProxy>()
.Setup(d => d.GetTasks(DownloadStationTaskType.NZB, _settings))
.Returns(tasks);
}
[Test]
public void Download_with_TvDirectory_should_force_directory()
{
GivenSerialNumber();
GivenTvDirectory();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromData(It.IsAny<byte[]>(), It.IsAny<string>(), _tvDirectory, It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void Download_with_category_should_force_directory()
{
GivenSerialNumber();
GivenTvCategory();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromData(It.IsAny<byte[]>(), It.IsAny<string>(), $"{_defaultDestination}/{_category}", It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void Download_without_TvDirectory_and_Category_should_use_default()
{
GivenSerialNumber();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromData(It.IsAny<byte[]>(), It.IsAny<string>(), null, It.IsAny<DownloadStationSettings>()), Times.Once());
}
[Test]
public void GetItems_should_return_empty_list_if_no_tasks_available()
{
_settings.TvDirectory = @"/shared/folder/sub";
GivenSerialNumber();
GivenSharedFolder();
GivenNZBs(new List<DownloadStationTask>());
Subject.GetItems().Should().BeEmpty();
}
[Test]
public void GetItems_should_ignore_downloads_in_wrong_folder()
{
_settings.TvDirectory = @"/shared/folder/sub";
GivenSerialNumber();
GivenSharedFolder();
GivenNZBs(new List<DownloadStationTask> { _completed });
Subject.GetItems().Should().BeEmpty();
}
[Test]
public void GetItems_should_throw_if_shared_folder_resolve_fails()
{
Mocker.GetMock<ISharedFolderResolver>()
.Setup(s => s.RemapToFullPath(It.IsAny<OsPath>(), It.IsAny<DownloadStationSettings>(), It.IsAny<string>()))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
GivenSerialNumber();
GivenAllKindOfTasks();
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetItems());
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void GetItems_should_throw_if_serial_number_unavailable()
{
Mocker.GetMock<ISerialNumberProvider>()
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
GivenSharedFolder();
GivenAllKindOfTasks();
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetItems());
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void Download_should_throw_and_not_add_tasks_if_cannot_get_serial_number()
{
var remoteEpisode = CreateRemoteEpisode();
Mocker.GetMock<ISerialNumberProvider>()
.Setup(s => s.GetSerialNumber(_settings))
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteEpisode));
Mocker.GetMock<IDownloadStationProxy>()
.Verify(v => v.AddTaskFromUrl(It.IsAny<string>(), null, _settings), Times.Never());
}
[Test]
public void GetItems_should_not_map_outputpath_for_queued_or_downloading_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenNZBs(new List<DownloadStationTask>
{
_queued, _downloading
});
var items = Subject.GetItems();
items.Should().HaveCount(2);
items.Should().OnlyContain(v => v.OutputPath.IsEmpty);
}
[Test]
public void GetItems_should_map_outputpath_for_completed_or_failed_tasks()
{
GivenSerialNumber();
GivenSharedFolder();
GivenNZBs(new List<DownloadStationTask>
{
_completed, _failed, _seeding
});
var items = Subject.GetItems();
items.Should().HaveCount(3);
items.Should().OnlyContain(v => !v.OutputPath.IsEmpty);
}
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)]
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)]
public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected)
{
GivenSerialNumber();
GivenSharedFolder();
_queued.Status = apiStatus;
GivenNZBs(new List<DownloadStationTask>() { _queued });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().IsReadOnly.Should().Be(readOnlyExpected);
}
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Error, DownloadItemStatus.Failed)]
[TestCase(DownloadStationTaskStatus.Extracting, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
GivenSerialNumber();
GivenSharedFolder();
_queued.Status = apiStatus;
GivenNZBs(new List<DownloadStationTask>() { _queued });
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().Status.Should().Be(expectedItemStatus);
}
}
}

View File

@@ -92,14 +92,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
protected void GivenFailedDownload()
{
Mocker.GetMock<INzbgetProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbgetSettings>()))
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<NzbgetSettings>()))
.Returns((string)null);
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<INzbgetProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbgetSettings>()))
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<NzbgetSettings>()))
.Returns(Guid.NewGuid().ToString().Replace("-", ""));
}

View File

@@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:torrent="http://xmlns.ezrss.it/0.1/">
<channel>
<title>tv torrents RSS feed - KickassTorrents</title>
<link>http://kickass.to/</link>
<description>tv torrents RSS feed</description>
<item>
<title>Doctor Stranger.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html</link>
<guid>http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html</guid>
<pubDate>Mon, 12 May 2014 16:16:49 +0000</pubDate>
<torrent:contentLength>1205364736</torrent:contentLength>
<torrent:infoHash>208C4F7866612CC88BFEBC7C496FA72C2368D1C0</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:208C4F7866612CC88BFEBC7C496FA72C2368D1C0&dn=doctor+stranger+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>206</torrent:seeds>
<torrent:peers>311</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>doctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/208C4F7866612CC88BFEBC7C496FA72C2368D1C0.torrent?title=%5Bkickass.to%5Ddoctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg" length="1205364736" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.XViD-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-xvid-ipop-avi-ctrg-t9100647.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-xvid-ipop-avi-ctrg-t9100647.html</guid>
<pubDate>Mon, 12 May 2014 16:16:31 +0000</pubDate>
<torrent:contentLength>677543936</torrent:contentLength>
<torrent:infoHash>BF22A53C9889A7D325F2A3D904E566B7DF4074EB</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:BF22A53C9889A7D325F2A3D904E566B7DF4074EB&dn=triangle+e03+140512+hdtv+xvid+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>242</torrent:seeds>
<torrent:peers>374</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.xvid.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/BF22A53C9889A7D325F2A3D904E566B7DF4074EB.torrent?title=%5Bkickass.to%5Dtriangle.e03.140512.hdtv.xvid.ipop.avi.ctrg" length="677543936" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100646.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100646.html</guid>
<pubDate>Mon, 12 May 2014 16:16:10 +0000</pubDate>
<torrent:contentLength>1196869632</torrent:contentLength>
<torrent:infoHash>8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC&dn=triangle+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>177</torrent:seeds>
<torrent:peers>268</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.h264.720p.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC.torrent?title=%5Bkickass.to%5Dtriangle.e03.140512.hdtv.h264.720p.ipop.avi.ctrg" length="1196869632" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.X264.720p-BarosG_.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-x264-720p-barosg-avi-ctrg-t9100644.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-x264-720p-barosg-avi-ctrg-t9100644.html</guid>
<pubDate>Mon, 12 May 2014 16:15:52 +0000</pubDate>
<torrent:contentLength>1418906266</torrent:contentLength>
<torrent:infoHash>5556B773893DB55287ECEC581E850B853163DB11</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:5556B773893DB55287ECEC581E850B853163DB11&dn=triangle+e03+140512+hdtv+x264+720p+barosg+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>522</torrent:seeds>
<torrent:peers>785</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.x264.720p.barosg.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/5556B773893DB55287ECEC581E850B853163DB11.torrent?title=%5Bkickass.to%5Dtriangle.e03.140512.hdtv.x264.720p.barosg.avi.ctrg" length="1418906266" type="application/x-bittorrent" />
</item>
<item>
<title>Battlestar Galactica 1978 Dvd3 e09 e10 e11 e12 [NL] [FR] [ENG] Sub</title>
<description>
<![CDATA[In een afgelegen zonnestelsel leeft een mensenras op twaalf koloniewerelden. Ze zijn al eeuwen in oorlog met de Cylons, gevechtsrobots die ooit werden gemaakt door een allang verdwenen buitenaards reptielachtig ras. Met de hulp van de menselijke verrader Baltar zijn de Cylons erin geslaagd de mensheid vrijwel uit te roeien. Slechts een oorlogsschip kan aan de vernietiging ontkomen: de Battlestar Galactica van commandant Adama.
Met een vloot burgerschepen vol vluchtelingen vlucht de Galactica voor de Cylons. Adama besluit op zoek te gaan naar de legendarische 13e en laatste kolonie, genaamd Aarde. Tijdens de lange en gevaarlijke reis worden ze voortdurend bedreigd door de achtervolgende Cylons en andere gevaren.]]>
</description>
<category>TV</category>
<author>http://kickass.to/user/hendriknl/</author>
<link>http://kickass.to/battlestar-galactica-1978-dvd3-e09-e10-e11-e12-nl-fr-eng-sub-t9100642.html</link>
<guid>http://kickass.to/battlestar-galactica-1978-dvd3-e09-e10-e11-e12-nl-fr-eng-sub-t9100642.html</guid>
<pubDate>Mon, 12 May 2014 16:15:46 +0000</pubDate>
<torrent:contentLength>4680841216</torrent:contentLength>
<torrent:infoHash>3D293CAFEDAC595F6E55F9C284DD76862FE254F6</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:3D293CAFEDAC595F6E55F9C284DD76862FE254F6&dn=battlestar+galactica+1978+dvd3+e09+e10+e11+e12+nl+fr+eng+sub&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>2</torrent:seeds>
<torrent:peers>5</torrent:peers>
<torrent:verified>0</torrent:verified>
<torrent:fileName>battlestar.galactica.1978.dvd3.e09.e10.e11.e12.nl.fr.eng.sub.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/3D293CAFEDAC595F6E55F9C284DD76862FE254F6.torrent?title=%5Bkickass.to%5Dbattlestar.galactica.1978.dvd3.e09.e10.e11.e12.nl.fr.eng.sub" length="4680841216" type="application/x-bittorrent" />
</item>
</channel>
</rss>

View File

@@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:torrent="http://xmlns.ezrss.it/0.1/">
<channel>
<title>tv torrents RSS feed - KickassTorrents</title>
<link>http://kickass.to/</link>
<description>tv torrents RSS feed</description>
<item>
<title>Doctor Strang&eacute;r.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html</link>
<guid>http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html</guid>
<pubDate>Mon, 12 May 2014 16:16:49 +0000</pubDate>
<torrent:contentLength>1205364736</torrent:contentLength>
<torrent:infoHash>208C4F7866612CC88BFEBC7C496FA72C2368D1C0</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:208C4F7866612CC88BFEBC7C496FA72C2368D1C0&dn=doctor+stranger+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>206</torrent:seeds>
<torrent:peers>311</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>doctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/208C4F7866612CC88BFEBC7C496FA72C2368D1C0.torrent?title=%5Bkickass.to%5Ddoctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg" length="1205364736" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.XViD-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-xvid-ipop-avi-ctrg-t9100647.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-xvid-ipop-avi-ctrg-t9100647.html</guid>
<pubDate>Mon, 12 May 2014 16:16:31 +0000</pubDate>
<torrent:contentLength>677543936</torrent:contentLength>
<torrent:infoHash>BF22A53C9889A7D325F2A3D904E566B7DF4074EB</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:BF22A53C9889A7D325F2A3D904E566B7DF4074EB&dn=triangle+e03+140512+hdtv+xvid+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>242</torrent:seeds>
<torrent:peers>374</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.xvid.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/BF22A53C9889A7D325F2A3D904E566B7DF4074EB.torrent?title=%5Bkickass.to%5Dtriangle.e03.140512.hdtv.xvid.ipop.avi.ctrg" length="677543936" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100646.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100646.html</guid>
<pubDate>Mon, 12 May 2014 16:16:10 +0000</pubDate>
<torrent:contentLength>1196869632</torrent:contentLength>
<torrent:infoHash>8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC&dn=triangle+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>177</torrent:seeds>
<torrent:peers>268</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.h264.720p.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC.torrent?title=%5Bkickass.to%5Dtriangle.e03.140512.hdtv.h264.720p.ipop.avi.ctrg" length="1196869632" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.X264.720p-BarosG_.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-x264-720p-barosg-avi-ctrg-t9100644.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-x264-720p-barosg-avi-ctrg-t9100644.html</guid>
<pubDate>Mon, 12 May 2014 16:15:52 +0000</pubDate>
<torrent:contentLength>1418906266</torrent:contentLength>
<torrent:infoHash>5556B773893DB55287ECEC581E850B853163DB11</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:5556B773893DB55287ECEC581E850B853163DB11&dn=triangle+e03+140512+hdtv+x264+720p+barosg+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>522</torrent:seeds>
<torrent:peers>785</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.x264.720p.barosg.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/5556B773893DB55287ECEC581E850B853163DB11.torrent?title=%5Bkickass.to%5Dtriangle.e03.140512.hdtv.x264.720p.barosg.avi.ctrg" length="1418906266" type="application/x-bittorrent" />
</item>
<item>
<title>Battlestar Galactica 1978 Dvd3 e09 e10 e11 e12 [NL] [FR] [ENG] Sub</title>
<description>
<![CDATA[In een afgelegen zonnestelsel leeft een mensenras op twaalf koloniewerelden. Ze zijn al eeuwen in oorlog met de Cylons, gevechtsrobots die ooit werden gemaakt door een allang verdwenen buitenaards reptielachtig ras. Met de hulp van de menselijke verrader Baltar zijn de Cylons erin geslaagd de mensheid vrijwel uit te roeien. Slechts een oorlogsschip kan aan de vernietiging ontkomen: de Battlestar Galactica van commandant Adama.
Met een vloot burgerschepen vol vluchtelingen vlucht de Galactica voor de Cylons. Adama besluit op zoek te gaan naar de legendarische 13e en laatste kolonie, genaamd Aarde. Tijdens de lange en gevaarlijke reis worden ze voortdurend bedreigd door de achtervolgende Cylons en andere gevaren.]]>
</description>
<category>TV</category>
<author>http://kickass.to/user/hendriknl/</author>
<link>http://kickass.to/battlestar-galactica-1978-dvd3-e09-e10-e11-e12-nl-fr-eng-sub-t9100642.html</link>
<guid>http://kickass.to/battlestar-galactica-1978-dvd3-e09-e10-e11-e12-nl-fr-eng-sub-t9100642.html</guid>
<pubDate>Mon, 12 May 2014 16:15:46 +0000</pubDate>
<torrent:contentLength>4680841216</torrent:contentLength>
<torrent:infoHash>3D293CAFEDAC595F6E55F9C284DD76862FE254F6</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:3D293CAFEDAC595F6E55F9C284DD76862FE254F6&dn=battlestar+galactica+1978+dvd3+e09+e10+e11+e12+nl+fr+eng+sub&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>2</torrent:seeds>
<torrent:peers>5</torrent:peers>
<torrent:verified>0</torrent:verified>
<torrent:fileName>battlestar.galactica.1978.dvd3.e09.e10.e11.e12.nl.fr.eng.sub.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/3D293CAFEDAC595F6E55F9C284DD76862FE254F6.torrent?title=%5Bkickass.to%5Dbattlestar.galactica.1978.dvd3.e09.e10.e11.e12.nl.fr.eng.sub" length="4680841216" type="application/x-bittorrent" />
</item>
</channel>
</rss>

View File

@@ -0,0 +1,98 @@
<?xml version='1.0' encoding='UTF-8' ?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>limetorrents.cc - RSS Feed</title>
<link>http://www.limetorrents.cc/</link>
<description>Latest Torrents RSS.</description>
<language>en-us</language>
<pubDate>Thu, 16 Feb 2017 05:48:36 +0200</pubDate>
<lastBuildDate>Thu, 16 Feb 2017 05:48:36 +0200</lastBuildDate>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<generator>limetorrents.cc RSS Generator 1.1</generator>
<item>
<title>The Expanse 2x04 (720p-HDTV-x264-SVA)[VTV]</title>
<guid isPermaLink='true'>http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html</guid>
<pubDate>16 Feb 2017 05:24:26 +0300</pubDate>
<category>TV shows</category>
<link>http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html</link>
<size>880496711</size>
<description>
<![CDATA[
Category: <a href="http://www.limetorrents.cc/browse-torrents/TV-shows/">TV shows</a><br />
Seeds: 0<br />Leechers: 0<br />Size: 839.71 MB<br /><br /><a href="http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html">More @ limetorrents.cc</a><br /> ]]>
</description>
<comments>http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html</comments>
<category domain="http://www.limetorrents.cc/browse-torrents/TV shows">TV shows</category>
<enclosure url="http://itorrents.org/torrent/51C578C9823DD58F6EEA287C368ED935843D63AB.torrent?title=The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]" length="880496711" type="application/x-bittorrent" />
</item>
<item>
<title>Criminal Minds S12E13 720p HDTV x264-FLEET[PRiME]</title>
<guid isPermaLink='true'>http://www.limetorrents.cc/Criminal-Minds-S12E13-720p-HDTV-x264-FLEET[PRiME]-torrent-8643586.html</guid>
<pubDate>16 Feb 2017 05:20:49 +0300</pubDate>
<category>TV shows</category>
<link>http://www.limetorrents.cc/Criminal-Minds-S12E13-720p-HDTV-x264-FLEET[PRiME]-torrent-8643586.html</link>
<size>940818158</size>
<description>
<![CDATA[
Category: <a href="http://www.limetorrents.cc/browse-torrents/TV-shows/">TV shows</a><br />
Seeds: 0<br />Leechers: 0<br />Size: 897.23 MB<br /><br /><a href="http://www.limetorrents.cc/Criminal-Minds-S12E13-720p-HDTV-x264-FLEET[PRiME]-torrent-8643586.html">More @ limetorrents.cc</a><br /> ]]>
</description>
<comments>http://www.limetorrents.cc/Criminal-Minds-S12E13-720p-HDTV-x264-FLEET[PRiME]-torrent-8643586.html</comments>
<category domain="http://www.limetorrents.cc/browse-torrents/TV shows">TV shows</category>
<enclosure url="http://itorrents.org/torrent/C7EBCBE53A82E7C8F0826417F5174C8709DB9DC0.torrent?title=Criminal-Minds-S12E13-720p-HDTV-x264-FLEET[PRiME]" length="940818158" type="application/x-bittorrent" />
</item>
<item>
<title>Legion S01E02 720p HDTV x264-AVS[PRiME]</title>
<guid isPermaLink='true'>http://www.limetorrents.cc/Legion-S01E02-720p-HDTV-x264-AVS[PRiME]-torrent-8643585.html</guid>
<pubDate>16 Feb 2017 05:20:48 +0300</pubDate>
<category>TV shows</category>
<link>http://www.limetorrents.cc/Legion-S01E02-720p-HDTV-x264-AVS[PRiME]-torrent-8643585.html</link>
<size>1320654292</size>
<description>
<![CDATA[
Category: <a href="http://www.limetorrents.cc/browse-torrents/TV-shows/">TV shows</a><br />
Seeds: 0<br />Leechers: 0<br />Size: 1.23 GB<br /><br /><a href="http://www.limetorrents.cc/Legion-S01E02-720p-HDTV-x264-AVS[PRiME]-torrent-8643585.html">More @ limetorrents.cc</a><br /> ]]>
</description>
<comments>http://www.limetorrents.cc/Legion-S01E02-720p-HDTV-x264-AVS[PRiME]-torrent-8643585.html</comments>
<category domain="http://www.limetorrents.cc/browse-torrents/TV shows">TV shows</category>
<enclosure url="http://itorrents.org/torrent/ED2903DB3F4B3D728D2E7091C33B6F502A0FB5D4.torrent?title=Legion-S01E02-720p-HDTV-x264-AVS[PRiME]" length="1320654292" type="application/x-bittorrent" />
</item>
<item>
<title>Suits S06E14 HDTV x264-SVA[PRiME]</title>
<guid isPermaLink='true'>http://www.limetorrents.cc/Suits-S06E14-HDTV-x264-SVA[PRiME]-torrent-8643579.html</guid>
<pubDate>16 Feb 2017 05:11:58 +0300</pubDate>
<category>TV shows</category>
<link>http://www.limetorrents.cc/Suits-S06E14-HDTV-x264-SVA[PRiME]-torrent-8643579.html</link>
<size>212274667</size>
<description>
<![CDATA[
Category: <a href="http://www.limetorrents.cc/browse-torrents/TV-shows/">TV shows</a><br />
Seeds: 0<br />Leechers: 0<br />Size: 202.44 MB<br /><br /><a href="http://www.limetorrents.cc/Suits-S06E14-HDTV-x264-SVA[PRiME]-torrent-8643579.html">More @ limetorrents.cc</a><br /> ]]>
</description>
<comments>http://www.limetorrents.cc/Suits-S06E14-HDTV-x264-SVA[PRiME]-torrent-8643579.html</comments>
<category domain="http://www.limetorrents.cc/browse-torrents/TV shows">TV shows</category>
<enclosure url="http://itorrents.org/torrent/5E412B3200773684AEDBEBF9B053ED58180279DD.torrent?title=Suits-S06E14-HDTV-x264-SVA[PRiME]" length="212274667" type="application/x-bittorrent" />
</item>
<item>
<title>The Expanse S02E04 HDTV x264-SVA[PRiME]</title>
<guid isPermaLink='true'>http://www.limetorrents.cc/The-Expanse-S02E04-HDTV-x264-SVA[PRiME]-torrent-8643578.html</guid>
<pubDate>16 Feb 2017 05:11:57 +0300</pubDate>
<category>TV shows</category>
<link>http://www.limetorrents.cc/The-Expanse-S02E04-HDTV-x264-SVA[PRiME]-torrent-8643578.html</link>
<size>269445781</size>
<description>
<![CDATA[
Category: <a href="http://www.limetorrents.cc/browse-torrents/TV-shows/">TV shows</a><br />
Seeds: 0<br />Leechers: 0<br />Size: 256.96 MB<br /><br /><a href="http://www.limetorrents.cc/The-Expanse-S02E04-HDTV-x264-SVA[PRiME]-torrent-8643578.html">More @ limetorrents.cc</a><br /> ]]>
</description>
<comments>http://www.limetorrents.cc/The-Expanse-S02E04-HDTV-x264-SVA[PRiME]-torrent-8643578.html</comments>
<category domain="http://www.limetorrents.cc/browse-torrents/TV shows">TV shows</category>
<enclosure url="http://itorrents.org/torrent/7E552CD2D99E43C34FBB233E3BAF0C1ECD416C76.torrent?title=The-Expanse-S02E04-HDTV-x264-SVA[PRiME]" length="269445781" type="application/x-bittorrent" />
</item>
</channel>
</rss>

View File

@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Womble's NZB Index</title>
<link>http://indexer.local/</link>
<description>NZB Index</description>
<webMaster>my.email@internet.me</webMaster>
<ttl>15</ttl>
<item>
<title>One.Child.S01E01.720p.HDTV.x264-TLA</title>
<link>http://indexer.local/nzb/bb4/One.Child.S01E01.720p.HDTV.x264-TLA.nzb</link>
<description>
One.Child.S01E01.720p.HDTV.x264-TLA
(Size:956 Mb)
</description>
<category>tv-hd</category>
<pubDate>2/17/2016 11:03:52 PM</pubDate>
<guid isPermaLink="true">http://indexer.local/nzb/bb4/One.Child.S01E01.720p.HDTV.x264-TLA.nzb</guid>
<enclosure url="http://indexer.local/nzb/bb4/One.Child.S01E01.720p.HDTV.x264-TLA.nzb" length="2048" type="text/xml" />
</item>
<item>
<title>Midsomer.Murders.S18E06.Harvest.Of.Souls.HDTV.x264-ORGANiC</title>
<link>http://indexer.local/nzb/17c/Midsomer.Murders.S18E06.Harvest.Of.Souls.HDTV.x264-ORGANiC.nzb</link>
<description>
Midsomer.Murders.S18E06.Harvest.Of.Souls.HDTV.x264-ORGANiC
(Size:730 Mb)
</description>
<category>tv-sd</category>
<pubDate>2/17/2016 10:55:33 PM</pubDate>
<guid isPermaLink="true">http://indexer.local/nzb/17c/Midsomer.Murders.S18E06.Harvest.Of.Souls.HDTV.x264-ORGANiC.nzb</guid>
<enclosure url="http://indexer.local/nzb/17c/Midsomer.Murders.S18E06.Harvest.Of.Souls.HDTV.x264-ORGANiC.nzb" length="2048" type="text/xml" />
</item>
<item>
<title>Forget.and.Forgive.2014.HDTV.x264-SQUEAK</title>
<link>http://indexer.local/nzb/f3f/Forget.and.Forgive.2014.HDTV.x264-SQUEAK.nzb</link>
<description>
Forget.and.Forgive.2014.HDTV.x264-SQUEAK
(Size:719 Mb)
</description>
<category>tv-sd</category>
<pubDate>2/17/2016 10:25:28 PM</pubDate>
<guid isPermaLink="true">http://indexer.local/nzb/f3f/Forget.and.Forgive.2014.HDTV.x264-SQUEAK.nzb</guid>
<enclosure url="http://indexer.local/nzb/f3f/Forget.and.Forgive.2014.HDTV.x264-SQUEAK.nzb" length="2048" type="text/xml" />
</item>
<item>
<title>National.Geographic.The.Lakeshore.Killers.HDTV.x264-SQUEAK</title>
<link>http://indexer.local/nzb/828/National.Geographic.The.Lakeshore.Killers.HDTV.x264-SQUEAK.nzb</link>
<description>
National.Geographic.The.Lakeshore.Killers.HDTV.x264-SQUEAK
(Size:436 Mb)
</description>
<category>tv-sd</category>
<pubDate>2/17/2016 10:20:36 PM</pubDate>
<guid isPermaLink="true">http://indexer.local/nzb/828/National.Geographic.The.Lakeshore.Killers.HDTV.x264-SQUEAK.nzb</guid>
<enclosure url="http://indexer.local/nzb/828/National.Geographic.The.Lakeshore.Killers.HDTV.x264-SQUEAK.nzb" length="2048" type="text/xml" />
</item>
<item>
<title>Desert.Lands.of.The.Middle.East.E01.Turkeys.Harsh.Paradise.HDTV.x264-SQUEAK</title>
<link>http://indexer.local/nzb/210/Desert.Lands.of.The.Middle.East.E01.Turkeys.Harsh.Paradise.HDTV.x264-SQUEAK.nzb</link>
<description>
Desert.Lands.of.The.Middle.East.E01.Turkeys.Harsh.Paradise.HDTV.x264-SQUEAK
(Size:460 Mb)
</description>
<category>tv-sd</category>
<pubDate>2/17/2016 10:18:29 PM</pubDate>
<guid isPermaLink="true">http://indexer.local/nzb/210/Desert.Lands.of.The.Middle.East.E01.Turkeys.Harsh.Paradise.HDTV.x264-SQUEAK.nzb</guid>
<enclosure url="http://indexer.local/nzb/210/Desert.Lands.of.The.Middle.East.E01.Turkeys.Harsh.Paradise.HDTV.x264-SQUEAK.nzb" length="2048" type="text/xml" />
</item>
</channel>
</rss>

View File

@@ -99,7 +99,6 @@ namespace NzbDrone.Core.Test.Framework
{
WithTempAsAppPath();
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());

View File

@@ -1,8 +1,8 @@
using System;
using FluentMigrator;
using FluentMigrator.Runner;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.AutoMoq;
namespace NzbDrone.Core.Test.Framework
{
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.Framework
BeforeMigration = m =>
{
var migration = m as TMigration;
if (beforeMigration != null && migration is TMigration)
if (beforeMigration != null && migration != null)
{
beforeMigration(migration);
}
@@ -39,10 +39,8 @@ namespace NzbDrone.Core.Test.Framework
[SetUp]
public override void SetupDb()
{
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
SetupContainer();
}
[Obsolete("Don't use Mocker/Repositories in MigrationTests, query the DB.", true)]
public new AutoMoqer Mocker => base.Mocker;
}
}

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
Subject.Definition = new IndexerDefinition()
{
Name = "BroadcastheNet",
Settings = new BroadcastheNetSettings() { ApiKey = "abc", BaseUrl = "https://api.btnapps.net/" }
Settings = new BroadcastheNetSettings() { ApiKey = "abc", BaseUrl = "https://api.broadcasthe.net/" }
};
}
@@ -139,7 +139,7 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
{
var recentFeed = ReadAllText(@"Files/Indexers/BroadcastheNet/RecentFeed.json");
(Subject.Definition.Settings as BroadcastheNetSettings).BaseUrl = "http://api.btnapps.net/";
(Subject.Definition.Settings as BroadcastheNetSettings).BaseUrl = "http://api.broadcasthe.net/";
recentFeed = recentFeed.Replace("http:", "https:");

View File

@@ -5,7 +5,6 @@ using NUnit.Framework;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Indexers.Omgwtfnzbs;
using NzbDrone.Core.Indexers.Wombles;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Test.Framework;
@@ -22,7 +21,6 @@ namespace NzbDrone.Core.Test.IndexerTests
_indexers.Add(Mocker.Resolve<Newznab>());
_indexers.Add(Mocker.Resolve<Omgwtfnzbs>());
_indexers.Add(Mocker.Resolve<Wombles>());
Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers);
}
@@ -44,4 +42,4 @@ namespace NzbDrone.Core.Test.IndexerTests
AllStoredModels.Should().NotContain(c => c.Id == existingIndexers.Id);
}
}
}
}

View File

@@ -4,9 +4,7 @@ using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.KickassTorrents;
using NzbDrone.Core.Indexers.Nyaa;
using NzbDrone.Core.Indexers.Wombles;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
@@ -40,58 +38,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
};
}
[Test]
public void wombles_fetch_recent()
{
var indexer = Mocker.Resolve<Wombles>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = NullConfig.Instance
};
var result = indexer.FetchRecent();
ValidateResult(result);
}
[Test]
[ManualTest]
[Explicit]
public void kickass_fetch_recent()
{
var indexer = Mocker.Resolve<KickassTorrents>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new KickassTorrentsSettings()
};
var result = indexer.FetchRecent();
ValidateTorrentResult(result, hasSize: true);
}
[Test]
[ManualTest]
[Explicit]
public void kickass_search_single()
{
var indexer = Mocker.Resolve<KickassTorrents>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new KickassTorrentsSettings()
};
var result = indexer.Fetch(_singleSearchCriteria);
ValidateTorrentResult(result, hasSize: true, hasMagnet: true);
}
[Test]
public void nyaa_fetch_recent()
{

View File

@@ -1,173 +0,0 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.KickassTorrents;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
using System;
using System.Linq;
using FluentAssertions;
using System.Text.RegularExpressions;
namespace NzbDrone.Core.Test.IndexerTests.KickassTorrentsTests
{
[TestFixture]
public class KickassTorrentsFixture : CoreTest<KickassTorrents>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Kickass Torrents",
Settings = new KickassTorrentsSettings() { VerifiedOnly = false }
};
}
[Test]
public void should_parse_recent_feed_from_KickassTorrents()
{
var recentFeed = ReadAllText(@"Files/Indexers/KickassTorrents/KickassTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = (TorrentInfo) releases.First();
torrentInfo.Title.Should().Be("Doctor Stranger.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://torcache.net/torrent/208C4F7866612CC88BFEBC7C496FA72C2368D1C0.torrent?title=%5Bkickass.to%5Ddoctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg");
torrentInfo.InfoUrl.Should().Be("http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/12 16:16:49"));
torrentInfo.Size.Should().Be(1205364736);
torrentInfo.InfoHash.Should().Be("208C4F7866612CC88BFEBC7C496FA72C2368D1C0");
torrentInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:208C4F7866612CC88BFEBC7C496FA72C2368D1C0&dn=doctor+stranger+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
}
[Test]
public void should_return_empty_list_on_404()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.NotFound));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(0);
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_return_unverified_releases_if_not_configured()
{
((KickassTorrentsSettings) Subject.Definition.Settings).VerifiedOnly = true;
var recentFeed = ReadAllText(@"Files/Indexers/KickassTorrents/KickassTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(4);
}
[Test]
public void should_set_seeders_to_null()
{
// Atm, Kickass supplies 0 as seeders and leechers on the rss feed (but not the site), so set it to null if there aren't any peers.
var recentFeed = ReadAllText(@"Files/Indexers/KickassTorrents/KickassTorrents.xml");
recentFeed = recentFeed.Replace("<pubDate>Mon, 12 May 2014 16:16:49 +0000</pubDate>", string.Format("<pubDate>{0:R}</pubDate>", DateTime.UtcNow));
recentFeed = Regex.Replace(recentFeed, @"(seeds|peers)\>\d*", "$1>0");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = (TorrentInfo)releases.First();
torrentInfo.Peers.Should().NotHaveValue();
torrentInfo.Seeders.Should().NotHaveValue();
}
[Test]
public void should_not_set_seeders_to_null_if_has_peers()
{
// Atm, Kickass supplies 0 as seeders and leechers on the rss feed (but not the site), so set it to null if there aren't any peers.
var recentFeed = ReadAllText(@"Files/Indexers/KickassTorrents/KickassTorrents.xml");
recentFeed = recentFeed.Replace("<pubDate>Mon, 12 May 2014 16:16:49 +0000</pubDate>", string.Format("<pubDate>{0:R}</pubDate>", DateTime.UtcNow));
recentFeed = Regex.Replace(recentFeed, @"(seeds)\>\d*", "$1>0");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = (TorrentInfo)releases.First();
torrentInfo.Peers.Should().Be(311);
torrentInfo.Seeders.Should().Be(0);
}
[Test]
public void should_not_set_seeders_to_null_if_older_than_12_hours()
{
// Atm, Kickass supplies 0 as seeders and leechers on the rss feed (but not the site), so set it to null if there aren't any peers.
var recentFeed = ReadAllText(@"Files/Indexers/KickassTorrents/KickassTorrents.xml");
recentFeed = Regex.Replace(recentFeed, @"(seeds|peers)\>\d*", "$1>0");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = (TorrentInfo)releases.First();
torrentInfo.Peers.Should().Be(0);
torrentInfo.Seeders.Should().Be(0);
}
[Test]
public void should_handle_xml_with_html_accents()
{
var recentFeed = ReadAllText(@"Files/Indexers/KickassTorrents/KickassTorrents_accents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
}
}
}

View File

@@ -173,6 +173,32 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
torrentInfo.Seeders.Should().NotHaveValue();
}
[Test]
public void should_parse_recent_feed_from_LimeTorrents()
{
GivenRecentFeedResponse("TorrentRss/LimeTorrents.xml");
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("The Expanse 2x04 (720p-HDTV-x264-SVA)[VTV]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://itorrents.org/torrent/51C578C9823DD58F6EEA287C368ED935843D63AB.torrent?title=The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]");
torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().Be("http://www.limetorrents.cc/The-Expanse-2x04-(720p-HDTV-x264-SVA)[VTV]-torrent-8643587.html");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("16 Feb 2017 05:24:26 +0300").ToUniversalTime());
torrentInfo.Size.Should().Be(880496711);
torrentInfo.InfoHash.Should().BeNull();
torrentInfo.MagnetUrl.Should().BeNull();
torrentInfo.Peers.Should().NotHaveValue();
torrentInfo.Seeders.Should().NotHaveValue();
}
[Test]
public void should_parse_recent_feed_from_AnimeTosho_without_size()
{

View File

@@ -180,6 +180,26 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
});
}
[Test]
public void should_detect_rss_settings_for_LimeTorrents()
{
_indexerSettings.AllowZeroSize = true;
GivenRecentFeedResponse("TorrentRss/LimeTorrents.xml");
var settings = Subject.Detect(_indexerSettings);
settings.ShouldBeEquivalentTo(new TorrentRssIndexerParserSettings
{
UseEZTVFormat = false,
UseEnclosureUrl = true,
UseEnclosureLength = true,
ParseSizeInDescription = false,
ParseSeedersInDescription = false,
SizeElementName = null
});
}
[Test]
[Ignore("Cannot reliably reject unparseable titles")]
public void should_reject_rss_settings_for_AwesomeHD()
@@ -215,7 +235,6 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
[TestCase("BitMeTv/BitMeTv.xml")]
[TestCase("Fanzub/fanzub.xml")]
[TestCase("KickassTorrents/KickassTorrents.xml")]
[TestCase("IPTorrents/IPTorrents.xml")]
[TestCase("Newznab/newznab_nzb_su.xml")]
[TestCase("Nyaa/Nyaa.xml")]

View File

@@ -1,59 +0,0 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Wombles;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Test.IndexerTests.WomblesTests
{
[TestFixture]
public class TorrentRssIndexerFixture : CoreTest<Wombles>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Wombles",
Settings = new NullConfig(),
};
}
private void GivenRecentFeedResponse(string rssXmlFile)
{
var recentFeed = ReadAllText(@"Files/Indexers/" + rssXmlFile);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
}
[Test]
public void should_parse_recent_feed_from_wombles()
{
GivenRecentFeedResponse("Wombles/wombles.xml");
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
var releaseInfo = releases.First();
releaseInfo.Title.Should().Be("One.Child.S01E01.720p.HDTV.x264-TLA");
releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
releaseInfo.DownloadUrl.Should().Be("http://indexer.local/nzb/bb4/One.Child.S01E01.720p.HDTV.x264-TLA.nzb");
releaseInfo.InfoUrl.Should().BeNullOrEmpty();
releaseInfo.CommentUrl.Should().BeNullOrEmpty();
releaseInfo.Indexer.Should().Be(Subject.Definition.Name);
releaseInfo.PublishDate.Should().Be(DateTime.Parse("2016-02-17 23:03:52 +0000").ToUniversalTime());
releaseInfo.Size.Should().Be(956*1024*1024);
}
}
}

View File

@@ -0,0 +1,120 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
{
[TestFixture]
public class FormattedAudioChannelsFixture
{
[Test]
public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 6,
AudioChannelPositions = null,
AudioChannelPositionsText = "Front: L C R, Side: L R, LFE"
};
mediaInfoModel.FormattedAudioChannels.Should().Be(5.1m);
}
[Test]
public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = "Front: L R"
};
mediaInfoModel.FormattedAudioChannels.Should().Be(2);
}
[Test]
public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = null,
SchemaRevision = 2
};
mediaInfoModel.FormattedAudioChannels.Should().Be(0);
}
[Test]
public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = null,
SchemaRevision = 3
};
mediaInfoModel.FormattedAudioChannels.Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "2/0/0",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
mediaInfoModel.FormattedAudioChannels.Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions_including_decimal()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "3/2/0.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
mediaInfoModel.FormattedAudioChannels.Should().Be(5.1m);
}
[Test]
public void should_cleanup_extraneous_text_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "Object Based / 3/2/2.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
mediaInfoModel.FormattedAudioChannels.Should().Be(7.1m);
}
[Test]
public void should_sum_first_series_of_numbers_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "3/2/2.1 / 3/2/2.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
mediaInfoModel.FormattedAudioChannels.Should().Be(7.1m);
}
}
}

View File

@@ -124,6 +124,7 @@
<Compile Include="Datastore\DatabaseRelationshipFixture.cs" />
<Compile Include="Datastore\MappingExtentionFixture.cs" />
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
<Compile Include="Datastore\Migration\106_update_btn_urlFixture.cs" />
<Compile Include="Datastore\Migration\103_fix_metadata_file_extensionsFixture.cs" />
<Compile Include="Datastore\Migration\099_extra_and_subtitle_filesFixture.cs" />
<Compile Include="Datastore\Migration\101_add_ultrahd_quality_in_profilesFixture.cs" />
@@ -165,6 +166,7 @@
<Compile Include="DecisionEngineTests\SameEpisodesSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RawDiskSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\UpgradeDiskSpecificationFixture.cs" />
<Compile Include="DiskSpace\DiskSpaceServiceFixture.cs" />
<Compile Include="Download\CompletedDownloadServiceFixture.cs" />
<Compile Include="Download\DownloadApprovedReportsTests\DownloadApprovedFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\ScanWatchFolderFixture.cs" />
@@ -172,6 +174,10 @@
<Compile Include="Download\DownloadClientTests\Blackhole\UsenetBlackholeFixture.cs" />
<Compile Include="Download\DownloadClientTests\DelugeTests\DelugeFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" />
<Compile Include="Download\DownloadClientTests\DownloadStationTests\TorrentDownloadStationFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadStationTests\SerialNumberProviderFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadStationTests\SharedFolderResolverFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadStationTests\UsenetDownloadStationFixture.cs" />
<Compile Include="Download\DownloadClientTests\HadoukenTests\HadoukenFixture.cs" />
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
<Compile Include="Download\DownloadClientTests\NzbVortexTests\NzbVortexFixture.cs" />
@@ -258,12 +264,10 @@
<Compile Include="IndexerTests\TestIndexer.cs" />
<Compile Include="IndexerTests\TestIndexerSettings.cs" />
<Compile Include="IndexerTests\IPTorrentsTests\IPTorrentsFixture.cs" />
<Compile Include="IndexerTests\KickassTorrentsTests\KickassTorrentsFixture.cs" />
<Compile Include="IndexerTests\NyaaTests\NyaaFixture.cs" />
<Compile Include="IndexerTests\TorrentleechTests\TorrentleechFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssIndexerFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TestTorrentRssIndexer.cs" />
<Compile Include="IndexerTests\WomblesTests\WomblesFixture.cs" />
<Compile Include="IndexerTests\XElementExtensionsFixture.cs" />
<Compile Include="Instrumentation\DatabaseTargetFixture.cs" />
<Compile Include="JobTests\JobRepositoryFixture.cs" />
@@ -285,6 +289,7 @@
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
<Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" />
<Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" />
<Compile Include="MediaFiles\MediaInfo\FormattedAudioChannelsFixture.cs" />
<Compile Include="Messaging\Commands\CommandQueueManagerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchSeriesComparerFixture.cs" />
@@ -415,6 +420,9 @@
<Link>sqlite3.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Indexers\TorrentRss\LimeTorrents.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="License.txt" />
<None Include="Files\Indexers\BroadcastheNet\RecentFeed.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@@ -431,9 +439,6 @@
<Content Include="Files\emptyfile.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Indexers\KickassTorrents\KickassTorrents_accents.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Indexers\Newznab\newznab_caps.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -530,20 +535,12 @@
<Content Include="Files\Indexers\Torznab\torznab_tpb.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Indexers\Wombles\wombles.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<SubType>Designer</SubType>
</Content>
<Content Include="Files\Indexers\BitMeTv\BitMeTv.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Indexers\IPTorrents\IPTorrents.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Indexers\KickassTorrents\KickassTorrents.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<SubType>Designer</SubType>
</Content>
<Content Include="Files\Indexers\Torrentleech\Torrentleech.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.ParserTests
}
};
[Test, TestCaseSource("HashedReleaseParserCases")]
[Test, TestCaseSource(nameof(HashedReleaseParserCases))]
public void should_properly_parse_hashed_releases(string path, string title, Quality quality, string releaseGroup)
{
var result = Parser.Parser.ParsePath(path);

View File

@@ -55,6 +55,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title.S6.E1-S6E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2 })]
[TestCase("Series Title.S6.E1E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2 })]
[TestCase("Series Title.S6.E1-E2-E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2, 3 })]
[TestCase("Mad.Men.S05E01-E02.720p.5.1Ch.BluRay", "Mad Men", 5, new[] { 1, 2 })]
[TestCase("Mad.Men.S05E01-02.720p.5.1Ch.BluRay", "Mad Men", 5, new[] { 1, 2 })]
//[TestCase("", "", , new [] { })]
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
{

View File

@@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.ParserTests
ParseAndVerifyQuality(title, Quality.Unknown, proper);
}
[Test, TestCaseSource("SelfQualityParserCases")]
[Test, TestCaseSource(nameof(SelfQualityParserCases))]
public void parsing_our_own_quality_enum_name(Quality quality)
{
var fileName = string.Format("My series S01E01 [{0}]", quality.Name);
@@ -246,7 +246,7 @@ namespace NzbDrone.Core.Test.ParserTests
result.Quality.Should().Be(quality);
}
[Test, TestCaseSource("OtherSourceQualityParserCases")]
[Test, TestCaseSource(nameof(OtherSourceQualityParserCases))]
public void should_parse_quality_from_other_source(string qualityString, Quality quality)
{
foreach (var c in new char[] { '-', '.', ' ', '_' })

View File

@@ -1,39 +1,47 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
{
public class GetVideoFilesFixture : CoreTest<DiskScanService>
{
private string[] _files;
private string[] _fileNames;
[SetUp]
public void Setup()
{
_files = new[]
_fileNames = new[]
{
@"C:\Test\30 Rock1.mkv",
@"C:\Test\30 Rock2.avi",
@"C:\Test\30 Rock3.MP4",
@"C:\Test\30 Rock4.wMv",
@"C:\Test\movie.exe",
@"C:\Test\movie"
@"30 Rock1.mkv",
@"30 Rock2.avi",
@"30 Rock3.MP4",
@"30 Rock4.wMv",
@"movie.exe",
@"movie"
};
GivenFiles();
}
private void GivenFiles()
private IEnumerable<string> GetFiles(string folder, string subFolder = "")
{
return _fileNames.Select(f => Path.Combine(folder, subFolder, f));
}
private void GivenFiles(IEnumerable<string> files)
{
var filesToReturn = files.ToArray();
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetFiles(It.IsAny<string>(), SearchOption.AllDirectories))
.Returns(_files);
.Setup(s => s.GetFiles(It.IsAny<string>(), SearchOption.AllDirectories))
.Returns(filesToReturn);
}
[Test]
@@ -73,8 +81,31 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
public void should_return_video_files_only()
{
var path = @"C:\Test\";
GivenFiles(GetFiles(path));
Subject.GetVideoFiles(path).Should().HaveCount(4);
}
[TestCase("Extras")]
[TestCase("@eadir")]
[TestCase("extrafanart")]
[TestCase("Plex Versions")]
[TestCase(".secret")]
[TestCase(".hidden")]
public void should_filter_certain_sub_folders(string subFolder)
{
var path = @"C:\Test\";
var files = GetFiles(path).ToList();
var specialFiles = GetFiles(path, subFolder).ToList();
var allFiles = files.Concat(specialFiles);
var series = Builder<Series>.CreateNew()
.With(s => s.Path = path)
.Build();
var filteredFiles = Subject.FilterFiles(series, allFiles);
filteredFiles.Should().NotContain(specialFiles);
filteredFiles.Count.Should().BeGreaterThan(0);
}
}
}

View File

@@ -47,14 +47,14 @@ namespace NzbDrone.Core.Test.Qualities
new object[] {Quality.Bluray2160p, 19},
};
[Test, TestCaseSource("FromIntCases")]
[Test, TestCaseSource(nameof(FromIntCases))]
public void should_be_able_to_convert_int_to_qualityTypes(int source, Quality expected)
{
var quality = (Quality)source;
quality.Should().Be(expected);
}
[Test, TestCaseSource("ToIntCases")]
[Test, TestCaseSource(nameof(ToIntCases))]
public void should_be_able_to_convert_qualityTypes_to_int(Quality source, int expected)
{
var i = (int)source;

View File

@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.RootFolderTests
@@ -102,6 +106,50 @@ namespace NzbDrone.Core.Test.RootFolderTests
.Returns(path);
Assert.Throws<InvalidOperationException>(() => Subject.Add(new RootFolder { Path = path }));
}
[TestCase("$recycle.bin")]
[TestCase("system volume information")]
[TestCase("recycler")]
[TestCase("lost+found")]
[TestCase(".appledb")]
[TestCase(".appledesktop")]
[TestCase(".appledouble")]
[TestCase("@eadir")]
[TestCase(".grab")]
public void should_get_root_folder_with_subfolders_excluding_special_sub_folders(string subFolder)
{
var rootFolderPath = @"C:\Test\TV".AsOsAgnostic();
var rootFolder = Builder<RootFolder>.CreateNew()
.With(r => r.Path = rootFolderPath)
.Build();
var subFolders = new[]
{
"Series1",
"Series2",
"Series3",
subFolder
};
var folders = subFolders.Select(f => Path.Combine(rootFolderPath, f)).ToArray();
Mocker.GetMock<IRootFolderRepository>()
.Setup(s => s.Get(It.IsAny<int>()))
.Returns(rootFolder);
Mocker.GetMock<ISeriesService>()
.Setup(s => s.GetAllSeries())
.Returns(new List<Series>());
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetDirectories(rootFolder.Path))
.Returns(folders);
var unmappedFolders = Subject.Get(rootFolder.Id).UnmappedFolders;
unmappedFolders.Count.Should().BeGreaterThan(0);
unmappedFolders.Should().NotContain(u => u.Name == subFolder);
}
}
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(104)]
public class remove_kickass : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'KickassTorrents';");
}
}
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(105)]
public class rename_torrent_downloadstation : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("UPDATE DownloadClients SET Implementation = 'TorrentDownloadStation' WHERE Implementation = 'DownloadStation';");
}
}
}

View File

@@ -0,0 +1,22 @@
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(106)]
public class update_btn_url : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("UPDATE Indexers SET Settings = Replace(Settings, 'api.btnapps.net', 'api.broadcasthe.net') WHERE Implementation = 'BroadcastheNet';");
}
}
public class BroadcastheNetSettings106
{
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(107)]
public class remove_wombles : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Wombles';");
}
}
}

View File

@@ -46,7 +46,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
public override void Up()
{
if (Context.BeforeMigration != null)
{
Context.BeforeMigration(this);

View File

@@ -4,6 +4,7 @@ using System.Linq;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.DecisionEngine
{
@@ -75,8 +76,21 @@ namespace NzbDrone.Core.DecisionEngine
private int CompareEpisodeCount(DownloadDecision x, DownloadDecision y)
{
return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.FullSeason),
CompareByReverse(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Episodes.Count));
var seasonPackCompare = CompareBy(x.RemoteEpisode, y.RemoteEpisode,
remoteEpisode => remoteEpisode.ParsedEpisodeInfo.FullSeason);
if (seasonPackCompare != 0)
{
return seasonPackCompare;
}
if (x.RemoteEpisode.Series.SeriesType == SeriesTypes.Anime &
y.RemoteEpisode.Series.SeriesType == SeriesTypes.Anime)
{
return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Episodes.Count);
}
return CompareByReverse(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Episodes.Count);
}
private int CompareEpisodeNumber(DownloadDecision x, DownloadDecision y)

View File

@@ -42,14 +42,17 @@ namespace NzbDrone.Core.DiskSpace
private IEnumerable<DiskSpace> GetSeriesFreeSpace()
{
var seriesRootPaths = _seriesService.GetAllSeries().Select(s => _diskProvider.GetPathRoot(s.Path)).Distinct();
var seriesRootPaths = _seriesService.GetAllSeries()
.Where(s => _diskProvider.FolderExists(s.Path))
.Select(s => _diskProvider.GetPathRoot(s.Path))
.Distinct();
return GetDiskSpace(seriesRootPaths);
}
private IEnumerable<DiskSpace> GetDroneFactoryFreeSpace()
{
if (!string.IsNullOrWhiteSpace(_configService.DownloadedEpisodesFolder))
if (_configService.DownloadedEpisodesFolder.IsNotNullOrWhiteSpace() && _diskProvider.FolderExists(_configService.DownloadedEpisodesFolder))
{
return GetDiskSpace(new[] { _diskProvider.GetPathRoot(_configService.DownloadedEpisodesFolder) });
}

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
public DelugeSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.TvCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -");
}

View File

@@ -0,0 +1,12 @@
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public enum DiskStationApi
{
Info,
Auth,
DownloadStationInfo,
DownloadStationTask,
FileStationList,
DSMInfo,
}
}

View File

@@ -0,0 +1,24 @@
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class DiskStationApiInfo
{
private string _path;
public int MaxVersion { get; set; }
public int MinVersion { get; set; }
public string Path
{
get { return _path; }
set
{
if (!string.IsNullOrEmpty(value))
{
_path = value.TrimStart(new char[] { '/', '\\' });
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class DownloadStationSettingsValidator : AbstractValidator<DownloadStationSettings>
{
public DownloadStationSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.TvDirectory).Matches(@"^(?!/).+")
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
.WithMessage("Cannot start with /");
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
RuleFor(c => c.TvCategory).Empty()
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
.WithMessage("Cannot use Category and Directory");
}
}
public class DownloadStationSettings : IProviderConfig
{
private static readonly DownloadStationSettingsValidator Validator = new DownloadStationSettingsValidator();
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
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. Creates a [category] subdirectory in the output directory.")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")]
public string TvDirectory { get; set; }
[FieldDefinition(6, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public DownloadStationSettings()
{
this.Host = "127.0.0.1";
this.Port = 5000;
}
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class DownloadStationTask
{
public string Username { get; set; }
public string Id { get; set; }
public string Title { get; set; }
public long Size { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DownloadStationTaskType Type { get; set; }
[JsonProperty(PropertyName = "status_extra")]
public Dictionary<string, string> StatusExtra { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DownloadStationTaskStatus Status { get; set; }
public DownloadStationTaskAdditional Additional { get; set; }
public override string ToString()
{
return this.Title;
}
}
public enum DownloadStationTaskType
{
BT, NZB, http, ftp, eMule
}
public enum DownloadStationTaskStatus
{
Waiting,
Downloading,
Paused,
Finishing,
Finished,
HashChecking,
Seeding,
FileHostingWaiting,
Extracting,
Error
}
public enum DownloadStationPriority
{
Auto,
Low,
Normal,
High
}
}

View File

@@ -0,0 +1,15 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class DownloadStationTaskAdditional
{
public Dictionary<string, string> Detail { get; set; }
public Dictionary<string, string> Transfer { get; set; }
[JsonProperty("File")]
public List<DownloadStationTaskFile> Files { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static NzbDrone.Core.Download.Clients.DownloadStation.DownloadStationTask;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class DownloadStationTaskFile
{
public string FileName { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DownloadStationPriority Priority { get; set; }
[JsonProperty("size")]
public long TotalSize { get; set; }
[JsonProperty("size_downloaded")]
public long BytesDownloaded { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
{
public interface IDSMInfoProxy
{
string GetSerialNumber(DownloadStationSettings settings);
}
public class DSMInfoProxy : DiskStationProxyBase, IDSMInfoProxy
{
public DSMInfoProxy(IHttpClient httpClient, Logger logger) :
base(httpClient, logger)
{
}
public string GetSerialNumber(DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>() {
{ "api", "SYNO.DSM.Info" },
{ "version", "2" },
{ "method", "getinfo" }
};
var response = ProcessRequest<DSMInfoResponse>(DiskStationApi.DSMInfo, arguments, settings, "get serial number");
return response.Data.SerialNumber;
}
}
}

View File

@@ -0,0 +1,213 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
{
public abstract class DiskStationProxyBase
{
private static readonly Dictionary<DiskStationApi, string> Resources;
private readonly IHttpClient _httpClient;
protected readonly Logger _logger;
private bool _authenticated;
static DiskStationProxyBase()
{
Resources = new Dictionary<DiskStationApi, string>
{
{ DiskStationApi.Info, "query.cgi" }
};
}
public DiskStationProxyBase(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
protected DiskStationResponse<object> ProcessRequest(DiskStationApi api,
Dictionary<string, object> arguments,
DownloadStationSettings settings,
string operation,
HttpMethod method = HttpMethod.GET)
{
return ProcessRequest<object>(api, arguments, settings, operation, method);
}
protected DiskStationResponse<T> ProcessRequest<T>(DiskStationApi api,
Dictionary<string, object> arguments,
DownloadStationSettings settings,
string operation,
HttpMethod method = HttpMethod.GET,
int retries = 0) where T : new()
{
if (retries == 5)
{
throw new DownloadClientException("Try to process same request more than 5 times");
}
if (!_authenticated && api != DiskStationApi.Info && api != DiskStationApi.DSMInfo)
{
AuthenticateClient(settings);
}
var request = BuildRequest(settings, api, arguments, method);
var response = _httpClient.Execute(request);
_logger.Debug("Trying to {0}", operation);
if (response.StatusCode == HttpStatusCode.OK)
{
var responseContent = Json.Deserialize<DiskStationResponse<T>>(response.Content);
if (responseContent.Success)
{
return responseContent;
}
else
{
if (responseContent.Error.SessionError)
{
_authenticated = false;
return ProcessRequest<T>(api, arguments, settings, operation, method, retries++);
}
var msg = $"Failed to {operation}. Reason: {responseContent.Error.GetMessage(api)}";
_logger.Error(msg);
throw new DownloadClientException(msg);
}
}
else
{
throw new HttpException(request, response);
}
}
private void AuthenticateClient(DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.API.Auth" },
{ "version", "1" },
{ "method", "login" },
{ "account", settings.Username },
{ "passwd", settings.Password },
{ "format", "cookie" },
{ "session", "DownloadStation" },
};
var authLoginRequest = BuildRequest(settings, DiskStationApi.Auth, arguments, HttpMethod.GET);
authLoginRequest.StoreResponseCookie = true;
var response = _httpClient.Execute(authLoginRequest);
var downloadStationResponse = Json.Deserialize<DiskStationResponse<DiskStationAuthResponse>>(response.Content);
var authResponse = Json.Deserialize<DiskStationResponse<DiskStationAuthResponse>>(response.Content);
_authenticated = authResponse.Success;
if (!_authenticated)
{
throw new DownloadClientAuthenticationException(downloadStationResponse.Error.GetMessage(DiskStationApi.Auth));
}
}
private HttpRequest BuildRequest(DownloadStationSettings settings, DiskStationApi api, Dictionary<string, object> arguments, HttpMethod method)
{
if (!Resources.ContainsKey(api))
{
GetApiVersion(settings, api);
}
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port).Resource($"webapi/{Resources[api]}");
requestBuilder.Method = method;
requestBuilder.LogResponseContent = true;
requestBuilder.SuppressHttpError = true;
requestBuilder.AllowAutoRedirect = false;
if (requestBuilder.Method == HttpMethod.POST)
{
if (api == DiskStationApi.DownloadStationTask && arguments.ContainsKey("file"))
{
requestBuilder.Headers.ContentType = "multipart/form-data";
foreach (var arg in arguments)
{
if (arg.Key == "file")
{
Dictionary<string, object> file = (Dictionary<string, object>)arg.Value;
requestBuilder.AddFormUpload(arg.Key, file["name"].ToString(), (byte[])file["data"]);
}
else
{
requestBuilder.AddFormParameter(arg.Key, arg.Value);
}
}
}
else
{
requestBuilder.Headers.ContentType = "application/json";
}
}
else
{
foreach (var arg in arguments)
{
requestBuilder.AddQueryParam(arg.Key, arg.Value);
}
}
return requestBuilder.Build();
}
protected IEnumerable<int> GetApiVersion(DownloadStationSettings settings, DiskStationApi api)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.API.Info" },
{ "version", "1" },
{ "method", "query" },
{ "query", "SYNO.API.Auth, SYNO.DownloadStation.Info, SYNO.DownloadStation.Task, SYNO.FileStation.List, SYNO.DSM.Info" },
};
var infoResponse = ProcessRequest<DiskStationApiInfoResponse>(DiskStationApi.Info, arguments, settings, "Get api version");
//TODO: Refactor this into more elegant code
var infoResponeDSAuth = infoResponse.Data["SYNO.API.Auth"];
var infoResponeDSInfo = infoResponse.Data["SYNO.DownloadStation.Info"];
var infoResponeDSTask = infoResponse.Data["SYNO.DownloadStation.Task"];
var infoResponseFSList = infoResponse.Data["SYNO.FileStation.List"];
var infoResponseDSMInfo = infoResponse.Data["SYNO.DSM.Info"];
Resources[DiskStationApi.Auth] = infoResponeDSAuth.Path;
Resources[DiskStationApi.DownloadStationInfo] = infoResponeDSInfo.Path;
Resources[DiskStationApi.DownloadStationTask] = infoResponeDSTask.Path;
Resources[DiskStationApi.FileStationList] = infoResponseFSList.Path;
Resources[DiskStationApi.DSMInfo] = infoResponseDSMInfo.Path;
switch (api)
{
case DiskStationApi.Auth:
return Enumerable.Range(infoResponeDSAuth.MinVersion, infoResponeDSAuth.MaxVersion - infoResponeDSAuth.MinVersion + 1);
case DiskStationApi.DownloadStationInfo:
return Enumerable.Range(infoResponeDSInfo.MinVersion, infoResponeDSInfo.MaxVersion - infoResponeDSInfo.MinVersion + 1);
case DiskStationApi.DownloadStationTask:
return Enumerable.Range(infoResponeDSTask.MinVersion, infoResponeDSTask.MaxVersion - infoResponeDSTask.MinVersion + 1);
case DiskStationApi.FileStationList:
return Enumerable.Range(infoResponseFSList.MinVersion, infoResponseFSList.MaxVersion - infoResponseFSList.MinVersion + 1);
case DiskStationApi.DSMInfo:
return Enumerable.Range(infoResponseDSMInfo.MinVersion, infoResponseDSMInfo.MaxVersion - infoResponseDSMInfo.MinVersion + 1);
default:
throw new DownloadClientException("Api not implemented");
}
}
}
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
{
public interface IDownloadStationProxy
{
IEnumerable<DownloadStationTask> GetTasks(DownloadStationTaskType type, DownloadStationSettings settings);
Dictionary<string, object> GetConfig(DownloadStationSettings settings);
void RemoveTask(string downloadId, DownloadStationSettings settings);
void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings);
void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings);
IEnumerable<int> GetApiVersion(DownloadStationSettings settings);
}
public class DownloadStationProxy : DiskStationProxyBase, IDownloadStationProxy
{
public DownloadStationProxy(IHttpClient httpClient, Logger logger)
: base(httpClient, logger)
{
}
public void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.DownloadStation.Task" },
{ "version", "2" },
{ "method", "create" }
};
if (downloadDirectory.IsNotNullOrWhiteSpace())
{
arguments.Add("destination", downloadDirectory);
}
arguments.Add("file", new Dictionary<string, object>() { { "name", filename }, { "data", data } });
var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"add task from data {filename}", HttpMethod.POST);
}
public void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.DownloadStation.Task" },
{ "version", "3" },
{ "method", "create" },
{ "uri", url }
};
if (downloadDirectory.IsNotNullOrWhiteSpace())
{
arguments.Add("destination", downloadDirectory);
}
var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"add task from url {url}");
}
public IEnumerable<DownloadStationTask> GetTasks(DownloadStationTaskType type, DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.DownloadStation.Task" },
{ "version", "1" },
{ "method", "list" },
{ "additional", "detail,transfer" }
};
try
{
var response = ProcessRequest<DownloadStationTaskInfoResponse>(DiskStationApi.DownloadStationTask, arguments, settings, "get tasks");
return response.Data.Tasks.Where(t => t.Type == type);
}
catch (DownloadClientException e)
{
_logger.Error(e);
return new List<DownloadStationTask>();
}
}
public Dictionary<string, object> GetConfig(DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.DownloadStation.Info" },
{ "version", "1" },
{ "method", "getconfig" }
};
var response = ProcessRequest<Dictionary<string, object>>(DiskStationApi.DownloadStationInfo, arguments, settings, "get config");
return response.Data;
}
public void RemoveTask(string downloadId, DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.DownloadStation.Task" },
{ "version", "1" },
{ "method", "delete" },
{ "id", downloadId },
{ "force_complete", false }
};
var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"remove item {downloadId}");
}
public IEnumerable<int> GetApiVersion(DownloadStationSettings settings)
{
return base.GetApiVersion(settings, DiskStationApi.DownloadStationInfo);
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
{
public interface IFileStationProxy
{
SharedFolderMapping GetSharedFolderMapping(string sharedFolder, DownloadStationSettings settings);
IEnumerable<int> GetApiVersion(DownloadStationSettings settings);
FileStationListFileInfoResponse GetInfoFileOrDirectory(string path, DownloadStationSettings settings);
}
public class FileStationProxy : DiskStationProxyBase, IFileStationProxy
{
public FileStationProxy(IHttpClient httpClient, Logger logger)
: base(httpClient, logger)
{
}
public IEnumerable<int> GetApiVersion(DownloadStationSettings settings)
{
return base.GetApiVersion(settings, DiskStationApi.FileStationList);
}
public SharedFolderMapping GetSharedFolderMapping(string sharedFolder, DownloadStationSettings settings)
{
var info = GetInfoFileOrDirectory(sharedFolder, settings);
var physicalPath = info.Additional["real_path"].ToString();
return new SharedFolderMapping(sharedFolder, physicalPath);
}
public FileStationListFileInfoResponse GetInfoFileOrDirectory(string path, DownloadStationSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "api", "SYNO.FileStation.List" },
{ "version", "2" },
{ "method", "getinfo" },
{ "path", new [] { path }.ToJson() },
{ "additional", $"[\"real_path\"]" }
};
var response = ProcessRequest<FileStationListResponse>(DiskStationApi.FileStationList, arguments, settings, $"get info of {path}");
return response.Data.Files.First();
}
}
}

View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class DSMInfoResponse
{
[JsonProperty("serial")]
public string SerialNumber { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class DiskStationAuthResponse
{
public string SId { get; set; }
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class DiskStationError
{
private static readonly Dictionary<int, string> CommonMessages;
private static readonly Dictionary<int, string> AuthMessages;
private static readonly Dictionary<int, string> DownloadStationTaskMessages;
private static readonly Dictionary<int, string> FileStationMessages;
static DiskStationError()
{
CommonMessages = new Dictionary<int, string>
{
{ 100, "Unknown error" },
{ 101, "Invalid parameter" },
{ 102, "The requested API does not exist" },
{ 103, "The requested method does not exist" },
{ 104, "The requested version does not support the functionality" },
{ 105, "The logged in session does not have permission" },
{ 106, "Session timeout" },
{ 107, "Session interrupted by duplicate login" }
};
AuthMessages = new Dictionary<int, string>
{
{ 400, "No such account or incorrect password" },
{ 401, "Account disabled" },
{ 402, "Permission denied" },
{ 403, "2-step verification code required" },
{ 404, "Failed to authenticate 2-step verification code" }
};
DownloadStationTaskMessages = new Dictionary<int, string>
{
{ 400, "File upload failed" },
{ 401, "Max number of tasks reached" },
{ 402, "Destination denied" },
{ 403, "Destination does not exist" },
{ 404, "Invalid task id" },
{ 405, "Invalid task action" },
{ 406, "No default destination" },
{ 407, "Set destination failed" },
{ 408, "File does not exist" }
};
FileStationMessages = new Dictionary<int, string>
{
{ 400, "Invalid parameter of file operation" },
{ 401, "Unknown error of file operation" },
{ 402, "System is too busy" },
{ 403, "Invalid user does this file operation" },
{ 404, "Invalid group does this file operation" },
{ 405, "Invalid user and group does this file operation" },
{ 406, "Cant get user/group information from the account server" },
{ 407, "Operation not permitted" },
{ 408, "No such file or directory" },
{ 409, "Non-supported file system" },
{ 410, "Failed to connect internet-based file system (ex: CIFS)" },
{ 411, "Read-only file system" },
{ 412, "Filename too long in the non-encrypted file system" },
{ 413, "Filename too long in the encrypted file system" },
{ 414, "File already exists" },
{ 415, "Disk quota exceeded" },
{ 416, "No space left on device" },
{ 417, "Input/output error" },
{ 418, "Illegal name or path" },
{ 419, "Illegal file name" },
{ 420, "Illegal file name on FAT file system" },
{ 421, "Device or resource busy" },
{ 599, "No such task of the file operation" },
};
}
public int Code { get; set; }
public bool SessionError => Code == 105 || Code == 106 || Code == 107;
public string GetMessage(DiskStationApi api)
{
if (api == DiskStationApi.Auth && AuthMessages.ContainsKey(Code))
{
return AuthMessages[Code];
}
if (api == DiskStationApi.DownloadStationTask && DownloadStationTaskMessages.ContainsKey(Code))
{
return DownloadStationTaskMessages[Code];
}
if (api == DiskStationApi.FileStationList && FileStationMessages.ContainsKey(Code))
{
return FileStationMessages[Code];
}
return CommonMessages[Code];
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class DiskStationApiInfoResponse : Dictionary<string, DiskStationApiInfo>
{
}
}

View File

@@ -0,0 +1,11 @@
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class DiskStationResponse<T> where T:new()
{
public bool Success { get; set; }
public DiskStationError Error { get; set; }
public T Data { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class DownloadStationTaskInfoResponse
{
public int Offset { get; set; }
public List<DownloadStationTask> Tasks {get;set;}
public int Total { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class FileStationListFileInfoResponse
{
public bool IsDir { get; set; }
public string Name { get; set; }
public string Path { get; set; }
public Dictionary <string, object> Additional { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
{
public class FileStationListResponse
{
public List<FileStationListFileInfoResponse> Files { get; set; }
}
}

View File

@@ -0,0 +1,49 @@
using System;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Crypto;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public interface ISerialNumberProvider
{
string GetSerialNumber(DownloadStationSettings settings);
}
public class SerialNumberProvider : ISerialNumberProvider
{
private readonly IDSMInfoProxy _proxy;
private ICached<string> _cache;
private readonly ILogger _logger;
public SerialNumberProvider(ICacheManager cacheManager,
IDSMInfoProxy proxy,
Logger logger)
{
_proxy = proxy;
_cache = cacheManager.GetCache<string>(GetType());
_logger = logger;
}
public string GetSerialNumber(DownloadStationSettings settings)
{
try
{
return _cache.Get(settings.Host, () => GetHashedSerialNumber(settings), TimeSpan.FromMinutes(5));
}
catch (Exception ex)
{
_logger.Warn(ex, "Could not get the serial number from Download Station {0}:{1}", settings.Host, settings.Port);
throw;
}
}
private string GetHashedSerialNumber(DownloadStationSettings settings)
{
var serialNumber = _proxy.GetSerialNumber(settings);
return HashConverter.GetHash(serialNumber).ToHexString();
}
}
}

View File

@@ -0,0 +1,21 @@
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class SharedFolderMapping
{
public OsPath PhysicalPath { get; private set; }
public OsPath SharedFolder { get; private set; }
public SharedFolderMapping(string sharedFolder, string physicalPath)
{
SharedFolder = new OsPath(sharedFolder);
PhysicalPath = new OsPath(physicalPath);
}
public override string ToString()
{
return $"{SharedFolder} -> {PhysicalPath}";
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public interface ISharedFolderResolver
{
OsPath RemapToFullPath(OsPath sharedFolderPath, DownloadStationSettings settings, string serialNumber);
}
public class SharedFolderResolver : ISharedFolderResolver
{
private readonly IFileStationProxy _proxy;
private ICached<SharedFolderMapping> _cache;
private readonly ILogger _logger;
public SharedFolderResolver(ICacheManager cacheManager,
IFileStationProxy proxy,
Logger logger)
{
_proxy = proxy;
_cache = cacheManager.GetCache<SharedFolderMapping>(GetType());
_logger = logger;
}
private SharedFolderMapping GetPhysicalPath(OsPath sharedFolder, DownloadStationSettings settings)
{
try
{
return _proxy.GetSharedFolderMapping(sharedFolder.FullPath, settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to get shared folder {0} from Disk Station {1}:{2}", sharedFolder, settings.Host, settings.Port);
throw;
}
}
public OsPath RemapToFullPath(OsPath sharedFolderPath, DownloadStationSettings settings, string serialNumber)
{
var index = sharedFolderPath.FullPath.IndexOf('/', 1);
var sharedFolder = index == -1 ? sharedFolderPath : new OsPath(sharedFolderPath.FullPath.Substring(0, index));
var mapping = _cache.Get($"{serialNumber}:{sharedFolder}", () => GetPhysicalPath(sharedFolder, settings), TimeSpan.FromHours(1));
var fullPath = mapping.PhysicalPath + (sharedFolderPath - mapping.SharedFolder);
return fullPath;
}
}
}

View File

@@ -0,0 +1,401 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class TorrentDownloadStation : TorrentClientBase<DownloadStationSettings>
{
protected readonly IDownloadStationProxy _proxy;
protected readonly ISharedFolderResolver _sharedFolderResolver;
protected readonly ISerialNumberProvider _serialNumberProvider;
protected readonly IFileStationProxy _fileStationProxy;
public TorrentDownloadStation(ISharedFolderResolver sharedFolderResolver,
ISerialNumberProvider serialNumberProvider,
IFileStationProxy fileStationProxy,
IDownloadStationProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
_fileStationProxy = fileStationProxy;
_sharedFolderResolver = sharedFolderResolver;
_serialNumberProvider = serialNumberProvider;
}
public override string Name => "Download Station";
public override IEnumerable<DownloadClientItem> GetItems()
{
var torrents = _proxy.GetTasks(DownloadStationTaskType.BT, Settings);
var serialNumber = _serialNumberProvider.GetSerialNumber(Settings);
var items = new List<DownloadClientItem>();
foreach (var torrent in torrents)
{
var outputPath = new OsPath($"/{torrent.Additional.Detail["destination"]}");
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
{
if (!new OsPath($"/{Settings.TvDirectory}").Contains(outputPath))
{
continue;
}
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.TvCategory))
{
continue;
}
}
var item = new DownloadClientItem()
{
Category = Settings.TvCategory,
DownloadClient = Definition.Name,
DownloadId = CreateDownloadId(torrent.Id, serialNumber),
Title = torrent.Title,
TotalSize = torrent.Size,
RemainingSize = GetRemainingSize(torrent),
RemainingTime = GetRemainingTime(torrent),
Status = GetStatus(torrent),
Message = GetMessage(torrent),
IsReadOnly = !IsFinished(torrent)
};
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
{
item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber);
}
items.Add(item);
}
return items;
}
public override DownloadClientStatus GetStatus()
{
try
{
var path = GetDownloadDirectory();
return new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(path)) }
};
}
catch (DownloadClientException e)
{
_logger.Debug(e, "Failed to get config from Download Station");
throw e;
}
}
public override void RemoveItem(string downloadId, bool deleteData)
{
if (deleteData)
{
DeleteItemData(downloadId);
}
_proxy.RemoveTask(ParseDownloadId(downloadId), Settings);
_logger.Debug("{0} removed correctly", downloadId);
}
protected OsPath GetOutputPath(OsPath outputPath, DownloadStationTask torrent, string serialNumber)
{
var fullPath = _sharedFolderResolver.RemapToFullPath(outputPath, Settings, serialNumber);
var remotePath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, fullPath);
var finalPath = remotePath + torrent.Title;
return finalPath;
}
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
_proxy.AddTaskFromUrl(magnetLink, GetDownloadDirectory(), Settings);
var item = _proxy.GetTasks(DownloadStationTaskType.BT, Settings).SingleOrDefault(t => t.Additional.Detail["uri"] == magnetLink);
if (item != null)
{
_logger.Debug("{0} added correctly", remoteEpisode);
return CreateDownloadId(item.Id, hashedSerialNumber);
}
_logger.Debug("No such task {0} in Download Station", magnetLink);
throw new DownloadClientException("Failed to add magnet task to Download Station");
}
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
{
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
_proxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings);
var items = _proxy.GetTasks(DownloadStationTaskType.BT, Settings).Where(t => t.Additional.Detail["uri"] == Path.GetFileNameWithoutExtension(filename));
var item = items.SingleOrDefault();
if (item != null)
{
_logger.Debug("{0} added correctly", remoteEpisode);
return CreateDownloadId(item.Id, hashedSerialNumber);
}
_logger.Debug("No such task {0} in Download Station", filename);
throw new DownloadClientException("Failed to add torrent task to Download Station");
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
failures.AddIfNotNull(TestOutputPath());
failures.AddIfNotNull(TestGetTorrents());
}
protected bool IsFinished(DownloadStationTask torrent)
{
return torrent.Status == DownloadStationTaskStatus.Finished;
}
protected string GetMessage(DownloadStationTask torrent)
{
if (torrent.StatusExtra != null)
{
if (torrent.Status == DownloadStationTaskStatus.Extracting)
{
return $"Extracting: {int.Parse(torrent.StatusExtra["unzip_progress"])}%";
}
if (torrent.Status == DownloadStationTaskStatus.Error)
{
return torrent.StatusExtra["error_detail"];
}
}
return null;
}
protected DownloadItemStatus GetStatus(DownloadStationTask torrent)
{
switch (torrent.Status)
{
case DownloadStationTaskStatus.Waiting:
return torrent.Size == 0 || GetRemainingSize(torrent) > 0 ? DownloadItemStatus.Queued : DownloadItemStatus.Completed;
case DownloadStationTaskStatus.Paused:
return DownloadItemStatus.Paused;
case DownloadStationTaskStatus.Finished:
case DownloadStationTaskStatus.Seeding:
return DownloadItemStatus.Completed;
case DownloadStationTaskStatus.Error:
return DownloadItemStatus.Failed;
}
return DownloadItemStatus.Downloading;
}
protected long GetRemainingSize(DownloadStationTask torrent)
{
var downloadedString = torrent.Additional.Transfer["size_downloaded"];
long downloadedSize;
if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out downloadedSize))
{
_logger.Debug("Torrent {0} has invalid size_downloaded: {1}", torrent.Title, downloadedString);
downloadedSize = 0;
}
return torrent.Size - Math.Max(0, downloadedSize);
}
protected TimeSpan? GetRemainingTime(DownloadStationTask torrent)
{
var speedString = torrent.Additional.Transfer["speed_download"];
long downloadSpeed;
if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out downloadSpeed))
{
_logger.Debug("Torrent {0} has invalid speed_download: {1}", torrent.Title, speedString);
downloadSpeed = 0;
}
if (downloadSpeed <= 0)
{
return null;
}
var remainingSize = GetRemainingSize(torrent);
return TimeSpan.FromSeconds(remainingSize / downloadSpeed);
}
protected ValidationFailure TestOutputPath()
{
try
{
var downloadDir = GetDownloadDirectory();
if (downloadDir != null)
{
var sharedFolder = downloadDir.Split('\\', '/')[0];
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
if (folderInfo.Additional == null)
{
return new NzbDroneValidationFailure(fieldName, $"Shared folder does not exist")
{
DetailedDescription = $"The DownloadStation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?"
};
}
if (!folderInfo.IsDir)
{
return new NzbDroneValidationFailure(fieldName, $"Folder does not exist")
{
DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'."
};
}
}
return null;
}
catch (Exception ex)
{
_logger.Error(ex);
return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}");
}
}
protected ValidationFailure TestConnection()
{
try
{
return ValidateVersion();
}
catch (DownloadClientAuthenticationException ex)
{
_logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Username", "Authentication failure")
{
DetailedDescription = $"Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration."
};
}
catch (WebException ex)
{
_logger.Error(ex);
if (ex.Status == WebExceptionStatus.ConnectFailure)
{
return new NzbDroneValidationFailure("Host", "Unable to connect")
{
DetailedDescription = "Please verify the hostname and port."
};
}
return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}");
}
catch (Exception ex)
{
_logger.Error(ex);
return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}");
}
}
protected ValidationFailure ValidateVersion()
{
var versionRange = _proxy.GetApiVersion(Settings);
_logger.Debug("Download Station api version information: Min {0} - Max {1}", versionRange.Min(), versionRange.Max());
if (!versionRange.Contains(2))
{
return new ValidationFailure(string.Empty, $"Download Station API version not supported, should be at least 2. It supports from {versionRange.Min()} to {versionRange.Max()}");
}
return null;
}
protected ValidationFailure TestGetTorrents()
{
try
{
_proxy.GetTasks(DownloadStationTaskType.BT, Settings);
return null;
}
catch (Exception ex)
{
return new NzbDroneValidationFailure(string.Empty, $"Failed to get the list of torrents: {ex.Message}");
}
}
protected string ParseDownloadId(string id)
{
return id.Split(':')[1];
}
protected string CreateDownloadId(string id, string hashedSerialNumber)
{
return $"{hashedSerialNumber}:{id}";
}
protected string GetDefaultDir()
{
var config = _proxy.GetConfig(Settings);
var path = config["default_destination"] as string;
return path;
}
protected string GetDownloadDirectory()
{
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
{
return Settings.TvDirectory.TrimStart('/');
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var destDir = GetDefaultDir();
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
}
return null;
}
}
}

View File

@@ -0,0 +1,400 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class UsenetDownloadStation : UsenetClientBase<DownloadStationSettings>
{
protected readonly IDownloadStationProxy _proxy;
protected readonly ISharedFolderResolver _sharedFolderResolver;
protected readonly ISerialNumberProvider _serialNumberProvider;
protected readonly IFileStationProxy _fileStationProxy;
public UsenetDownloadStation(ISharedFolderResolver sharedFolderResolver,
ISerialNumberProvider serialNumberProvider,
IFileStationProxy fileStationProxy,
IDownloadStationProxy proxy,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger
)
: base(httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
_fileStationProxy = fileStationProxy;
_sharedFolderResolver = sharedFolderResolver;
_serialNumberProvider = serialNumberProvider;
}
public override string Name => "Download Station";
public override IEnumerable<DownloadClientItem> GetItems()
{
var nzbTasks = _proxy.GetTasks(DownloadStationTaskType.NZB, Settings);
var serialNumber = _serialNumberProvider.GetSerialNumber(Settings);
var items = new List<DownloadClientItem>();
long totalRemainingSize = 0;
long globalSpeed = nzbTasks.Where(t => t.Status == DownloadStationTaskStatus.Downloading)
.Select(GetDownloadSpeed)
.Sum();
foreach (var nzb in nzbTasks)
{
var outputPath = new OsPath($"/{nzb.Additional.Detail["destination"]}");
var taskRemainingSize = GetRemainingSize(nzb);
if (nzb.Status != DownloadStationTaskStatus.Paused)
{
totalRemainingSize += taskRemainingSize;
}
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
{
if (!new OsPath($"/{Settings.TvDirectory}").Contains(outputPath))
{
continue;
}
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.TvCategory))
{
continue;
}
}
var item = new DownloadClientItem()
{
Category = Settings.TvCategory,
DownloadClient = Definition.Name,
DownloadId = CreateDownloadId(nzb.Id, serialNumber),
Title = nzb.Title,
TotalSize = nzb.Size,
RemainingSize = taskRemainingSize,
Status = GetStatus(nzb),
Message = GetMessage(nzb),
IsReadOnly = !IsFinished(nzb)
};
if (item.Status != DownloadItemStatus.Paused)
{
item.RemainingTime = GetRemainingTime(totalRemainingSize, globalSpeed);
}
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
{
item.OutputPath = GetOutputPath(outputPath, nzb, serialNumber);
}
items.Add(item);
}
return items;
}
protected OsPath GetOutputPath(OsPath outputPath, DownloadStationTask task, string serialNumber)
{
var fullPath = _sharedFolderResolver.RemapToFullPath(outputPath, Settings, serialNumber);
var remotePath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, fullPath);
var finalPath = remotePath + task.Title;
return finalPath;
}
public override DownloadClientStatus GetStatus()
{
try
{
var path = GetDownloadDirectory();
return new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(path)) }
};
}
catch (DownloadClientException e)
{
_logger.Debug(e, "Failed to get config from Download Station");
throw e;
}
}
public override void RemoveItem(string downloadId, bool deleteData)
{
if (deleteData)
{
DeleteItemData(downloadId);
}
_proxy.RemoveTask(ParseDownloadId(downloadId), Settings);
_logger.Debug("{0} removed correctly", downloadId);
}
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
{
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
_proxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings);
var items = _proxy.GetTasks(DownloadStationTaskType.NZB, Settings).Where(t => t.Additional.Detail["uri"] == filename);
var item = items.SingleOrDefault();
if (item != null)
{
_logger.Debug("{0} added correctly", remoteEpisode);
return CreateDownloadId(item.Id, hashedSerialNumber);
}
_logger.Debug("No such task {0} in Download Station", filename);
throw new DownloadClientException("Failed to add NZB task to Download Station");
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
failures.AddIfNotNull(TestOutputPath());
failures.AddIfNotNull(TestGetNZB());
}
protected ValidationFailure TestOutputPath()
{
try
{
var downloadDir = GetDownloadDirectory();
if (downloadDir != null)
{
var sharedFolder = downloadDir.Split('\\', '/')[0];
var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory);
var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings);
if (folderInfo.Additional == null)
{
return new NzbDroneValidationFailure(fieldName, $"Shared folder does not exist")
{
DetailedDescription = $"The DownloadStation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?"
};
}
if (!folderInfo.IsDir)
{
return new NzbDroneValidationFailure(fieldName, $"Folder does not exist")
{
DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'."
};
}
}
return null;
}
catch (Exception ex)
{
_logger.Error(ex);
return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}");
}
}
protected ValidationFailure TestConnection()
{
try
{
return ValidateVersion();
}
catch (DownloadClientAuthenticationException ex)
{
_logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Username", "Authentication failure")
{
DetailedDescription = $"Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration."
};
}
catch (WebException ex)
{
_logger.Error(ex);
if (ex.Status == WebExceptionStatus.ConnectFailure)
{
return new NzbDroneValidationFailure("Host", "Unable to connect")
{
DetailedDescription = "Please verify the hostname and port."
};
}
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex);
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
}
}
protected ValidationFailure ValidateVersion()
{
var versionRange = _proxy.GetApiVersion(Settings);
_logger.Debug("Download Station api version information: Min {0} - Max {1}", versionRange.Min(), versionRange.Max());
if (!versionRange.Contains(2))
{
return new ValidationFailure(string.Empty, $"Download Station API version not supported, should be at least 2. It supports from {versionRange.Min()} to {versionRange.Max()}");
}
return null;
}
protected bool IsFinished(DownloadStationTask task)
{
return task.Status == DownloadStationTaskStatus.Finished;
}
protected string GetMessage(DownloadStationTask task)
{
if (task.StatusExtra != null)
{
if (task.Status == DownloadStationTaskStatus.Extracting)
{
return $"Extracting: {int.Parse(task.StatusExtra["unzip_progress"])}%";
}
if (task.Status == DownloadStationTaskStatus.Error)
{
return task.StatusExtra["error_detail"];
}
}
return null;
}
protected DownloadItemStatus GetStatus(DownloadStationTask task)
{
switch (task.Status)
{
case DownloadStationTaskStatus.Waiting:
return task.Size == 0 || GetRemainingSize(task) > 0 ? DownloadItemStatus.Queued : DownloadItemStatus.Completed;
case DownloadStationTaskStatus.Paused:
return DownloadItemStatus.Paused;
case DownloadStationTaskStatus.Finished:
case DownloadStationTaskStatus.Seeding:
return DownloadItemStatus.Completed;
case DownloadStationTaskStatus.Error:
return DownloadItemStatus.Failed;
}
return DownloadItemStatus.Downloading;
}
protected long GetRemainingSize(DownloadStationTask task)
{
var downloadedString = task.Additional.Transfer["size_downloaded"];
long downloadedSize;
if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out downloadedSize))
{
_logger.Debug("Task {0} has invalid size_downloaded: {1}", task.Title, downloadedString);
downloadedSize = 0;
}
return task.Size - Math.Max(0, downloadedSize);
}
protected long GetDownloadSpeed(DownloadStationTask task)
{
var speedString = task.Additional.Transfer["speed_download"];
long downloadSpeed;
if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out downloadSpeed))
{
_logger.Debug("Task {0} has invalid speed_download: {1}", task.Title, speedString);
downloadSpeed = 0;
}
return Math.Max(downloadSpeed, 0);
}
protected TimeSpan? GetRemainingTime(long remainingSize, long downloadSpeed)
{
if (downloadSpeed > 0)
{
return TimeSpan.FromSeconds(remainingSize / downloadSpeed);
}
else
{
return null;
}
}
protected ValidationFailure TestGetNZB()
{
try
{
_proxy.GetTasks(DownloadStationTaskType.NZB, Settings);
return null;
}
catch (Exception ex)
{
return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of NZBs: " + ex.Message);
}
}
protected string ParseDownloadId(string id)
{
return id.Split(':')[1];
}
protected string CreateDownloadId(string id, string hashedSerialNumber)
{
return $"{hashedSerialNumber}:{id}";
}
protected string GetDefaultDir()
{
var config = _proxy.GetConfig(Settings);
var path = config["default_destination"] as string;
return path;
}
protected string GetDownloadDirectory()
{
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
{
return Settings.TvDirectory.TrimStart('/');
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var destDir = GetDefaultDir();
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
}
return null;
}
}
}

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
public HadoukenSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.Username).NotEmpty()
.WithMessage("Username must not be empty.");

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
public NzbVortexSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.ApiKey).NotEmpty()
.WithMessage("API Key is required");

View File

@@ -32,9 +32,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
{
var category = Settings.TvCategory;
var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings);
var addpaused = Settings.AddPaused;
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, addpaused, Settings);
if (response == null)
{

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
public interface INzbgetProxy
{
string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings);
string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings);
NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings);
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings);
@@ -45,12 +45,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return version >= minimumVersion;
}
public string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings)
public string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings)
{
if (HasVersion(16, settings))
{
var droneId = Guid.NewGuid().ToString().Replace("-", "");
var response = ProcessRequest<int>(settings, "append", title, nzbData, category, priority, false, false, string.Empty, 0, "all", new string[] { "drone", droneId });
var response = ProcessRequest<int>(settings, "append", title, nzbData, category, priority, false, addpaused, string.Empty, 0, "all", new string[] { "drone", droneId });
if (response <= 0)
{
return null;

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public NzbgetSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password));
RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username));
@@ -55,6 +55,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public QBittorrentSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(0, 65535);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
}
// patch can be a number (releases) or 'x' (git)
private static readonly Regex VersionRegex = new Regex(@"(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+|x)(?<candidate>.*)", RegexOptions.Compiled);
private static readonly Regex VersionRegex = new Regex(@"(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+|x)", RegexOptions.Compiled);
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
{
@@ -269,110 +269,103 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
failures.AddIfNotNull(TestCategory());
}
private bool HasVersion(int major, int minor, int patch = 0, string candidate = null)
private bool HasVersion(int major, int minor, int patch = 0)
{
candidate = candidate ?? string.Empty;
var rawVersion = _proxy.GetVersion(Settings);
var version = ParseVersion(rawVersion);
var version = _proxy.GetVersion(Settings);
if (version == null)
{
return false;
}
if (version.Major > major)
{
return true;
}
else if (version.Major < major)
{
return false;
}
if (version.Minor > minor)
{
return true;
}
else if (version.Minor < minor)
{
return false;
}
if (version.Build > patch)
{
return true;
}
else if (version.Build < patch)
{
return false;
}
return true;
}
private Version ParseVersion(string version)
{
var parsed = VersionRegex.Match(version);
int actualMajor;
int actualMinor;
int actualPatch;
string actualCandidate;
int major;
int minor;
int patch;
if (!parsed.Success)
if (parsed.Success)
{
major = Convert.ToInt32(parsed.Groups["major"].Value);
minor = Convert.ToInt32(parsed.Groups["minor"].Value);
patch = Convert.ToInt32(parsed.Groups["patch"].Value.Replace("x", "0"));
}
else
{
if (!version.Equals("develop", StringComparison.InvariantCultureIgnoreCase))
{
return false;
return null;
}
actualMajor = 1;
actualMinor = 1;
actualPatch = 0;
actualCandidate = null;
major = 1;
minor = 1;
patch = 0;
}
else
{
actualMajor = Convert.ToInt32(parsed.Groups["major"].Value);
actualMinor = Convert.ToInt32(parsed.Groups["minor"].Value);
actualPatch = Convert.ToInt32(parsed.Groups["patch"].Value.Replace("x", ""));
actualCandidate = parsed.Groups["candidate"].Value.ToUpper();
}
if (actualMajor > major)
{
return true;
}
else if (actualMajor < major)
{
return false;
}
if (actualMinor > minor)
{
return true;
}
else if (actualMinor < minor)
{
return false;
}
if (actualPatch > patch)
{
return true;
}
else if (actualPatch < patch)
{
return false;
}
if (actualCandidate.IsNullOrWhiteSpace())
{
return true;
}
else if (candidate.IsNullOrWhiteSpace())
{
return false;
}
else
{
return actualCandidate.CompareTo(candidate) > 0;
}
return new Version(major, minor, patch);
}
private ValidationFailure TestConnectionAndVersion()
{
try
{
var version = _proxy.GetVersion(Settings);
var parsed = VersionRegex.Match(version);
var rawVersion = _proxy.GetVersion(Settings);
var version = ParseVersion(rawVersion);
if (!parsed.Success)
if (version == null)
{
if (version.Equals("develop", StringComparison.InvariantCultureIgnoreCase))
{
return new NzbDroneValidationFailure("Version", "Sabnzbd develop version, assuming version 1.1.0 or higher.")
{
IsWarning = true,
DetailedDescription = "Sonarr may not be able to support new features added to SABnzbd when running develop versions."
};
}
return new ValidationFailure("Version", "Unknown Version: " + version);
}
var major = Convert.ToInt32(parsed.Groups["major"].Value);
var minor = Convert.ToInt32(parsed.Groups["minor"].Value);
if (rawVersion.Equals("develop", StringComparison.InvariantCultureIgnoreCase))
{
return new NzbDroneValidationFailure("Version", "Sabnzbd develop version, assuming version 1.1.0 or higher.")
{
IsWarning = true,
DetailedDescription = "Sonarr may not be able to support new features added to SABnzbd when running develop versions."
};
}
if (major >= 1)
if (version.Major >= 1)
{
return null;
}
if (minor >= 7)
if (version.Minor >= 7)
{
return null;
}

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public SabnzbdSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.ApiKey).NotEmpty()
.WithMessage("API Key is required when username/password are not configured")

View File

@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public TransmissionSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.UrlBase).ValidUrlBase();

View File

@@ -72,8 +72,8 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
var tries = 2;
var retryDelay = 100;
var tries = 5;
var retryDelay = 200;
if (WaitForTorrent(hash, tries, retryDelay))
{
_proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public RTorrentSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(0, 65535);
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.TvCategory).NotEmpty()
.WithMessage("A category is recommended")
.AsWarning();

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