1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-21 16:54:30 -04:00

Compare commits

...

87 Commits

Author SHA1 Message Date
Mark McDowall
2b72c0e328 Fixed: Manual Import Series selection
Closes #996
2016-01-09 13:16:34 -08:00
Mark McDowall
536aa350f0 Merge pull request #960 from Sonarr/nzb-vortex
NZBVortex Download Client
2016-01-07 15:53:57 -08:00
Mark McDowall
7c382c0e0c NZBVortex Download Client
New: NZBVortex Download Client
Closes #360
2016-01-07 15:50:52 -08:00
Mark McDowall
dda0d3259f Fixed broken test 2016-01-07 15:49:51 -08:00
Mark McDowall
a96718f7b3 Fixed Twitter notifications
New: Twitter notifications now require a Twitter (see settings for details)

Closes #1049
2016-01-06 22:32:12 -08:00
Mark McDowall
7ca67fe57a New: Special searching on RARBG 2016-01-05 23:07:49 -08:00
Mark McDowall
8373024f9d Fixed: Parsing of queued specials from download client queue 2016-01-05 22:56:34 -08:00
Mark McDowall
b62ef0c40c Fixed: Use folder quality when better than file quality (regression)
Closes #1022
2016-01-04 23:12:14 -08:00
Mark McDowall
376481eda5 Merge pull request #981 from Sonarr/ical-parameters
iCal parameters
2016-01-02 10:31:12 -08:00
Mark McDowall
800fa42982 Selectable range for iCal
New: Support for pastDays and fututeDays query parameters on iCal requests
Closes #974
2016-01-02 10:21:06 -08:00
Taloth Saldono
4e728c3a02 Increased timeout for Deluge to prevent timeout errors when posting large torrent files. 2015-12-30 22:50:17 +01:00
Taloth Saldono
663d254ced Fixed Ospath incorrectly detecting arbitrary colon as windows path. 2015-12-30 21:01:29 +01:00
vawen
8753c232c7 Fix: Paths with colons prevent Sonarr from communicating with Transmission
Issue #954
2015-12-29 13:25:36 +01:00
Taloth Saldono
221f3ef08c Safety net to handle MaxSize=0. Some users still have 0 = unlimited in their db and migration failed for them. 2015-12-28 20:24:24 +01:00
Taloth Saldono
b59175a87c Fixed: Indexer sites returning date as Retry-After header.
fixes #994
2015-12-28 20:24:23 +01:00
Taloth Saldono
37c621dcdb Fixed: Health Check produced warning if Sonarr binaries folder was not writable even when the external script update mechanism was selected.
fixes #964
2015-12-28 20:24:22 +01:00
Taloth Saldono
eaf3228bb7 Fixed Regex mistake in CleanLogMessage. 2015-12-28 20:24:21 +01:00
Jake Pusateri
c8debbf470 New: Better resolution posters on retina screens. 2015-12-28 11:12:12 -08:00
Mark McDowall
8dcd8d17b5 Fixed: Username must not be null or empty when logging in 2015-12-27 00:59:28 -08:00
Mark McDowall
cfe121c777 Don't return series as subtype for /api/episode
Fixed: Sped up loading episodes from server
2015-12-27 00:45:20 -08:00
Mark McDowall
b4f83d8a4e New: Media file extension .webm 2015-12-25 18:35:42 -08:00
Mark McDowall
9039d7e694 Fixed: Show a better error message when no episodes are parsed in a release 2015-12-25 18:32:03 -08:00
Taloth Saldono
7a25717da6 Fixed donate button. 2015-12-24 19:51:36 +01:00
Taloth Saldono
7e1c444c02 Fixed: Curl Fallback should ignore invalid cookies. 2015-12-24 19:30:16 +01:00
Mark McDowall
dc3f7c9bda Fixed: Improved parsing for single digit multi-episode titles
Closes #965
2015-12-20 00:11:41 -08:00
Mark McDowall
de754169fb Fixed: RSS Sync Interval validation 2015-12-19 18:02:38 -08:00
Mark McDowall
2d3c3bbb0c Changed torrent blackhole message 2015-12-19 17:49:54 -08:00
Mark McDowall
15cefe4a43 New: Option to Hardlink or Copy instead of move for Torrent Blackhole
Closes #1011
2015-12-19 17:18:22 -08:00
Mark McDowall
95da301975 A few UI Fixes
Closes #1009
Closes #1010
2015-12-18 23:41:30 -08:00
Mark McDowall
e03906b294 Fixed: Hardlink/Copy files from QBittorrent 2015-12-17 22:16:38 -08:00
Mark McDowall
7921dd3f96 Fixed: Removed Titans of TV tracker
Closes #992
2015-12-17 00:46:02 -08:00
Mark McDowall
9f066f7a6b New: Newznab preset for Usenet Crawler 2015-12-17 00:24:51 -08:00
Mark McDowall
81d131e732 Merge pull request #779 from cbodley/qbittorrent
Download client for qBittorrent
2015-12-15 23:10:20 -08:00
Casey Bodley
0552b56b71 qbittorrent: client plugin based heavily on uTorrent
supports a minimum qBittorrent version of 3.2.4, and uses labels for
v3.3.0 and later

Signed-off-by: Casey Bodley <cbodley@gmail.com>
2015-12-14 00:20:14 -05:00
Keivan Beigi
3952ee402b Update readme.md 2015-12-12 11:24:46 -08:00
Mark McDowall
0b3e27cb44 Don't keep dylibs for WIndows and Linux builds 2015-12-09 23:24:50 -08:00
Mark McDowall
4fa4b3507e Fixed: Force grabbing some delayed releases
Closes #984
2015-12-08 22:50:34 -08:00
Mark McDowall
8c211364e2 Fixed: Improved parsing of some multi-episode filenames 2015-12-08 15:26:52 -08:00
Mark McDowall
2d9917d074 Re-order regex to prefer [1x01] over 101 2015-12-06 11:03:11 -08:00
Mark McDowall
d514699ab7 Fixed: Prevent series from being added with an invalid Profile ID
Closes #977
2015-12-05 02:22:22 -08:00
Mark McDowall
dc176a83b3 Update CONTRIBUTING.md 2015-12-01 08:48:41 -08:00
Mark McDowall
69e3516a89 New: Allow Uppercase in Transmission category
Closes #934
2015-11-29 22:01:20 -08:00
Mark McDowall
c8a0f9fa7a Fixed: Saving settings changes 2015-11-26 12:05:37 -08:00
Mark McDowall
c2b9504b15 Merge pull request #931 from Dahlgren/osx-development
Include mediainfo and sqlite3 libraries for Mac
2015-11-24 18:35:07 -08:00
Mark McDowall
2693a3df2e Merge pull request #959 from roguecode/develop
Fixed: Indexer failure log message with local time
2015-11-24 15:41:57 -08:00
Matt
8062466ab8 Changing Indexer failure log message to local from UTC. 2015-11-24 23:42:20 +02:00
Björn Dahlgren
6cde1dd5ae Include mediainfo and sqlite3 libraries for Mac
Enables usage within MonoDevelop and Xamarin Studio including NUnit
2015-11-24 10:21:42 +01:00
Mark McDowall
b6c4a97675 Merge pull request #889 from Sonarr/quality-source
Folder quality when file quality determined by its extension
2015-11-23 23:01:10 -08:00
Mark McDowall
a9444cef30 Fixed: Folder quality when file quality determined by its extension
Closes #603
2015-11-23 23:00:51 -08:00
Mark McDowall
bf217a7093 Merge pull request #754 from Sonarr/real-releases
Support for REAL releases
2015-11-23 22:59:41 -08:00
Mark McDowall
b6b5355261 New: support for REAL releases
Closes #453

New: Added `Quality Real` naming Token
New: Quality Full will add real to file name when applicable
2015-11-23 22:58:53 -08:00
Mark McDowall
bc37084ec4 Merge pull request #928 from Dahlgren/mono-tests
Fixed tests for Mono
2015-11-23 22:50:34 -08:00
Mark McDowall
0a1a30f2af Merge pull request #953 from zetas/nn_preset_nzbcat
New: Newznab Preset for NZBCat
2015-11-23 22:50:15 -08:00
Keivan Beigi
7e023a7944 ConfigServiceFixture shouldn't be touching the DB. 2015-11-23 21:57:01 -08:00
zetas
91f68de8a7 Adding new newznab preset for NZBCat 2015-11-22 07:09:57 -05:00
Björn Dahlgren
994e2a6c57 Fixed failing tests on Mono
Test case unicode characters in escaped format
2015-11-22 01:11:43 +01:00
Mark McDowall
04da2d845a Merge pull request #941 from uzegonemad/hotfix/calendar-legend-width
Give calendar legend ul max width of 100%. Fixes #922
2015-11-18 22:12:39 -08:00
Benjamin Uzelac
d3b87bc3e8 give legend ul max width of 100%
give legend ul max width of 100%
2015-11-18 22:17:05 -06:00
Mark McDowall
554c81f251 Merge pull request #901 from brgaulin/jquery_upgrade
Update jQuery to 1.11.3
2015-11-16 13:44:42 -08:00
Mark McDowall
6de3f9dd0b Merge pull request #942 from uzegonemad/hotfix/calendar-tooltip
Fixed: Hidden calendar tooltips
2015-11-16 13:38:30 -08:00
Mark McDowall
e9692d5b9c Merge pull request #940 from uzegonemad/hotfix/fix-long-path-label
Fixed: Add wrapping to fix long paths in labels
2015-11-16 13:37:26 -08:00
Benjamin Uzelac
1a74990e9b alter calendar tooltip container. fixes #857 2015-11-15 20:50:17 -06:00
Benjamin Uzelac
3b9ac8699d Add wrapping to fix long paths in labels. Fixes #875 2015-11-15 18:25:30 -06:00
Mark McDowall
ea6ae85f7a Fixed: Logging invalid version when failing to connect to Kodi
Closes #927
2015-11-14 22:35:45 -08:00
Mark McDowall
b02b9f026f Fixed: Failing missing episode search when one search fails
Closes #917
2015-11-14 22:35:28 -08:00
Mark McDowall
c419e7b710 Merge pull request #915 from uzegonemad/hotfix/scroll-up-zindex
Fix z-index on scroll to top button
2015-11-14 21:43:37 -08:00
Benjamin Uzelac
cd9132520d fix z-index on scroll to top button 2015-11-05 19:43:53 -06:00
Mark McDowall
08d19df3f7 Fixed: Manual import when quality was not available after failed parsing
Closes #911
2015-11-03 15:12:56 -08:00
Mark McDowall
b34879b4f6 New: Manual search shows error when download fails 2015-11-02 22:46:06 -08:00
Mark McDowall
6b9c4af591 Fixed: Magnet links with torrent blackhole 2015-11-02 22:21:27 -08:00
Mark McDowall
c00c207517 Prevent regrab for all grabs
Fixed: Prevent incorrectly grabbing a similar or identical release for 12 hours
2015-11-01 21:54:49 -08:00
Mark McDowall
6f7fea3591 PFMonkey Newznab preset 2015-11-01 21:35:44 -08:00
Mark McDowall
83eebfe153 Fixed: Stricter parsing of some release filenames 2015-10-29 23:58:56 -07:00
Gaulin, Brendan
fce3f86be7 UI: Update jQuery to 1.11.3 2015-10-29 08:38:21 -04:00
Mark McDowall
2d42c59d70 Fixed: Log download client name when communication fails 2015-10-27 22:19:24 -07:00
Mark McDowall
f0933b9786 Fixed: Test messaging when indexer API returns an error with a message 2015-10-27 22:04:20 -07:00
Mark McDowall
e4e687c2a4 Fixed: Parsing anime series with number in title
Closes #898
2015-10-26 14:56:28 -07:00
Taloth Saldono
44de353b8b Sanitize dognzb apikey in nzb fetch url. 2015-10-25 23:15:21 +01:00
Taloth Saldono
aac4938598 Fixed handling cookies in different system languages.
fixes #896... hopefully
2015-10-25 22:57:37 +01:00
Mark McDowall
d37b24cd0b Better UI messaging when searching for all specials in a series
Fixed: Specials season search UI messaging
Closes #881
2015-10-25 10:10:57 -07:00
Taloth Saldono
c9a36fe4b2 Fixed sorting on Progress in Queue.
Fixes #882
2015-10-22 23:19:24 +02:00
Taloth Saldono
f01a21ce43 Degraded 101 regex to favour S01 regex to match prevent matching 3 digit series title.
Fixes #885
2015-10-22 23:19:22 +02:00
Taloth Saldono
cc72699b8a Fixed: Added verified file transfer mode that doesn't revert to copy. 2015-10-22 23:19:21 +02:00
Mark McDowall
04de0049fe Don't try to process a download client item with an invalid path for the OS 2015-10-21 15:06:54 -07:00
Taloth Saldono
330554edb0 Fixed: External links again open in new windows. 2015-10-20 20:58:56 +02:00
Taloth Saldono
a06a3fa5d6 Fixed: Removal of common suffixes such as [ettv] while parsing.
Fixes #874
2015-10-20 20:12:35 +02:00
Taloth Saldono
e8d6d62fba Warning message when BTN API throw internal server error 2015-10-20 19:31:40 +02:00
150 changed files with 13022 additions and 10666 deletions

View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{Sonarr node_modules}" />
<includedPredefinedLibrary name="ECMAScript 6" />
</component>
</project>

View File

@@ -16,7 +16,7 @@ Setup guides, FAQ, the more information we have on the wiki the better.
### Getting started ###
1. Fork Sonarr
2. Clone (develop branch)
2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)*
3. Run `npm install`
4. Run `gulp watch` - Used to compile the UI components and copy them (leave this window open)
5. Compile in Visual Studio

View File

@@ -33,6 +33,7 @@ Function Build()
Write-Host "Removing Mono.Posix.dll"
Remove-Item "$outputFolder\Mono.Posix.dll"
Get-ChildItem $outputFolder -File -Filter "*.dylib" -Recurse | foreach ($_) {Remove-Item $_.Fullname}
Write-Host "##teamcity[progressFinish 'Build']"
}
@@ -233,6 +234,9 @@ Function RunGulp()
Invoke-Expression 'gulp build' -ErrorAction Continue -Verbose
CheckExitCode
Invoke-Expression 'gulp build --phantom' -ErrorAction Continue -Verbose
CheckExitCode
Write-Host "##teamcity[progressFinish 'Running Gulp']"
}

View File

@@ -21,7 +21,7 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
## Configuring Development Environment: ##
### Requirements ###
- Visual Studio 2013 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
- 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/)
- [Gulp](http://gulpjs.com)

View File

@@ -3,6 +3,7 @@ using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
using Nancy.ModelBinding;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
@@ -23,6 +24,11 @@ namespace NzbDrone.Api.Authentication
private Response Login(LoginResource resource)
{
Ensure.That(resource.Username, () => resource.Username).IsNotNullOrWhiteSpace();
// TODO: A null or empty password should not be allowed, uncomment in v3
//Ensure.That(resource.Password, () => resource.Password).IsNotNullOrWhiteSpace();
var user = _userService.FindUser(resource.Username, resource.Password);
if (user == null)

View File

@@ -22,15 +22,32 @@ namespace NzbDrone.Api.Calendar
private Response GetCalendarFeed()
{
var start = DateTime.Today.AddDays(-7);
var end = DateTime.Today.AddDays(28);
var pastDays = 7;
var futureDays = 28;
var start = DateTime.Today.AddDays(-pastDays);
var end = DateTime.Today.AddDays(futureDays);
// TODO: Remove start/end parameters in v3, they don't work well for iCal
var queryStart = Request.Query.Start;
var queryEnd = Request.Query.End;
var queryPastDays = Request.Query.PastDays;
var queryFutureDays = Request.Query.FutureDays;
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
if (queryPastDays.HasValue)
{
pastDays = int.Parse(queryPastDays.Value);
start = DateTime.Today.AddDays(-pastDays);
}
if (queryFutureDays.HasValue)
{
futureDays = int.Parse(queryFutureDays.Value);
end = DateTime.Today.AddDays(futureDays);
}
var episodes = _episodeService.EpisodesBetweenDates(start, end, false);
var icalCalendar = new iCalendar();

View File

@@ -1,4 +1,5 @@
using FluentValidation;
using NzbDrone.Api.Validation;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Api.Config
@@ -16,8 +17,7 @@ namespace NzbDrone.Api.Config
.GreaterThanOrEqualTo(0);
SharedValidator.RuleFor(c => c.RssSyncInterval)
.InclusiveBetween(10, 120)
.When(c => c.RssSyncInterval > 0);
.IsValidRssSyncInterval();
}
}
}

View File

@@ -36,5 +36,10 @@ namespace NzbDrone.Api.Episodes
{
_episodeService.SetEpisodeMonitored(episodeResource.Id, episodeResource.Monitored);
}
protected override List<EpisodeResource> LoadSeries(List<EpisodeResource> resources)
{
return resources;
}
}
}
}

View File

@@ -79,7 +79,8 @@ namespace NzbDrone.Api.Episodes
{
var resources = base.ToListResource(modelList);
return resources.LoadSubtype<EpisodeResource, SeriesResource, Core.Tv.Series>(e => e.SeriesId, _seriesService.GetSeries).ToList();
return LoadSeries(resources);
}
public void Handle(EpisodeGrabbedEvent message)
@@ -100,5 +101,10 @@ namespace NzbDrone.Api.Episodes
BroadcastResourceChange(ModelAction.Updated, episode.Id);
}
}
protected virtual List<EpisodeResource> LoadSeries(List<EpisodeResource> resources)
{
return resources.LoadSubtype<EpisodeResource, SeriesResource, Core.Tv.Series>(e => e.SeriesId, _seriesService.GetSeries).ToList();
}
}
}
}

View File

@@ -30,10 +30,13 @@ namespace NzbDrone.Api.ManualImport
private ManualImportResource AddQualityWeight(ManualImportResource item)
{
item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight;
item.QualityWeight += item.Quality.Revision.Real * 10;
item.QualityWeight += item.Quality.Revision.Version;
if (item.Quality != null)
{
item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight;
item.QualityWeight += item.Quality.Revision.Real * 10;
item.QualityWeight += item.Quality.Revision.Version;
}
return item;
}
}

View File

@@ -238,6 +238,7 @@
<Compile Include="TinyIoCNancyBootstrapper.cs" />
<Compile Include="Update\UpdateModule.cs" />
<Compile Include="Update\UpdateResource.cs" />
<Compile Include="Validation\RssSyncIntervalValidator.cs" />
<Compile Include="Validation\EmptyCollectionValidator.cs" />
<Compile Include="Validation\RuleBuilderExtensions.cs" />
<Compile Include="Wanted\CutoffModule.cs" />

View File

@@ -10,11 +10,11 @@ using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.SeriesStats;
using NzbDrone.Core.Tv;
using NzbDrone.Api.Validation;
using NzbDrone.Api.Mapping;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Validation;
using NzbDrone.SignalR;
namespace NzbDrone.Api.Series
@@ -43,7 +43,8 @@ namespace NzbDrone.Api.Series
SeriesPathValidator seriesPathValidator,
SeriesExistsValidator seriesExistsValidator,
DroneFactoryValidator droneFactoryValidator,
SeriesAncestorValidator seriesAncestorValidator
SeriesAncestorValidator seriesAncestorValidator,
ProfileExistsValidator profileExistsValidator
)
: base(signalRBroadcaster)
{
@@ -59,7 +60,7 @@ namespace NzbDrone.Api.Series
UpdateResource = UpdateSeries;
DeleteResource = DeleteSeries;
SharedValidator.RuleFor(s => s.ProfileId).ValidId();
Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId));
SharedValidator.RuleFor(s => s.Path)
.Cascade(CascadeMode.StopOnFirstFailure)
@@ -70,6 +71,8 @@ namespace NzbDrone.Api.Series
.SetValidator(seriesAncestorValidator)
.When(s => !s.Path.IsNullOrWhiteSpace());
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.Title).NotEmpty();

View File

@@ -0,0 +1,34 @@
using FluentValidation.Validators;
namespace NzbDrone.Api.Validation
{
public class RssSyncIntervalValidator : PropertyValidator
{
public RssSyncIntervalValidator()
: base("Must be between 10 and 120 or 0 to disable")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null)
{
return true;
}
var value = (int)context.PropertyValue;
if (value == 0)
{
return true;
}
if (value >= 10 && value <= 120)
{
return true;
}
return false;
}
}
}

View File

@@ -31,5 +31,10 @@ namespace NzbDrone.Api.Validation
{
return ruleBuilder.SetValidator(new EmptyCollectionValidator<TProp>());
}
public static IRuleBuilderOptions<T, int> IsValidRssSyncInterval<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new RssSyncIntervalValidator());
}
}
}

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Common.Test.DiskTests
{
MonoOnly();
Subject.VerificationMode.Should().Be(DiskTransferVerificationMode.Transactional);
Subject.VerificationMode.Should().Be(DiskTransferVerificationMode.TryTransactional);
}
[Test]
@@ -199,9 +199,6 @@ namespace NzbDrone.Common.Test.DiskTests
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.CopyFile(_sourcePath, _targetPath, false), Times.Once());
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetFileSize(It.IsAny<string>()), Times.Never());
}
[Test]
@@ -213,9 +210,6 @@ namespace NzbDrone.Common.Test.DiskTests
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.MoveFile(_sourcePath, _targetPath, false), Times.Once());
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetFileSize(It.IsAny<string>()), Times.Never());
}
[Test]
@@ -377,6 +371,52 @@ namespace NzbDrone.Common.Test.DiskTests
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void mode_transactional_should_move_and_verify_if_same_folder()
{
Subject.VerificationMode = DiskTransferVerificationMode.Transactional;
var targetPath = _sourcePath + ".test";
var result = Subject.TransferFile(_sourcePath, targetPath, TransferMode.Move);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.TryCreateHardLink(_sourcePath, _backupPath), Times.Never());
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.CopyFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.MoveFile(_sourcePath, targetPath, false), Times.Once());
}
[Test]
public void mode_trytransactional_should_revert_to_verifyonly_if_hardlink_fails()
{
Subject.VerificationMode = DiskTransferVerificationMode.TryTransactional;
WithFailedHardlink();
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFile(_sourcePath, _targetPath, false))
.Callback(() =>
{
WithExistingFile(_sourcePath, false);
WithExistingFile(_targetPath, true);
});
var result = Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Move);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.TryCreateHardLink(_sourcePath, _backupPath), Times.Once());
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.CopyFile(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.MoveFile(_sourcePath, _targetPath, false), Times.Once());
}
[Test]
public void mode_transactional_should_delete_old_backup_on_move()
{

View File

@@ -1,17 +1,19 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Threading;
using FluentAssertions;
using Moq;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.TPL;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories;
using NLog;
using NzbDrone.Common.TPL;
using Moq;
using NzbDrone.Common.Http.Dispatchers;
namespace NzbDrone.Common.Test.Http
{
@@ -309,30 +311,70 @@ namespace NzbDrone.Common.Test.Http
.Verify(v => v.PostResponse(It.IsAny<HttpResponse>()), Times.Once());
}
public void should_parse_malformed_cloudflare_cookie()
[TestCase("en-US")]
[TestCase("es-ES")]
public void should_parse_malformed_cloudflare_cookie(string culture)
{
// the date is bad in the below - should be 13-Jul-2016
string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Wed, 13-Jul-16 16:19:50 GMT; path=/; HttpOnly";
string url = "http://eu.httpbin.org/response-headers?Set-Cookie=" +
System.Uri.EscapeUriString(malformedCookie);
var origCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
try
{
// the date is bad in the below - should be 13-Jul-2016
string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Wed, 13-Jul-16 16:19:50 GMT; path=/; HttpOnly";
string url = "http://eu.httpbin.org/response-headers?Set-Cookie=" +
System.Uri.EscapeUriString(malformedCookie);
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest("http://eu.httpbin.org/get");
var request = new HttpRequest("http://eu.httpbin.org/get");
var response = Subject.Get<HttpBinResource>(request);
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
response.Resource.Headers.Should().ContainKey("Cookie");
var cookie = response.Resource.Headers["Cookie"].ToString();
var cookie = response.Resource.Headers["Cookie"].ToString();
cookie.Should().Contain("__cfduid=d29e686a9d65800021c66faca0a29b4261436890790");
cookie.Should().Contain("__cfduid=d29e686a9d65800021c66faca0a29b4261436890790");
ExceptionVerification.IgnoreErrors();
ExceptionVerification.IgnoreErrors();
}
finally
{
Thread.CurrentThread.CurrentCulture = origCulture;
Thread.CurrentThread.CurrentUICulture = origCulture;
}
}
[TestCase("lang_code=en; expires=Fri, 23-Dec-2016 18:09:14 GMT; Max-Age=31536000; path=/; domain=.abc.com")]
public void should_reject_malformed_domain_cookie(string malformedCookie)
{
try
{
// the date is bad in the below - should be 13-Jul-2016
string url = "http://eu.httpbin.org/response-headers?Set-Cookie=" + Uri.EscapeUriString(malformedCookie);
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest("http://eu.httpbin.org/get");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().NotContainKey("Cookie");
ExceptionVerification.IgnoreErrors();
}
finally
{
}
}
}

View File

@@ -13,6 +13,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"http://rss.torrentleech.org/mySecret")]
[TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
[TestCase(@"https://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user=sonarr&api=mySecret&eng=1")]
[TestCase(@"https://dognzb.cr/fetch/2b51db35e1912ffc138825a12b9933d2/2b51db35e1910123321025a12b9933d2")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]
@@ -44,6 +45,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("01233210");
}
}
}

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Common.Test
[TestCase("/rooted/linux/path", OsPathKind.Unix)]
[TestCase("/", OsPathKind.Unix)]
[TestCase("linux/path", OsPathKind.Unix)]
[TestCase(@"Castle:unrooted+linux+path", OsPathKind.Unknown)]
public void should_auto_detect_kind(string path, OsPathKind kind)
{
var result = new OsPath(path);
@@ -94,6 +95,8 @@ namespace NzbDrone.Common.Test
[TestCase(@"rooted\windows\path")]
[TestCase(@"path")]
[TestCase("linux/path")]
[TestCase(@"Castle:unrooted+linux+path")]
[TestCase(@"C:unrooted+linux+path")]
public void should_detect_unrooted_ospaths(string path)
{
var osPath = new OsPath(path);

View File

@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Disk
{
None,
VerifyOnly,
TryTransactional,
Transactional
}
@@ -40,7 +41,7 @@ namespace NzbDrone.Common.Disk
// TODO: Atm we haven't seen partial transfers on windows so we disable verified transfer.
// (If enabled in the future, be sure to check specifically for ReFS, which doesn't support hardlinks.)
VerificationMode = OsInfo.IsWindows ? DiskTransferVerificationMode.VerifyOnly : DiskTransferVerificationMode.Transactional;
VerificationMode = OsInfo.IsWindows ? DiskTransferVerificationMode.VerifyOnly : DiskTransferVerificationMode.TryTransactional;
}
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode, bool verified = true)
@@ -48,11 +49,6 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
if (VerificationMode != DiskTransferVerificationMode.Transactional)
{
verified = false;
}
if (!_diskProvider.FolderExists(targetPath))
{
_diskProvider.CreateFolder(targetPath);
@@ -85,12 +81,14 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
if (VerificationMode != DiskTransferVerificationMode.Transactional)
if (VerificationMode != DiskTransferVerificationMode.Transactional && VerificationMode != DiskTransferVerificationMode.TryTransactional)
{
verified = false;
}
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
var originalSize = _diskProvider.GetFileSize(sourcePath);
if (sourcePath == targetPath)
{
@@ -127,6 +125,15 @@ namespace NzbDrone.Common.Disk
return TransferMode.None;
}
if (sourcePath.GetParentPath() == targetPath.GetParentPath())
{
if (mode.HasFlag(TransferMode.Move))
{
TryMoveFileVerified(sourcePath, targetPath, originalSize);
return TransferMode.Move;
}
}
if (sourcePath.IsParentPath(targetPath))
{
throw new IOException(string.Format("Destination cannot be a child of the source [{0}] => [{1}]", sourcePath, targetPath));
@@ -151,7 +158,7 @@ namespace NzbDrone.Common.Disk
{
if (mode.HasFlag(TransferMode.Copy))
{
if (TryCopyFile(sourcePath, targetPath))
if (TryCopyFileTransactional(sourcePath, targetPath, originalSize))
{
return TransferMode.Copy;
}
@@ -159,7 +166,7 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
if (TryMoveFile(sourcePath, targetPath))
if (TryMoveFileTransactional(sourcePath, targetPath, originalSize))
{
return TransferMode.Move;
}
@@ -167,50 +174,18 @@ namespace NzbDrone.Common.Disk
throw new IOException(string.Format("Failed to completely transfer [{0}] to [{1}], aborting.", sourcePath, targetPath));
}
else if (VerificationMode == DiskTransferVerificationMode.VerifyOnly)
else if (VerificationMode != DiskTransferVerificationMode.None)
{
var originalSize = _diskProvider.GetFileSize(sourcePath);
if (mode.HasFlag(TransferMode.Copy))
{
try
{
_diskProvider.CopyFile(sourcePath, targetPath);
var targetSize = _diskProvider.GetFileSize(targetPath);
if (targetSize != originalSize)
{
throw new IOException(string.Format("File copy incomplete. [{0}] was {1} bytes long instead of {2} bytes.", targetPath, targetSize, originalSize));
}
return TransferMode.Copy;
}
catch
{
RollbackCopy(sourcePath, targetPath);
throw;
}
TryCopyFileVerified(sourcePath, targetPath, originalSize);
return TransferMode.Copy;
}
if (mode.HasFlag(TransferMode.Move))
{
try
{
_diskProvider.MoveFile(sourcePath, targetPath);
var targetSize = _diskProvider.GetFileSize(targetPath);
if (targetSize != originalSize)
{
throw new IOException(string.Format("File copy incomplete, data loss may have occured. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize));
}
return TransferMode.Move;
}
catch
{
RollbackPartialMove(sourcePath, targetPath);
throw;
}
TryMoveFileVerified(sourcePath, targetPath, originalSize);
return TransferMode.Move;
}
}
else
@@ -310,10 +285,8 @@ namespace NzbDrone.Common.Disk
Thread.Sleep(3000);
}
private bool TryCopyFile(string sourcePath, string targetPath)
private bool TryCopyFileTransactional(string sourcePath, string targetPath, long originalSize)
{
var originalSize = _diskProvider.GetFileSize(sourcePath);
var tempTargetPath = targetPath + ".partial~";
if (_diskProvider.FileExists(tempTargetPath))
@@ -367,10 +340,8 @@ namespace NzbDrone.Common.Disk
return false;
}
private bool TryMoveFile(string sourcePath, string targetPath)
private bool TryMoveFileTransactional(string sourcePath, string targetPath, long originalSize)
{
var originalSize = _diskProvider.GetFileSize(sourcePath);
var backupPath = sourcePath + ".backup~";
var tempTargetPath = targetPath + ".partial~";
@@ -423,16 +394,63 @@ namespace NzbDrone.Common.Disk
}
}
_logger.Trace("Hardlink move failed, reverting to copy.");
if (TryCopyFile(sourcePath, targetPath))
if (VerificationMode == DiskTransferVerificationMode.Transactional)
{
_logger.Trace("Copy succeeded, deleting source.");
_diskProvider.DeleteFile(sourcePath);
_logger.Trace("Hardlink move failed, reverting to copy.");
if (TryCopyFileTransactional(sourcePath, targetPath, originalSize))
{
_logger.Trace("Copy succeeded, deleting source.");
_diskProvider.DeleteFile(sourcePath);
return true;
}
}
else
{
_logger.Trace("Hardlink move failed, reverting to move.");
TryMoveFileVerified(sourcePath, targetPath, originalSize);
return true;
}
_logger.Trace("Copy failed.");
_logger.Trace("Move failed.");
return false;
}
private void TryCopyFileVerified(string sourcePath, string targetPath, long originalSize)
{
try
{
_diskProvider.CopyFile(sourcePath, targetPath);
var targetSize = _diskProvider.GetFileSize(targetPath);
if (targetSize != originalSize)
{
throw new IOException(string.Format("File copy incomplete. [{0}] was {1} bytes long instead of {2} bytes.", targetPath, targetSize, originalSize));
}
}
catch
{
RollbackCopy(sourcePath, targetPath);
throw;
}
}
private void TryMoveFileVerified(string sourcePath, string targetPath, long originalSize)
{
try
{
_diskProvider.MoveFile(sourcePath, targetPath);
var targetSize = _diskProvider.GetFileSize(targetPath);
if (targetSize != originalSize)
{
throw new IOException(string.Format("File move incomplete, data loss may have occurred. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize));
}
}
catch
{
RollbackPartialMove(sourcePath, targetPath);
throw;
}
}
}
}

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Common.Disk
{
return OsPathKind.Unix;
}
if (path.Contains(':') || path.Contains('\\'))
if (HasWindowsDriveLetter(path) || path.Contains('\\'))
{
return OsPathKind.Windows;
}
@@ -55,6 +55,15 @@ namespace NzbDrone.Common.Disk
return OsPathKind.Unknown;
}
private static bool HasWindowsDriveLetter(string path)
{
if (path.Length < 2) return false;
if (!char.IsLetter(path[0]) || path[1] != ':') return false;
if (path.Length > 2 && path[2] != '\\' && path[2] != '/') return false;
return true;
}
private static string FixSlashes(string path, OsPathKind kind)
{
switch (kind)
@@ -97,7 +106,7 @@ namespace NzbDrone.Common.Disk
{
if (IsWindowsPath)
{
return _path.StartsWith(@"\\") || _path.Contains(':');
return _path.StartsWith(@"\\") || HasWindowsDriveLetter(_path);
}
if (IsUnixPath)
{
@@ -153,6 +162,14 @@ namespace NzbDrone.Common.Disk
}
}
public bool IsValid
{
get
{
return _path.IsPathValid();
}
}
private int GetFileNameIndex()
{
if (_path.Length < 2)

View File

@@ -92,5 +92,13 @@ namespace NzbDrone.Common.Extensions
return "\"" + text + "\"";
}
public static byte[] HexToByteArray(this string input)
{
return Enumerable.Range(0, input.Length)
.Where(x => x%2 == 0)
.Select(x => Convert.ToByte(input.Substring(x, 2), 16))
.ToArray();
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -164,7 +165,14 @@ namespace NzbDrone.Common.Http.Dispatchers
var setCookie = webHeaderCollection.Get("Set-Cookie");
if (setCookie != null && setCookie.Length > 0 && cookies != null)
{
cookies.SetCookies(request.Url, FixSetCookieHeader(setCookie));
try
{
cookies.SetCookies(request.Url, FixSetCookieHeader(setCookie));
}
catch (CookieException ex)
{
_logger.Debug("Rejected cookie {0}: {1}", ex.InnerException.Message, setCookie);
}
}
return webHeaderCollection;
@@ -175,9 +183,15 @@ namespace NzbDrone.Common.Http.Dispatchers
// fix up the date if it was malformed
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
{
string format = "ddd, dd-MMM-yyyy HH:mm:ss";
DateTime dt = Convert.ToDateTime(match.Groups[2].Value);
return match.Groups[1].Value + dt.ToUniversalTime().ToString(format) + " GMT";
string shortFormat = "ddd, dd-MMM-yy HH:mm:ss";
string longFormat = "ddd, dd-MMM-yyyy HH:mm:ss";
DateTime dt;
if (DateTime.TryParseExact(match.Groups[2].Value, longFormat, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt) ||
DateTime.TryParseExact(match.Groups[2].Value, shortFormat, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt) ||
DateTime.TryParse(match.Groups[2].Value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt))
return match.Groups[1].Value + dt.ToUniversalTime().ToString(longFormat, CultureInfo.InvariantCulture) + " GMT";
else
return match.Value;
});
return setCookieClean;
}

View File

@@ -14,7 +14,18 @@ namespace NzbDrone.Common.Http
{
if (response.Headers.ContainsKey("Retry-After"))
{
RetryAfter = TimeSpan.FromSeconds(int.Parse(response.Headers["Retry-After"].ToString()));
var retryAfter = response.Headers["Retry-After"].ToString();
int seconds;
DateTime date;
if (int.TryParse(retryAfter, out seconds))
{
RetryAfter = TimeSpan.FromSeconds(seconds);
}
else if (DateTime.TryParse(retryAfter, out date))
{
RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
}
}
}
}

View File

@@ -13,6 +13,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -26,7 +27,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// uTorrent
new Regex(@"\[""[a-z._]*(|username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\[""[a-z._]*(username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// BroadcastheNet

View File

@@ -1,110 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Configuration
{
[TestFixture]
public class ConfigServiceFixture : DbTest<ConfigService, Config>
public class ConfigServiceFixture : TestBase<ConfigService>
{
[SetUp]
public void SetUp()
{
Mocker.SetConstant<IConfigRepository>(Mocker.Resolve<ConfigRepository>());
Db.All<Config>().ForEach(Db.Delete);
}
[Test]
public void Add_new_value_to_database()
{
const string key = "MY_KEY";
const string value = "MY_VALUE";
const string key = "RssSyncInterval";
const int value = 12;
Subject.SetValue(key, value);
Subject.GetValue(key, "").Should().Be(value);
}
Subject.RssSyncInterval = value;
[Test]
public void Get_value_from_database()
{
const string key = "MY_KEY";
const string value = "MY_VALUE";
Db.Insert(new Config { Key = key, Value = value });
Db.Insert(new Config { Key = "Other Key", Value = "OtherValue" });
var result = Subject.GetValue(key, "");
result.Should().Be(value);
AssertUpsert(key, value);
}
[Test]
public void Get_value_should_return_default_when_no_value()
{
const string key = "MY_KEY";
const string value = "MY_VALUE";
var result = Subject.GetValue(key, value);
result.Should().Be(value);
}
[Test]
public void New_value_should_update_old_value_new_value()
{
const string key = "MY_KEY";
const string originalValue = "OLD_VALUE";
const string newValue = "NEW_VALUE";
Db.Insert(new Config { Key = key, Value = originalValue });
Subject.SetValue(key, newValue);
var result = Subject.GetValue(key, "");
result.Should().Be(newValue);
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void New_value_should_update_old_value_same_value()
{
const string key = "MY_KEY";
const string value = "OLD_VALUE";
Subject.SetValue(key, value);
Subject.SetValue(key, value);
var result = Subject.GetValue(key, "");
result.Should().Be(value);
AllStoredModels.Should().HaveCount(1);
Subject.RssSyncInterval.Should().Be(15);
}
[Test]
public void get_value_with_persist_should_store_default_value()
{
const string key = "MY_KEY";
string value = Guid.NewGuid().ToString();
Subject.GetValue(key, value, persist: true).Should().Be(value);
Subject.GetValue(key, string.Empty).Should().Be(value);
var salt = Subject.HmacSalt;
salt.Should().NotBeNullOrWhiteSpace();
AssertUpsert("HmacSalt", salt);
}
[Test]
public void get_value_with_out_persist_should_not_store_default_value()
{
const string key = "MY_KEY";
string value1 = Guid.NewGuid().ToString();
string value2 = Guid.NewGuid().ToString();
var interval = Subject.RssSyncInterval;
interval.Should().Be(15);
Mocker.GetMock<IConfigRepository>().Verify(c => c.Insert(It.IsAny<Config>()), Times.Never());
}
Subject.GetValue(key, value1).Should().Be(value1);
Subject.GetValue(key, value2).Should().Be(value2);
private void AssertUpsert(string key, object value)
{
Mocker.GetMock<IConfigRepository>().Verify(c => c.Upsert(key.ToLowerInvariant(), value.ToString()));
}
[Test]
@@ -114,7 +63,16 @@ namespace NzbDrone.Core.Test.Configuration
var configProvider = Subject;
var allProperties = typeof(ConfigService).GetProperties().Where(p => p.GetSetMethod() != null).ToList();
var keys = new List<string>();
var values = new List<Config>();
Mocker.GetMock<IConfigRepository>().Setup(c => c.Upsert(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, value) =>
{
keys.Add(key);
values.Add(new Config { Key = key, Value = value });
});
Mocker.GetMock<IConfigRepository>().Setup(c => c.All()).Returns(values);
foreach (var propertyInfo in allProperties)
{
@@ -148,8 +106,7 @@ namespace NzbDrone.Core.Test.Configuration
returnValue.Should().Be(value, propertyInfo.Name);
}
AllStoredModels.Should()
.HaveSameCount(allProperties, "two different properties are writing to the same key in db. Copy/Past fail.");
keys.Should().OnlyHaveUniqueItems();
}
}
}

View File

@@ -103,12 +103,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_if_latest_history_has_a_download_id_and_cdh_is_enabled()
{
GivenMostRecentForEpisode(FIRST_EPISODE_ID, "test", _upgradableQuality, DateTime.UtcNow, HistoryEventType.Grabbed);
_upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue();
}
// [Test]
// public void should_return_true_if_latest_history_has_a_download_id_and_cdh_is_enabled()
// {
// GivenMostRecentForEpisode(FIRST_EPISODE_ID, "test", _notupgradableQuality, DateTime.UtcNow, HistoryEventType.Grabbed);
// _upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue();
// }
[Test]
public void should_return_true_if_latest_history_item_is_older_than_twelve_hours()

View File

@@ -381,6 +381,30 @@ namespace NzbDrone.Core.Test.Download
AssertCompletedDownload();
}
[Test]
public void should_warn_if_path_is_not_valid_for_windows()
{
WindowsOnly();
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"/invalid/Windows/Path");
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
[Test]
public void should_warn_if_path_is_not_valid_for_linux()
{
MonoOnly();
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"C:\Invalid\Mono\Path");
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
private void AssertNoAttemptedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()

View File

@@ -9,7 +9,9 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.TorrentBlackhole;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
@@ -67,6 +69,21 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
.Returns(1000000);
}
protected override RemoteEpisode CreateRemoteEpisode()
{
var remoteEpisode = base.CreateRemoteEpisode();
var torrentInfo = new TorrentInfo();
torrentInfo.Title = remoteEpisode.Release.Title;
torrentInfo.DownloadUrl = remoteEpisode.Release.DownloadUrl;
torrentInfo.DownloadProtocol = remoteEpisode.Release.DownloadProtocol;
torrentInfo.MagnetUrl = "magnet:?xt=urn:btih:755248817d32b00cc853e633ecdc48e4c21bff15&dn=Series.S05E10.PROPER.HDTV.x264-DEFiNE%5Brartv%5D&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce&tr=udp%3A%2F%2F9.rarbg.me%3A2710&tr=udp%3A%2F%2F9.rarbg.to%3A2710";
remoteEpisode.Release = torrentInfo;
return remoteEpisode;
}
[Test]
public void completed_download_should_have_required_properties()
{
@@ -116,6 +133,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
}
[Test]
public void Download_should_throw_if_magnet_and_torrent_url_does_not_exist()
{
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = null;
Assert.Throws<ReleaseDownloadException>(() => Subject.Download(remoteEpisode));
}
[Test]
public void GetItems_should_considered_locked_files_queued()
{

View File

@@ -0,0 +1,300 @@
using System;
using System.Linq;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Test.Common;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.NzbVortex;
using NzbDrone.Core.Download.Clients.NzbVortex.Responses;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests
{
[TestFixture]
public class NzbVortexFixture : DownloadClientFixtureBase<NzbVortex>
{
private NzbVortexQueueItem _queued;
private NzbVortexQueueItem _failed;
private NzbVortexQueueItem _completed;
[SetUp]
public void Setup()
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new NzbVortexSettings
{
Host = "127.0.0.1",
Port = 2222,
ApiKey = "1234-ABCD",
TvCategory = "tv",
RecentTvPriority = (int)NzbgetPriority.High
};
_queued = new NzbVortexQueueItem
{
Id = RandomNumber,
DownloadedSize = 1000,
TotalDownloadSize = 10,
GroupName = "tv",
UiTitle = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"
};
_failed = new NzbVortexQueueItem
{
DownloadedSize = 1000,
TotalDownloadSize = 1000,
GroupName = "tv",
UiTitle = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE",
DestinationPath = "somedirectory",
State = NzbVortexStateType.UncompressFailed,
};
_completed = new NzbVortexQueueItem
{
DownloadedSize = 1000,
TotalDownloadSize = 1000,
GroupName = "tv",
UiTitle = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE",
DestinationPath = "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE",
State = NzbVortexStateType.Done
};
}
protected void GivenFailedDownload()
{
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns((string)null);
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(Guid.NewGuid().ToString().Replace("-", ""));
}
protected virtual void GivenQueue(NzbVortexQueueItem queue)
{
var list = new List<NzbVortexQueueItem>();
list.AddIfNotNull(queue);
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.GetQueue(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexQueue
{
Items = list
});
}
[Test]
public void GetItems_should_return_no_items_when_queue_is_empty()
{
GivenQueue(null);
Subject.GetItems().Should().BeEmpty();
}
[Test]
public void queued_item_should_have_required_properties()
{
GivenQueue(_queued);
var result = Subject.GetItems().Single();
VerifyQueued(result);
}
[Test]
public void paused_item_should_have_required_properties()
{
_queued.IsPaused = true;
GivenQueue(_queued);
var result = Subject.GetItems().Single();
VerifyPaused(result);
}
[Test]
public void downloading_item_should_have_required_properties()
{
_queued.State = NzbVortexStateType.Downloading;
GivenQueue(_queued);
var result = Subject.GetItems().Single();
VerifyDownloading(result);
}
[Test]
public void completed_download_should_have_required_properties()
{
GivenQueue(_completed);
var result = Subject.GetItems().Single();
VerifyCompleted(result);
}
[Test]
public void failed_item_should_have_required_properties()
{
GivenQueue(_failed);
var result = Subject.GetItems().Single();
VerifyFailed(result);
}
[Test]
public void should_report_UncompressFailed_as_failed()
{
_queued.State = NzbVortexStateType.UncompressFailed;
GivenQueue(_failed);
var items = Subject.GetItems();
items.First().Status.Should().Be(DownloadItemStatus.Failed);
}
[Test]
public void should_report_CheckFailedDataCorrupt_as_failed()
{
_queued.State = NzbVortexStateType.CheckFailedDataCorrupt;
GivenQueue(_failed);
var result = Subject.GetItems().Single();
result.Status.Should().Be(DownloadItemStatus.Failed);
}
[Test]
public void should_report_BadlyEncoded_as_failed()
{
_queued.State = NzbVortexStateType.BadlyEncoded;
GivenQueue(_failed);
var items = Subject.GetItems();
items.First().Status.Should().Be(DownloadItemStatus.Failed);
}
[Test]
public void Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_should_throw_if_failed()
{
GivenFailedDownload();
var remoteEpisode = CreateRemoteEpisode();
Assert.Throws<DownloadClientException>(() => Subject.Download(remoteEpisode));
}
[Test]
public void GetItems_should_ignore_downloads_from_other_categories()
{
_completed.GroupName = "mycat";
GivenQueue(null);
var items = Subject.GetItems();
items.Should().BeEmpty();
}
[Test]
public void should_remap_storage_if_mounted()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()));
GivenQueue(_completed);
var result = Subject.GetItems().Single();
result.OutputPath.Should().Be(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic());
}
[Test]
public void should_get_files_if_completed_download_is_not_in_a_job_folder()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\".AsOsAgnostic()));
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexFiles{ Files = new List<NzbVortexFile> { new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" } } });
_completed.State = NzbVortexStateType.Done;
GivenQueue(_completed);
var result = Subject.GetItems().Single();
result.OutputPath.Should().Be(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv".AsOsAgnostic());
}
[Test]
public void should_be_warning_if_more_than_one_file_is_not_in_a_job_folder()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\".AsOsAgnostic()));
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexFiles { Files = new List<NzbVortexFile>
{
new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" },
new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.nfo" }
} });
_completed.State = NzbVortexStateType.Done;
GivenQueue(_completed);
var result = Subject.GetItems().Single();
result.Status.Should().Be(DownloadItemStatus.Warning);
}
[TestCase("1.0", false)]
[TestCase("2.2", false)]
[TestCase("2.3", true)]
[TestCase("2.4", true)]
[TestCase("3.0", true)]
public void should_test_api_version(string version, bool expected)
{
Mocker.GetMock<INzbVortexProxy>()
.Setup(v => v.GetGroups(It.IsAny<NzbVortexSettings>()))
.Returns(new List<NzbVortexGroup> { new NzbVortexGroup { GroupName = ((NzbVortexSettings)Subject.Definition.Settings).TvCategory } });
Mocker.GetMock<INzbVortexProxy>()
.Setup(v => v.GetApiVersion(It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexApiVersionResponse { ApiLevel = version });
var error = Subject.Test();
error.IsValid.Should().Be(expected);
}
}
}

View File

@@ -0,0 +1,297 @@
using System;
using System.Linq;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
[TestFixture]
public class QBittorrentFixture : DownloadClientFixtureBase<QBittorrent>
{
[SetUp]
public void Setup()
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new QBittorrentSettings
{
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass",
TvCategory = "tv"
};
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]));
}
protected void GivenRedirectToMagnet()
{
var httpHeader = new HttpHeader();
httpHeader["Location"] = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp";
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new Byte[0], System.Net.HttpStatusCode.SeeOther));
}
protected void GivenRedirectToTorrent()
{
var httpHeader = new HttpHeader();
httpHeader["Location"] = "http://test.sonarr.tv/not-a-real-torrent.torrent";
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.Is<HttpRequest>(h => h.Url.AbsoluteUri == _downloadUrl)))
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new Byte[0], System.Net.HttpStatusCode.Found));
}
protected void GivenFailedDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Callback(() =>
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "queuedUP",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
});
}
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
{
if (torrents == null)
torrents = new List<QBittorrentTorrent>();
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetTorrents(It.IsAny<QBittorrentSettings>()))
.Returns(torrents);
}
[Test]
public void error_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "error",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyFailed(item);
}
[Test]
public void paused_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedDL",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyPaused(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[TestCase("pausedUP")]
[TestCase("queuedUP")]
[TestCase("uploading")]
[TestCase("stalledUP")]
[TestCase("checkingUP")]
public void completed_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = state,
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyCompleted(item);
item.RemainingTime.Should().Be(TimeSpan.Zero);
}
[TestCase("queuedDL")]
[TestCase("checkingDL")]
public void queued_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = state,
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyQueued(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[Test]
public void downloading_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 60,
State = "downloading",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyDownloading(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[Test]
public void stalledDL_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "stalledDL",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyWarning(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[Test]
public void Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteEpisode);
id.Should().Be(expectedHash);
}
[Test]
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<string, Object>();
configItems.Add("save_path", @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
Mocker.GetMock<IQBittorrentProxy>()
.Setup(v => v.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(configItems);
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
}
[Test]
public void Download_should_handle_http_redirect_to_magnet()
{
GivenRedirectToMagnet();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_should_handle_http_redirect_to_torrent()
{
GivenRedirectToTorrent();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
}
}

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
protected TransmissionTorrent _downloading;
protected TransmissionTorrent _failed;
protected TransmissionTorrent _completed;
protected TransmissionTorrent _magnet;
protected Dictionary<string, object> _transmissionConfigItems;
[SetUp]
@@ -80,6 +81,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
DownloadDir = "somepath"
};
_magnet = new TransmissionTorrent
{
HashString = "HASH",
IsFinished = false,
Status = TransmissionTorrentStatus.Downloading,
Name = _title,
TotalSize = 0,
LeftUntilDone = 100,
DownloadDir = "somepath"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
@@ -171,6 +183,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
});
}
protected void PrepareClientToReturnMagnetItem()
{
GivenTorrents(new List<TransmissionTorrent>
{
_magnet
});
}
[Test]
public void queued_item_should_have_required_properties()
{
@@ -203,6 +223,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
VerifyCompleted(item);
}
[Test]
public void magnet_download_should_not_return_the_item()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
}
[Test]
public void Download_should_return_unique_id()
{

View File

@@ -143,7 +143,28 @@ namespace NzbDrone.Core.Test.Download
Assert.Throws<ReleaseDownloadException>(() => Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5)), Times.Once());
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
}
[Test]
public void Download_report_should_trigger_indexer_backoff_on_http429_based_on_date()
{
var request = new HttpRequest("http://my.indexer.com");
var response = new HttpResponse(request, new HttpHeader(), new byte[0], (HttpStatusCode)429);
response.Headers["Retry-After"] = DateTime.UtcNow.AddSeconds(300).ToString("r");
var mock = WithUsenetClient();
mock.Setup(s => s.Download(It.IsAny<RemoteEpisode>()))
.Callback<RemoteEpisode>(v =>
{
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
});
Assert.Throws<ReleaseDownloadException>(() => Subject.DownloadReport(_parseResult));
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(),
It.IsInRange<TimeSpan>(TimeSpan.FromMinutes(4.9), TimeSpan.FromMinutes(5.1), Range.Inclusive)), Times.Once());
}
[Test]

View File

@@ -48,8 +48,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
};
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.Is<ParsedEpisodeInfo>(i => i.SeasonNumber == 1 && i.SeriesTitle == "TV Series"), It.IsAny<int>(), It.IsAny<IEnumerable<int>>()))
.Returns(remoteEpisode);
.Setup(s => s.Map(It.Is<ParsedEpisodeInfo>(i => i.SeasonNumber == 1 && i.SeriesTitle == "TV Series"), It.IsAny<int>(), It.IsAny<IEnumerable<int>>()))
.Returns(remoteEpisode);
var client = new DownloadClientDefinition()
{
@@ -72,5 +72,61 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4);
trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(1);
}
[Test]
public void should_parse_as_special_when_source_title_parsing_fails()
{
var remoteEpisode = new RemoteEpisode
{
Series = new Series() { Id = 5 },
Episodes = new List<Episode> { new Episode { Id = 4 } },
ParsedEpisodeInfo = new ParsedEpisodeInfo()
{
SeriesTitle = "TV Series",
SeasonNumber = 0,
EpisodeNumbers = new []{ 1 }
}
};
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.Is<string>(sr => sr == "35238")))
.Returns(new List<History.History>(){
new History.History(){
DownloadId = "35238",
SourceTitle = "TV Series Special",
SeriesId = 5,
EpisodeId = 4
}
});
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.Is<ParsedEpisodeInfo>(i => i.SeasonNumber == 0 && i.SeriesTitle == "TV Series"), It.IsAny<int>(), It.IsAny<IEnumerable<int>>()))
.Returns(remoteEpisode);
Mocker.GetMock<IParsingService>()
.Setup(s => s.ParseSpecialEpisodeTitle(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(remoteEpisode.ParsedEpisodeInfo);
var client = new DownloadClientDefinition()
{
Id = 1,
Protocol = DownloadProtocol.Torrent
};
var item = new DownloadClientItem()
{
Title = "The torrent release folder",
DownloadId = "35238",
};
var trackedDownload = Subject.TrackDownload(client, item);
trackedDownload.Should().NotBeNull();
trackedDownload.RemoteEpisode.Should().NotBeNull();
trackedDownload.RemoteEpisode.Series.Should().NotBeNull();
trackedDownload.RemoteEpisode.Series.Id.Should().Be(5);
trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4);
trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(0);
}
}
}

View File

@@ -1,67 +0,0 @@
{
"code": "SUCCESSFUL",
"http_code": 200,
"limit": "2",
"offset": 0,
"results": [
{
"air_date": "20150623",
"anonymous": 1,
"codec": "x264",
"container": "MKV",
"created_at": "2015-06-25 04:13:44",
"download": "https://titansof.tv/api/torrents/19445/download?apikey=abc",
"ecommentUrl": "https://titansof.tv/series/287053/episode/5453241#comments",
"episode": "S02E04",
"episodeUrl": "https://titansof.tv/series/287053/episode/5453241",
"episode_id": "5453241",
"id": "19445",
"language": "en",
"leechers": 5,
"network": "truTV",
"origin": "Scene",
"release_name": "Series.Title.S02E04.720p.HDTV.x264-W4F",
"resolution": "720p",
"season": "",
"season_id": 0,
"seeders": 2,
"series": "Series Title",
"series_id": "287053",
"size": 435402993,
"snatched": 0,
"source": "HDTV",
"updated_at": "2015-06-25 04:13:44",
"user_id": 0
},
{
"air_date": "20150624",
"anonymous": 1,
"codec": "x264",
"container": "MKV",
"created_at": "2015-06-25 04:11:59",
"download": "https://titansof.tv/api/torrents/19444/download?apikey=abc",
"ecommentUrl": "https://titansof.tv/series/75382/episode/5443517#comments",
"episode": "S21E10",
"episodeUrl": "https://titansof.tv/series/75382/episode/5443517",
"episode_id": "5443517",
"id": "19444",
"language": "en",
"leechers": 0,
"network": "FX",
"origin": "User",
"release_name": "Series.Title.S21E10.720p.HDTV.x264-KOENiG",
"resolution": "720p",
"season": "",
"season_id": 0,
"seeders": 1,
"series": "Series Title",
"series_id": "75382",
"size": 949968933,
"snatched": 0,
"source": "HDTV",
"updated_at": "2015-06-25 04:11:59",
"user_id": 0
}
],
"total": 18546
}

View File

@@ -3,7 +3,9 @@ using System.Collections.Generic;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck.Checks
@@ -25,6 +27,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public void should_return_error_when_download_client_throws()
{
var downloadClient = Mocker.GetMock<IDownloadClient>();
downloadClient.Setup(s => s.Definition).Returns(new IndexerDefinition{Name = "Test"});
downloadClient.Setup(s => s.GetItems())
.Throws<Exception>();

View File

@@ -6,6 +6,7 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Update;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
@@ -47,5 +48,28 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Check().ShouldBeError();
}
[Test]
public void should_not_return_error_when_app_folder_is_write_protected_and_external_script_enabled()
{
MonoOnly();
Mocker.GetMock<IConfigFileProvider>()
.Setup(s => s.UpdateAutomatically)
.Returns(true);
Mocker.GetMock<IConfigFileProvider>()
.Setup(s => s.UpdateMechanism)
.Returns(UpdateMechanism.Script);
Mocker.GetMock<IAppFolderInfo>()
.Setup(s => s.StartUpFolder)
.Returns(@"/opt/nzbdrone");
Mocker.GetMock<NzbDrone.Common.Disk.IDiskProvider>()
.Verify(c => c.FolderWritable(Moq.It.IsAny<string>()), Times.Never());
Subject.Check().ShouldBeOk();
}
}
}

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
[TestCase("Hawaii Five-0", Result = "Hawaii+Five+0")]
[TestCase("Franklin & Bash", Result = "Franklin+and+Bash")]
[TestCase("Chicago P.D.", Result = "Chicago+PD")]
[TestCase("Kourtney And Khloé Take The Hamptons", Result = "Kourtney+And+Khloe+Take+The+Hamptons")]
[TestCase("Kourtney And Khlo\u00E9 Take The Hamptons", Result = "Kourtney+And+Khloe+Take+The+Hamptons")]
public string should_replace_some_special_characters(string input)
{
Subject.SceneTitles = new List<string> { input };

View File

@@ -1,157 +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.TitansOfTv;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.IndexerTests.TitansOfTvTests
{
[TestFixture]
public class TitansOfTvFixture : CoreTest<TitansOfTv>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition
{
Name = "TitansOfTV",
Settings = new TitansOfTvSettings { ApiKey = "abc", BaseUrl = "https://titansof.tv/api" }
};
}
[Test]
public void should_parse_recent_feed_from_TitansOfTv()
{
var recentFeed = ReadAllText(@"Files/Indexers/TitansOfTv/RecentFeed.json");
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(2);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Guid.Should().Be("ToTV-19445");
torrentInfo.Title.Should().Be("Series.Title.S02E04.720p.HDTV.x264-W4F");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://titansof.tv/api/torrents/19445/download?apikey=abc");
torrentInfo.InfoUrl.Should().Be("https://titansof.tv/series/287053/episode/5453241");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-25 04:13:44"));
torrentInfo.Size.Should().Be(435402993);
torrentInfo.InfoHash.Should().BeNullOrEmpty();
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.TvRageId.Should().Be(0);
torrentInfo.MagnetUrl.Should().BeNullOrEmpty();
torrentInfo.Peers.Should().Be(2+5);
torrentInfo.Seeders.Should().Be(2);
}
private void VerifyBackOff()
{
Mocker.GetMock<IIndexerStatusService>()
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
}
[Test]
public void should_back_off_on_bad_request()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.BadRequest));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_and_report_api_key_invalid()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.Unauthorized));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_on_unknown_method()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.NotFound));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_api_limit_reached()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.ServiceUnavailable));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_replace_https_http_as_needed()
{
var recentFeed = ReadAllText(@"Files/Indexers/TitansOfTv/RecentFeed.json");
(Subject.Definition.Settings as TitansOfTvSettings).BaseUrl = "http://titansof.tv/api/torrents";
recentFeed = recentFeed.Replace("http:", "https:");
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(2);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.DownloadUrl.Should().Be("http://titansof.tv/api/torrents/19445/download?apikey=abc");
torrentInfo.InfoUrl.Should().Be("http://titansof.tv/series/287053/episode/5453241");
}
}
}

View File

@@ -84,8 +84,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
_videoFiles = videoFiles.ToList();
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
.Returns(_videoFiles);
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
.Returns(_videoFiles);
}
[Test]
@@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
}
[Test]
public void should_use_file_quality_if_folder_quality_is_lower_than_file_quality()
public void should_use_file_quality_if_file_quality_was_determined_by_name()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
@@ -191,10 +191,32 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
}
[Test]
public void should_use_folder_quality_when_it_is_greater_than_file_quality()
public void should_use_folder_quality_when_file_quality_was_determined_by_the_extension()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = new QualityModel(Quality.Bluray1080p);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single();
_localEpisode.Quality.QualitySource = QualitySource.Extension;
_localEpisode.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.SDTV);
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo { Quality = expectedQuality }, true);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_folder_quality_when_greater_than_file_quality()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single();
_localEpisode.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.Bluray720p);
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo { Quality = expectedQuality }, true);

View File

@@ -161,8 +161,10 @@
<Compile Include="Download\DownloadClientTests\DelugeTests\DelugeFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" />
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
<Compile Include="Download\DownloadClientTests\NzbVortexTests\NzbVortexFixture.cs" />
<Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
<Compile Include="Download\DownloadClientTests\RTorrentTests\RTorrentFixture.cs" />
<Compile Include="Download\DownloadClientTests\QBittorrentTests\QBittorrentFixture.cs" />
<Compile Include="Download\DownloadClientTests\SabnzbdTests\SabnzbdFixture.cs" />
<Compile Include="Download\DownloadClientTests\TransmissionTests\TransmissionFixture.cs" />
<Compile Include="Download\DownloadClientTests\UTorrentTests\UTorrentFixture.cs" />
@@ -222,7 +224,6 @@
<Compile Include="IndexerTests\IndexerStatusServiceFixture.cs" />
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
<Compile Include="IndexerTests\RarbgTests\RarbgFixture.cs" />
<Compile Include="IndexerTests\TitansOfTvTests\TitansOfTvFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssParserFactoryFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssSettingsDetectorFixture.cs" />
<Compile Include="IndexerTests\TorznabTests\TorznabFixture.cs" />
@@ -508,9 +509,6 @@
</Content>
</ItemGroup>
<ItemGroup>
<None Include="Files\Indexers\TitansOfTv\RecentFeed.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Files\TestArchive.tar.gz">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

View File

@@ -57,6 +57,11 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_episodeFile.Quality.Revision.Version = 2;
}
private void GivenReal()
{
_episodeFile.Quality.Revision.Real = 1;
}
[Test]
public void should_replace_Series_space_Title()
{
@@ -207,6 +212,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be("Proper");
}
[Test]
public void should_replace_quality_real_with_real()
{
_namingConfig.StandardEpisodeFormat = "{Quality Real}";
GivenReal();
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("REAL");
}
[Test]
public void should_replace_all_contents_in_pattern()
{
@@ -617,6 +632,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be("South Park - S15E06 [HDTV-720p Proper]");
}
[Test]
public void should_replace_quality_full_with_quality_title_and_real_when_a_real()
{
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} [{Quality Full}]";
GivenReal();
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South Park - S15E06 [HDTV-720p REAL]");
}
[TestCase(' ')]
[TestCase('-')]
[TestCase('.')]

View File

@@ -82,6 +82,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 018 (116) [1280x720][C4A3B16E]", "Dragon Ball Kai (2014)", 18, 0, 0)]
[TestCase("Dragon Ball Kai (2014) - 39 (137) [v2][720p.HDTV][Unison Fansub]", "Dragon Ball Kai (2014)", 39, 0, 0)]
[TestCase("[HorribleSubs] Eyeshield 21 - 101 [480p].mkv", "Eyeshield 21", 101, 0, 0)]
[TestCase("[Cthuyuu].Taimadou.Gakuen.35.Shiken.Shoutai.-.03.[720p.H264.AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
//[TestCase("Taimadou.Gakuen.35.Shiken.Shoutai.-.03.(1280x720.HEVC.AAC)", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
[TestCase("[Cthuyuu] Taimadou Gakuen 35 Shiken Shoutai - 03 [720p H264 AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
//[TestCase("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{

View File

@@ -1,5 +1,4 @@
using System;
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Test.Framework;
@@ -14,18 +13,19 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Chuck.S03E17.REAL.PROPER.720p.HDTV.x264-ORENJI-RP", 1)]
[TestCase("Covert.Affairs.S05E09.REAL.PROPER.HDTV.x264-KILLERS", 1)]
[TestCase("Mythbusters.S14E01.REAL.PROPER.720p.HDTV.x264-KILLERS", 1)]
[TestCase("Orange.Is.the.New.Black.s02e06.real.proper.720p.webrip.x264-2hd", 1)]
[TestCase("Top.Gear.S21E07.Super.Duper.Real.Proper.HDTV.x264-FTP", 1)]
[TestCase("Orange.Is.the.New.Black.s02e06.real.proper.720p.webrip.x264-2hd", 0)]
[TestCase("Top.Gear.S21E07.Super.Duper.Real.Proper.HDTV.x264-FTP", 0)]
[TestCase("Top.Gear.S21E07.PROPER.HDTV.x264-RiVER-RP", 0)]
[TestCase("House.S07E11.PROPER.REAL.RERIP.1080p.BluRay.x264-TENEIGHTY", 1)]
[TestCase("[MGS] - Kuragehime - Episode 02v2 - [D8B6C90D]", 0)]
[TestCase("[Hatsuyuki] Tokyo Ghoul - 07 [v2][848x480][23D8F455].avi", 0)]
[TestCase("[DeadFish] Barakamon - 01v3 [720p][AAC]", 0)]
[TestCase("[DeadFish] Momo Kyun Sword - 01v4 [720p][AAC]", 0)]
[TestCase("The Real Housewives of Some Place - S01E01 - Why are we doing this?", 0)]
public void should_parse_reality_from_title(string title, int reality)
{
//TODO: re-enable this when we have a reliable way to determine real
//QualityParser.ParseQuality(title).Revision.Real.Should().Be(reality);
QualityParser.ParseQuality(title).Revision.Real.Should().Be(reality);
}
[TestCase("Chuck.S04E05.HDTV.XviD-LOL", 1)]

View File

@@ -42,7 +42,21 @@ namespace NzbDrone.Core.Test.ParserTests
//[TestCase("Adventure Time - 5x01 - x02 - Finn the Human (2) & Jake the Dog (3)", "Adventure Time", 5, new [] { 1, 2 })]
[TestCase("The Young And The Restless - S42 Ep10718 - Ep10722", "The Young And The Restless", 42, new[] { 10718, 10719, 10720, 10721, 10722 })]
[TestCase("The Young And The Restless - S42 Ep10688 - Ep10692", "The Young And The Restless", 42, new[] { 10688, 10689, 10690, 10691, 10692 })]
//[TestCase("", "", ,new [] { })]
[TestCase("RWBY.S01E02E03.1080p.BluRay.x264-DeBTViD", "RWBY", 1, new [] { 2, 3 })]
[TestCase("grp-zoos01e11e12-1080p", "grp-zoo", 1, new [] { 11, 12 })]
[TestCase("grp-zoo-s01e11e12-1080p", "grp-zoo", 1, new [] { 11, 12 })]
[TestCase("Series Title.S6.E1.E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })]
[TestCase("Series Title.S6E1-E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })]
[TestCase("Series Title.S6E1-S6E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })]
[TestCase("Series Title.S6E1E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2 })]
[TestCase("Series Title.S6E1-E2-E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2, 3})]
[TestCase("Series Title.S6.E1E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new [] { 1, 2, 3 })]
[TestCase("Series Title.S6.E1-E2.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2 })]
[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("Series Title.S6.E1E3.Episode Name.1080p.WEB-DL", "Series Title", 6, new[] { 1, 2, 3 })]
//[TestCase("", "", , new [] { })]
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -37,6 +37,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Match of the Day 2", "matchday2")]
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "Revenge")]
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "Seed")]
[TestCase("Reno.911.S01.DVDRip.DD2.0.x264-DEEP", "Reno 911")]
public void should_parse_series_name(string postTitle, string title)
{
var result = Parser.Parser.ParseSeriesName(postTitle).CleanSeriesTitle();
@@ -46,7 +47,7 @@ namespace NzbDrone.Core.Test.ParserTests
[Test]
public void should_remove_accents_from_title()
{
const string title = "Carnivŕle";
const string title = "Carniv\u00E0le";
title.CleanSeriesTitle().Should().Be("carnivale");
}

View File

@@ -204,6 +204,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Sonny.With.a.Chance.S02E15", false)]
[TestCase("Law & Order: Special Victims Unit - 11x11 - Quickie", false)]
[TestCase("Series.Title.S01E01.webm", false)]
public void quality_parse(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Unknown, proper);
@@ -228,6 +229,24 @@ namespace NzbDrone.Core.Test.ParserTests
}
}
[TestCase("Saturday.Night.Live.Vintage.S10E09.Eddie.Murphy.The.Honeydrippers.1080i.UPSCALE.HDTV.DD5.1.MPEG2-zebra")]
[TestCase("Dexter - S01E01 - Title [HDTV-1080p]")]
[TestCase("[CR] Sailor Moon - 004 [480p][48CE2D0F]")]
[TestCase("White.Van.Man.2011.S02E01.WS.PDTV.x264-REPACK-TLA")]
public void should_parse_quality_from_name(string title)
{
QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Name);
}
[TestCase("Revolution.S01E02.Chained.Heat.mkv")]
[TestCase("Dexter - S01E01 - Title.avi")]
[TestCase("the_x-files.9x18.sunshine_days.avi")]
[TestCase("[CR] Sailor Moon - 004 [48CE2D0F].avi")]
public void should_parse_quality_from_extension(string title)
{
QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Extension);
}
private void ParseAndVerifyQuality(string title, Quality quality, bool proper)
{
var result = QualityParser.ParseQuality(title);

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title S01E01 Episode Title", null)]
[TestCase("The Colbert Report - 2014-06-02 - Thomas Piketty.mkv", null)]
[TestCase("Real Time with Bill Maher S12E17 May 23, 2014.mp4", null)]
[TestCase("Reizen Waes - S01E08 - Transistrië, Zuid-Ossetië en Abchazië SDTV.avi", null)]
[TestCase("Reizen Waes - S01E08 - Transistri\u00EB, Zuid-Osseti\u00EB en Abchazi\u00EB SDTV.avi", null)]
[TestCase("Simpsons 10x11 - Wild Barts Cant Be Broken [rl].avi", null)]
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "DIMENSION")]
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "2HD")]

View File

@@ -111,6 +111,11 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title Season 01 Episode 05 720p", "Series Title", 1, 5)]
//[TestCase("Off the Air - 101 - Animals (460p.x264.vorbis-2.0) [449].mkv", "Off the Air", 1, 1)]
[TestCase("The Young And the Restless - S42 E10713 - 2015-07-20.mp4", "The Young And the Restless", 42, 10713)]
[TestCase("quantico.103.hdtv-lol[ettv].mp4", "quantico", 1, 3)]
[TestCase("Fargo - 01x02 - The Rooster Prince - [itz_theo]", "Fargo", 1, 2)]
[TestCase("Castle (2009) - [06x16] - Room 147.mp4", "Castle (2009)", 6, 16)]
[TestCase("grp-zoos01e11-1080p", "grp-zoo", 1, 11)]
[TestCase("grp-zoo-s01e11-1080p", "grp-zoo", 1, 11)]
//[TestCase("", "", 0, 0)]
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
{

View File

@@ -18,7 +18,6 @@ namespace NzbDrone.Core.Test.TvTests
{
_series = Builder<Series>.CreateNew()
.With(v => v.Status == SeriesStatusType.Continuing)
.With(v => v.LastInfoSync == DateTime.UtcNow.AddHours(-12))
.Build();
Mocker.GetMock<IEpisodeService>()
@@ -45,6 +44,11 @@ namespace NzbDrone.Core.Test.TvTests
_series.LastInfoSync = DateTime.UtcNow.AddDays(-1);
}
private void GivenSeriesLastRefreshedHalfADayAgo()
{
_series.LastInfoSync = DateTime.UtcNow.AddHours(-12);
}
private void GivenSeriesLastRefreshedRecently()
{
_series.LastInfoSync = DateTime.UtcNow.AddHours(-1);
@@ -66,6 +70,8 @@ namespace NzbDrone.Core.Test.TvTests
[Test]
public void should_return_true_if_running_series_last_refreshed_more_than_6_hours_ago()
{
GivenSeriesLastRefreshedHalfADayAgo();
Subject.ShouldRefresh(_series).Should().BeTrue();
}

View File

@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Configuration
public interface IConfigRepository : IBasicRepository<Config>
{
Config Get(string key);
Config Upsert(string key, string value);
}
public class ConfigRepository : BasicRepository<Config>, IConfigRepository
@@ -23,5 +23,19 @@ namespace NzbDrone.Core.Configuration
{
return Query.Where(c => c.Key == key).SingleOrDefault();
}
public Config Upsert(string key, string value)
{
var dbValue = Get(key);
if (dbValue == null)
{
return Insert(new Config {Key = key, Value = value});
}
dbValue.Value = value;
return Update(dbValue);
}
}
}

View File

@@ -30,12 +30,7 @@ namespace NzbDrone.Core.Configuration
_cache = new Dictionary<string, string>();
}
public IEnumerable<Config> All()
{
return _repository.All();
}
public Dictionary<string, object> AllWithDefaults()
private Dictionary<string, object> AllWithDefaults()
{
var dict = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
@@ -45,7 +40,6 @@ namespace NzbDrone.Core.Configuration
foreach (var propertyInfo in properties)
{
var value = propertyInfo.GetValue(this, null);
dict.Add(propertyInfo.Name, value);
}
@@ -65,7 +59,9 @@ namespace NzbDrone.Core.Configuration
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
if (!equal)
{
SetValue(configValue.Key, configValue.Value.ToString());
}
}
_eventAggregator.PublishEvent(new ConfigSavedEvent());
@@ -331,7 +327,7 @@ namespace NzbDrone.Core.Configuration
return Convert.ToInt32(GetValue(key, defaultValue));
}
public T GetValueEnum<T>(string key, T defaultValue)
private T GetValueEnum<T>(string key, T defaultValue)
{
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
}
@@ -346,7 +342,9 @@ namespace NzbDrone.Core.Configuration
string dbValue;
if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue))
{
return dbValue;
}
_logger.Trace("Using default config value for '{0}' defaultValue:'{1}'", key, defaultValue);
@@ -354,6 +352,7 @@ namespace NzbDrone.Core.Configuration
{
SetValue(key, defaultValue.ToString());
}
return defaultValue.ToString();
}
@@ -367,44 +366,34 @@ namespace NzbDrone.Core.Configuration
SetValue(key, value.ToString());
}
public void SetValue(string key, string value)
private void SetValue(string key, Enum value)
{
SetValue(key, value.ToString().ToLower());
}
private void SetValue(string key, string value)
{
key = key.ToLowerInvariant();
_logger.Trace("Writing Setting to database. Key:'{0}' Value:'{1}'", key, value);
var dbValue = _repository.Get(key);
if (dbValue == null)
{
_repository.Insert(new Config { Key = key, Value = value });
}
else
{
dbValue.Value = value;
_repository.Update(dbValue);
}
_repository.Upsert(key, value);
ClearCache();
}
public void SetValue(string key, Enum value)
{
SetValue(key, value.ToString().ToLower());
}
private void EnsureCache()
{
lock (_cache)
{
if (!_cache.Any())
{
_cache = All().ToDictionary(c => c.Key.ToLower(), c => c.Value);
var all = _repository.All();
_cache = all.ToDictionary(c => c.Key.ToLower(), c => c.Value);
}
}
}
public static void ClearCache()
private static void ClearCache()
{
lock (_cache)
{

View File

@@ -6,8 +6,6 @@ namespace NzbDrone.Core.Configuration
{
public interface IConfigService
{
IEnumerable<Config> All();
Dictionary<string, object> AllWithDefaults();
void SaveConfigDictionary(Dictionary<string, object> configValues);
bool IsDefined(string key);

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(98)]
public class remove_titans_of_tv : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Delete.FromTable("Indexers").Row(new { Implementation = "TitansOfTv" });
}
}
}

View File

@@ -78,14 +78,18 @@ namespace NzbDrone.Core.DecisionEngine
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, searchCriteria);
remoteEpisode.Release = report;
if (remoteEpisode.Series != null)
if (remoteEpisode.Series == null)
{
remoteEpisode.DownloadAllowed = remoteEpisode.Episodes.Any();
decision = GetDecisionForReport(remoteEpisode, searchCriteria);
decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown Series"));
}
else if (remoteEpisode.Episodes.Empty())
{
decision = new DownloadDecision(remoteEpisode, new Rejection("Unable to parse episodes from release name"));
}
else
{
decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown Series"));
remoteEpisode.DownloadAllowed = remoteEpisode.Episodes.Any();
decision = GetDecisionForReport(remoteEpisode, searchCriteria);
}
}
}

View File

@@ -57,7 +57,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return Decision.Reject("{0} is smaller than minimum allowed: {1}", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix());
}
}
if (!qualityDefinition.MaxSize.HasValue)
if (!qualityDefinition.MaxSize.HasValue || qualityDefinition.MaxSize.Value == 0)
{
_logger.Debug("Max size is unlimited - skipping check.");
}

View File

@@ -46,18 +46,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed)
{
var recentBlackhole = mostRecent.DownloadId.IsNullOrWhiteSpace() && mostRecent.Date.After(DateTime.UtcNow.AddHours(-12));
var recent = mostRecent.Date.After(DateTime.UtcNow.AddHours(-12));
var cutoffUnmet = _qualityUpgradableSpecification.CutoffNotMet(subject.Series.Profile, mostRecent.Quality, subject.ParsedEpisodeInfo.Quality);
var upgradeable = _qualityUpgradableSpecification.IsUpgradable(subject.Series.Profile, mostRecent.Quality, subject.ParsedEpisodeInfo.Quality);
if (!recentBlackhole && cdhEnabled)
if (!recent && cdhEnabled)
{
continue;
}
if (!cutoffUnmet)
{
if (recentBlackhole)
if (recent)
{
return Decision.Reject("Recent grab event in history already meets cutoff: {0}", mostRecent.Quality);
}
@@ -67,7 +67,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
if (!upgradeable)
{
if (recentBlackhole)
if (recent)
{
return Decision.Reject("Recent grab event in history is of equal or higher quality: {0}", mostRecent.Quality);
}

View File

@@ -243,7 +243,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
}
var restClient = RestClientFactory.BuildClient(url);
restClient.Timeout = 4000;
restClient.Timeout = 15000;
if (_authPassword != settings.Password || _authCookieContainer == null)
{

View File

@@ -0,0 +1,29 @@
using System;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters
{
public class NzbVortexLoginResultTypeConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var priorityType = (NzbVortexLoginResultType)value;
writer.WriteValue(priorityType.ToString().ToLower());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var result = reader.Value.ToString().Replace("_", string.Empty);
NzbVortexLoginResultType output;
Enum.TryParse(result, true, out output);
return output;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(NzbVortexLoginResultType);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters
{
public class NzbVortexResultTypeConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var priorityType = (NzbVortexResultType)value;
writer.WriteValue(priorityType.ToString().ToLower());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var result = reader.Value.ToString().Replace("_", string.Empty);
NzbVortexResultType output;
Enum.TryParse(result, true, out output);
return output;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(NzbVortexResultType);
}
}
}

View File

@@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortex : UsenetClientBase<NzbVortexSettings>
{
private readonly INzbVortexProxy _proxy;
public NzbVortex(INzbVortexProxy proxy,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
}
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
{
var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
var response = _proxy.DownloadNzb(fileContent, filename, priority, Settings);
if (response == null)
{
throw new DownloadClientException("Failed to add nzb {0}", filename);
}
return response;
}
public override string Name
{
get
{
return "NZBVortex";
}
}
public override IEnumerable<DownloadClientItem> GetItems()
{
NzbVortexQueue vortexQueue;
try
{
vortexQueue = _proxy.GetQueue(30, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn("Couldn't get download queue. {0}", ex.Message);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>();
foreach (var vortexQueueItem in vortexQueue.Items)
{
var queueItem = new DownloadClientItem();
queueItem.DownloadClient = Definition.Name;
queueItem.DownloadId = vortexQueueItem.AddUUID ?? vortexQueueItem.Id.ToString();
queueItem.Category = vortexQueueItem.GroupName;
queueItem.Title = vortexQueueItem.UiTitle;
queueItem.TotalSize = vortexQueueItem.TotalDownloadSize;
queueItem.RemainingSize = vortexQueueItem.TotalDownloadSize - vortexQueueItem.DownloadedSize;
queueItem.RemainingTime = null;
if (vortexQueueItem.IsPaused)
{
queueItem.Status = DownloadItemStatus.Paused;
}
else switch (vortexQueueItem.State)
{
case NzbVortexStateType.Waiting:
queueItem.Status = DownloadItemStatus.Queued;
break;
case NzbVortexStateType.Done:
queueItem.Status = DownloadItemStatus.Completed;
break;
case NzbVortexStateType.UncompressFailed:
case NzbVortexStateType.CheckFailedDataCorrupt:
case NzbVortexStateType.BadlyEncoded:
queueItem.Status = DownloadItemStatus.Failed;
break;
default:
queueItem.Status = DownloadItemStatus.Downloading;
break;
}
queueItem.OutputPath = GetOutputPath(vortexQueueItem, queueItem);
if (vortexQueueItem.State == NzbVortexStateType.PasswordRequest)
{
queueItem.IsEncrypted = true;
}
if (queueItem.Status == DownloadItemStatus.Completed)
{
queueItem.RemainingTime = TimeSpan.Zero;
}
queueItems.Add(queueItem);
}
return queueItems;
}
public override void RemoveItem(string downloadId, bool deleteData)
{
// Try to find the download by numerical ID, otherwise try by AddUUID
int id;
if (int.TryParse(downloadId, out id))
{
_proxy.Remove(id, deleteData, Settings);
}
else
{
var queue = _proxy.GetQueue(30, Settings);
var queueItem = queue.Items.FirstOrDefault(c => c.AddUUID == downloadId);
if (queueItem != null)
{
_proxy.Remove(queueItem.Id, deleteData, Settings);
}
}
}
protected List<NzbVortexGroup> GetGroups()
{
return _proxy.GetGroups(Settings);
}
public override DownloadClientStatus GetStatus()
{
var status = new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
};
return status;
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
failures.AddIfNotNull(TestApiVersion());
failures.AddIfNotNull(TestAuthentication());
failures.AddIfNotNull(TestCategory());
}
private ValidationFailure TestConnection()
{
try
{
_proxy.GetVersion(Settings);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new ValidationFailure("Host", "Unable to connect to NZBVortex");
}
return null;
}
private ValidationFailure TestApiVersion()
{
try
{
var response = _proxy.GetApiVersion(Settings);
var version = new Version(response.ApiLevel);
if (version.Major < 2 || (version.Major == 2 && version.Minor < 3))
{
return new ValidationFailure("Host", "NZBVortex needs to be updated");
}
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new ValidationFailure("Host", "Unable to connect to NZBVortex");
}
return null;
}
private ValidationFailure TestAuthentication()
{
try
{
_proxy.GetQueue(1, Settings);
}
catch (NzbVortexAuthenticationException ex)
{
return new ValidationFailure("ApiKey", "API Key Incorrect");
}
return null;
}
private ValidationFailure TestCategory()
{
var group = GetGroups().FirstOrDefault(c => c.GroupName == Settings.TvCategory);
if (group == null)
{
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
return new NzbDroneValidationFailure("TvCategory", "Group does not exist")
{
DetailedDescription = "The Group you entered doesn't exist in NzbVortex. Go to NzbVortex to create it."
};
}
}
return null;
}
private OsPath GetOutputPath(NzbVortexQueueItem vortexQueueItem, DownloadClientItem queueItem)
{
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(vortexQueueItem.DestinationPath));
if (outputPath.FileName == vortexQueueItem.UiTitle)
{
return outputPath;
}
// If the release isn't done yet, skip the files check and return null
if (vortexQueueItem.State != NzbVortexStateType.Done)
{
return new OsPath(null);
}
var filesResponse = _proxy.GetFiles(vortexQueueItem.Id, Settings);
if (filesResponse.Files.Count > 1)
{
var message = string.Format("Download contains multiple files and is not in a job folder: {0}", outputPath);
queueItem.Status = DownloadItemStatus.Warning;
queueItem.Message = message;
_logger.Debug(message);
}
return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.Files.First().FileName));
}
}
}

View File

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

View File

@@ -0,0 +1,16 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexFile
{
public int Id { get; set; }
public string FileName { get; set; }
public NzbVortexStateType State { get; set; }
public long DileSize { get; set; }
public long DownloadedSize { get; set; }
public long TotalDownloadedSize { get; set; }
public bool ExtractPasswordRequired { get; set; }
public string ExtractPassword { get; set; }
public long PostDate { get; set; }
public bool Crc32CheckFailed { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexFiles
{
public List<NzbVortexFile> Files { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexGroup
{
public string GroupName { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexJsonError
{
public string Status { get; set; }
public string Error { get; set; }
public bool Failed
{
get
{
return !string.IsNullOrWhiteSpace(Status) &&
Status.Equals("false", StringComparison.InvariantCultureIgnoreCase);
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexLoginResultType
{
Successful,
Failed
}
}

View File

@@ -0,0 +1,27 @@
using System;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
class NzbVortexNotLoggedInException : DownloadClientException
{
public NzbVortexNotLoggedInException() : this("Authentication is required")
{
}
public NzbVortexNotLoggedInException(string message, params object[] args) : base(message, args)
{
}
public NzbVortexNotLoggedInException(string message) : base(message)
{
}
public NzbVortexNotLoggedInException(string message, Exception innerException, params object[] args) : base(message, innerException, args)
{
}
public NzbVortexNotLoggedInException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexPriority
{
Low = -1,
Normal = 0,
High = 1,
}
}

View File

@@ -0,0 +1,235 @@
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Security.Cryptography;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Rest;
using NzbDrone.Core.Download.Clients.NzbVortex.Responses;
using RestSharp;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public interface INzbVortexProxy
{
string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings);
void Remove(int id, bool deleteData, NzbVortexSettings settings);
NzbVortexVersionResponse GetVersion(NzbVortexSettings settings);
NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings);
List<NzbVortexGroup> GetGroups(NzbVortexSettings settings);
NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings);
NzbVortexFiles GetFiles(int id, NzbVortexSettings settings);
}
public class NzbVortexProxy : INzbVortexProxy
{
private readonly ICached<string> _authCache;
private readonly Logger _logger;
public NzbVortexProxy(ICacheManager cacheManager, Logger logger)
{
_authCache = cacheManager.GetCache<string>(GetType(), "authCache");
_logger = logger;
}
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings)
{
var request = BuildRequest("/nzb/add", Method.POST, true, settings);
request.AddFile("name", nzbData, filename, "application/x-nzb");
request.AddQueryParameter("priority", priority.ToString());
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddQueryParameter("groupname", settings.TvCategory);
}
var response = ProcessRequest<NzbVortexAddResponse>(request, settings);
return response.Id;
}
public void Remove(int id, bool deleteData, NzbVortexSettings settings)
{
var request = BuildRequest(string.Format("nzb/{0}/cancel", id), Method.GET, true, settings);
if (deleteData)
{
request.Resource += "Delete";
}
ProcessRequest(request, settings);
}
public NzbVortexVersionResponse GetVersion(NzbVortexSettings settings)
{
var request = BuildRequest("app/appversion", Method.GET, false, settings);
var response = ProcessRequest<NzbVortexVersionResponse>(request, settings);
return response;
}
public NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings)
{
var request = BuildRequest("app/apilevel", Method.GET, false, settings);
var response = ProcessRequest<NzbVortexApiVersionResponse>(request, settings);
return response;
}
public List<NzbVortexGroup> GetGroups(NzbVortexSettings settings)
{
var request = BuildRequest("group", Method.GET, true, settings);
var response = ProcessRequest<NzbVortexGroupResponse>(request, settings);
return response.Groups;
}
public NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings)
{
var request = BuildRequest("nzb", Method.GET, true, settings);
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddQueryParameter("groupName", settings.TvCategory);
}
request.AddQueryParameter("limitDone", doneLimit.ToString());
var response = ProcessRequest<NzbVortexQueue>(request, settings);
return response;
}
public NzbVortexFiles GetFiles(int id, NzbVortexSettings settings)
{
var request = BuildRequest(string.Format("file/{0}", id), Method.GET, true, settings);
var response = ProcessRequest<NzbVortexFiles>(request, settings);
return response;
}
private string GetSessionId(bool force, NzbVortexSettings settings)
{
var authCacheKey = string.Format("{0}_{1}_{2}", settings.Host, settings.Port, settings.ApiKey);
if (force)
{
_authCache.Remove(authCacheKey);
}
var sessionId = _authCache.Get(authCacheKey, () => Authenticate(settings));
return sessionId;
}
private string Authenticate(NzbVortexSettings settings)
{
var nonce = GetNonce(settings);
var cnonce = Guid.NewGuid().ToString();
var hashString = string.Format("{0}:{1}:{2}", nonce, cnonce, settings.ApiKey);
var sha256 = hashString.SHA256Hash();
var base64 = Convert.ToBase64String(sha256.HexToByteArray());
var request = BuildRequest("auth/login", Method.GET, false, settings);
request.AddQueryParameter("nonce", nonce);
request.AddQueryParameter("cnonce", cnonce);
request.AddQueryParameter("hash", base64);
var response = ProcessRequest(request, settings);
var result = Json.Deserialize<NzbVortexAuthResponse>(response);
if (result.LoginResult == NzbVortexLoginResultType.Failed)
{
throw new NzbVortexAuthenticationException("Authentication failed, check your API Key");
}
return result.SessionId;
}
private string GetNonce(NzbVortexSettings settings)
{
var request = BuildRequest("auth/nonce", Method.GET, false, settings);
return ProcessRequest<NzbVortexAuthNonceResponse>(request, settings).AuthNonce;
}
private IRestClient BuildClient(NzbVortexSettings settings)
{
var url = string.Format(@"https://{0}:{1}/api", settings.Host, settings.Port);
return RestClientFactory.BuildClient(url);
}
private IRestRequest BuildRequest(string resource, Method method, bool requiresAuthentication, NzbVortexSettings settings)
{
var request = new RestRequest(resource, method);
if (requiresAuthentication)
{
request.AddQueryParameter("sessionid", GetSessionId(false, settings));
}
return request;
}
private T ProcessRequest<T>(IRestRequest request, NzbVortexSettings settings) where T : new()
{
return Json.Deserialize<T>(ProcessRequest(request, settings));
}
private string ProcessRequest(IRestRequest request, NzbVortexSettings settings)
{
var client = BuildClient(settings);
try
{
return ProcessRequest(client, request).Content;
}
catch (NzbVortexNotLoggedInException ex)
{
_logger.Warn("Not logged in response received, reauthenticating and retrying");
request.AddQueryParameter("sessionid", GetSessionId(true, settings));
return ProcessRequest(client, request).Content;
}
}
private IRestResponse ProcessRequest(IRestClient client, IRestRequest request)
{
_logger.Debug("URL: {0}/{1}", client.BaseUrl, request.Resource);
var response = client.Execute(request);
_logger.Trace("Response: {0}", response.Content);
CheckForError(response);
return response;
}
private void CheckForError(IRestResponse response)
{
if (response.ResponseStatus != ResponseStatus.Completed)
{
throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", response.ErrorException);
}
NzbVortexResponseBase result;
if (Json.TryDeserialize<NzbVortexResponseBase>(response.Content, out result))
{
if (result.Result == NzbVortexResultType.NotLoggedIn)
{
throw new NzbVortexNotLoggedInException();
}
}
else
{
throw new DownloadClientException("Response could not be processed: {0}", response.Content);
}
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexQueue
{
[JsonProperty(PropertyName = "nzbs")]
public List<NzbVortexQueueItem> Items { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexQueueItem
{
public int Id { get; set; }
public string UiTitle { get; set; }
public string DestinationPath { get; set; }
public string NzbFilename { get; set; }
public bool IsPaused { get; set; }
public NzbVortexStateType State { get; set; }
public string StatusText { get; set; }
public int TransferedSpeed { get; set; }
public double Progress { get; set; }
public long DownloadedSize { get; set; }
public long TotalDownloadSize { get; set; }
public long PostDate { get; set; }
public int TotalArticleCount { get; set; }
public int FailedArticleCount { get; set; }
public string GroupUUID { get; set; }
public string AddUUID { get; set; }
public string GroupName { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexResultType
{
Ok,
NotLoggedIn,
UnknownCommand
}
}

View File

@@ -0,0 +1,60 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexSettingsValidator : AbstractValidator<NzbVortexSettings>
{
public NzbVortexSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.ApiKey).NotEmpty()
.WithMessage("API Key is required");
RuleFor(c => c.TvCategory).NotEmpty()
.WithMessage("A category is recommended")
.AsWarning();
}
}
public class NzbVortexSettings : IProviderConfig
{
private static readonly NzbVortexSettingsValidator Validator = new NzbVortexSettingsValidator();
public NzbVortexSettings()
{
Host = "localhost";
Port = 4321;
TvCategory = "TV Shows";
RecentTvPriority = (int)NzbVortexPriority.Normal;
OlderTvPriority = (int)NzbVortexPriority.Normal;
}
[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 = "API Key", Type = FieldType.Textbox)]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(4, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(5, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,31 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexStateType
{
Waiting = 0,
Downloading = 1,
WaitingForSave = 2,
Saving = 3,
Saved = 4,
PasswordRequest = 5,
QuaedForProcessing = 6,
UserWaitForProcessing = 7,
Checking = 8,
Repairing = 9,
Joining = 10,
WaitForFurtherProcessing = 11,
Joining2 = 12,
WaitForUncompress = 13,
Uncompressing = 14,
WaitForCleanup = 15,
CleaningUp = 16,
CleanedUp = 17,
MovingToCompleted = 18,
MoveCompleted = 19,
Done = 20,
UncompressFailed = 21,
CheckFailedDataCorrupt = 22,
MoveFailed = 23,
BadlyEncoded = 24
}
}

View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexAddResponse : NzbVortexResponseBase
{
[JsonProperty(PropertyName = "add_uuid")]
public string Id { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexApiVersionResponse : NzbVortexResponseBase
{
public string ApiLevel { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexAuthNonceResponse
{
public string AuthNonce { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using Newtonsoft.Json;
using NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters;
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexAuthResponse : NzbVortexResponseBase
{
[JsonConverter(typeof(NzbVortexLoginResultTypeConverter))]
public NzbVortexLoginResultType LoginResult { get; set; }
public string SessionId { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexGroupResponse : NzbVortexResponseBase
{
public List<NzbVortexGroup> Groups { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using Newtonsoft.Json;
using NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters;
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexResponseBase
{
[JsonConverter(typeof(NzbVortexResultTypeConverter))]
public NzbVortexResultType Result { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexRetryResponse
{
public bool Status { get; set; }
[JsonProperty(PropertyName = "nzo_id")]
public string Id { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexVersionResponse : NzbVortexResponseBase
{
public string Version { get; set; }
}
}

View File

@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
{
get
{
return new ProviderMessage("Sonarr will move files from the Watch folder, it will not hardlink or copy", ProviderMessageType.Warning);
return new ProviderMessage("Magnet links are not supported.", ProviderMessageType.Warning);
}
}
@@ -103,6 +103,8 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
historyItem.RemainingTime = TimeSpan.Zero;
}
historyItem.IsReadOnly = Settings.ReadOnly;
yield return historyItem;
}
@@ -132,6 +134,8 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
historyItem.RemainingTime = TimeSpan.Zero;
}
historyItem.IsReadOnly = Settings.ReadOnly;
yield return historyItem;
}
}

View File

@@ -1,5 +1,6 @@
using FluentValidation;
using System;
using System.ComponentModel;
using FluentValidation;
using Newtonsoft.Json;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -18,6 +19,11 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
public class TorrentBlackholeSettings : IProviderConfig
{
public TorrentBlackholeSettings()
{
ReadOnly = true;
}
private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator();
[FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Sonarr will store the .torrent file")]
@@ -26,6 +32,11 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
[FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Sonarr should import completed downloads")]
public string WatchFolder { get; set; }
[DefaultValue(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[FieldDefinition(2, Label = "Read Only", Type = FieldType.Checkbox, HelpText = "Instead of moving files this will instruct Sonarr to Copy or Hardlink (depending on settings/system configuration)")]
public bool ReadOnly { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -99,6 +99,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
foreach (var torrent in torrents)
{
// If totalsize == 0 the torrent is a magnet downloading metadata
if (torrent.TotalSize == 0)
continue;
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadDir));
if (Settings.TvCategory.IsNotNullOrWhiteSpace())

View File

@@ -1,4 +1,4 @@
using System;
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
RuleFor(c => c.UrlBase).ValidUrlBase();
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$").WithMessage("Allowed characters a-z and -");
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
}
}

View File

@@ -0,0 +1,22 @@
using RestSharp;
using System.Net;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class DigestAuthenticator : IAuthenticator
{
private readonly string _user;
private readonly string _pass;
public DigestAuthenticator(string user, string pass)
{
_user = user;
_pass = pass;
}
public void Authenticate(IRestClient client, IRestRequest request)
{
request.Credentials = new NetworkCredential(_user, _pass);
}
}
}

View File

@@ -0,0 +1,275 @@
using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NLog;
using NzbDrone.Core.Validation;
using FluentValidation.Results;
using System.Net;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
{
private readonly IQBittorrentProxy _proxy;
public QBittorrent(IQBittorrentProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
}
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
}
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
return hash;
}
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
}
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
return hash;
}
public override string Name
{
get
{
return "qBittorrent";
}
}
public override ProviderMessage Message
{
get
{
return new ProviderMessage("Sonarr is unable to remove torrents that have finished seeding when using qBittorrent", ProviderMessageType.Warning);
}
}
public override IEnumerable<DownloadClientItem> GetItems()
{
List<QBittorrentTorrent> torrents;
try
{
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>();
foreach (var torrent in torrents)
{
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper();
item.Category = torrent.Label;
item.Title = torrent.Name;
item.TotalSize = torrent.Size;
item.DownloadClient = Definition.Name;
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
// At the moment there isn't an easy way to detect if the torrent has
// reached the seeding limit, We would need to check the preferences
// and still not be completely sure if that torrent has a limit set for it
item.IsReadOnly = true;
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
{
item.OutputPath += torrent.Name;
}
switch (torrent.State)
{
case "error": // some error occurred, applies to paused torrents
item.Status = DownloadItemStatus.Failed;
item.Message = "QBittorrent is reporting an error";
break;
case "pausedDL": // torrent is paused and has NOT finished downloading
item.Status = DownloadItemStatus.Paused;
break;
case "queuedDL": // queuing is enabled and torrent is queued for download
case "checkingDL": // same as checkingUP, but torrent has NOT finished downloading
item.Status = DownloadItemStatus.Queued;
break;
case "pausedUP": // torrent is paused and has finished downloading
case "uploading": // torrent is being seeded and data is being transfered
case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload
case "checkingUP": // torrent has finished downloading and is being checked
item.Status = DownloadItemStatus.Completed;
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
break;
case "stalledDL": // torrent is being downloaded, but no connection were made
item.Status = DownloadItemStatus.Warning;
item.Message = "The download is stalled with no connections";
break;
case "downloading": // torrent is being downloaded and data is being transfered
default: // new status in API? default to downloading
item.Status = DownloadItemStatus.Downloading;
break;
}
queueItems.Add(item);
}
return queueItems;
}
public override void RemoveItem(string hash, bool deleteData)
{
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
}
public override DownloadClientStatus GetStatus()
{
var config = _proxy.GetConfig(Settings);
var destDir = new OsPath((string)config.GetValueOrDefault("save_path"));
return new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }
};
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
failures.AddIfNotNull(TestGetTorrents());
}
private ValidationFailure TestConnection()
{
try
{
var version = _proxy.GetVersion(Settings);
if (version < 5)
{
// API version 5 introduced the "save_path" property in /query/torrents
return new NzbDroneValidationFailure("Host", "Unsupported client version")
{
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
};
}
else if (version < 6)
{
// API version 6 introduced support for labels
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
return new NzbDroneValidationFailure("Category", "Category is not supported")
{
DetailedDescription = "Labels are not supported until qBittorrent version 3.3.0. Please upgrade or try again with an empty Category."
};
}
}
else if (Settings.TvCategory.IsNullOrWhiteSpace())
{
// warn if labels are supported, but category is not provided
return new NzbDroneValidationFailure("TvCategory", "Category is recommended")
{
IsWarning = true,
DetailedDescription = "Sonarr will not attempt to import completed downloads without a category."
};
}
}
catch (DownloadClientAuthenticationException ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure("Username", "Authentication failure")
{
DetailedDescription = "Please verify your username and password."
};
}
catch (WebException ex)
{
_logger.ErrorException(ex.Message, 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.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
return null;
}
private ValidationFailure TestGetTorrents()
{
try
{
_proxy.GetTorrents(Settings);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message);
}
return null;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentPriority
{
Last = 0,
First = 1
}
}

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Rest;
using RestSharp;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
public interface IQBittorrentProxy
{
int GetVersion(QBittorrentSettings settings);
Dictionary<string, Object> GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
}
public class QBittorrentProxy : IQBittorrentProxy
{
private readonly Logger _logger;
private readonly CookieContainer _cookieContainer;
private readonly ICached<bool> _logins;
private readonly TimeSpan _loginTimeout = TimeSpan.FromSeconds(10);
public QBittorrentProxy(ICacheManager cacheManager, Logger logger)
{
_logger = logger;
_cookieContainer = new CookieContainer();
_logins = cacheManager.GetCache<bool>(GetType(), "logins");
}
public int GetVersion(QBittorrentSettings settings)
{
var request = new RestRequest("/version/api", Method.GET);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
return Convert.ToInt32(response.Content);
}
public Dictionary<string, Object> GetConfig(QBittorrentSettings settings)
{
var request = new RestRequest("/query/preferences", Method.GET);
request.RequestFormat = DataFormat.Json;
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
return response.Read<Dictionary<string, Object>>(client);
}
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
{
var request = new RestRequest("/query/torrents", Method.GET);
request.RequestFormat = DataFormat.Json;
request.AddParameter("label", settings.TvCategory);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
return response.Read<List<QBittorrentTorrent>>(client);
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
{
var request = new RestRequest("/command/download", Method.POST);
request.AddParameter("urls", torrentUrl);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
{
var request = new RestRequest("/command/upload", Method.POST);
request.AddFile("torrents", fileContent, fileName);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
{
var cmd = removeData ? "/command/deletePerm" : "/command/delete";
var request = new RestRequest(cmd, Method.POST);
request.AddParameter("hashes", hash);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
{
var request = new RestRequest("/command/setLabel", Method.POST);
request.AddParameter("hashes", hash);
request.AddParameter("label", label);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
{
var request = new RestRequest("/command/topPrio", Method.POST);
request.AddParameter("hashes", hash);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
if (response.StatusCode == HttpStatusCode.Forbidden)
{
return;
}
response.ValidateResponse(client);
}
private IRestResponse ProcessRequest(IRestClient client, IRestRequest request, QBittorrentSettings settings)
{
var response = client.Execute(request);
if (response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Info("Authentication required, logging in.");
var loggedIn = _logins.Get(settings.Username + settings.Password, () => Login(client, settings), _loginTimeout);
if (!loggedIn)
{
throw new DownloadClientAuthenticationException("Failed to authenticate");
}
// success! retry the original request
response = client.Execute(request);
}
return response;
}
private bool Login(IRestClient client, QBittorrentSettings settings)
{
var request = new RestRequest("/login", Method.POST);
request.AddParameter("username", settings.Username);
request.AddParameter("password", settings.Password);
var response = client.Execute(request);
if (response.StatusCode != HttpStatusCode.OK)
{
_logger.Warn("Login failed with {0}.", response.StatusCode);
return false;
}
if (response.Content != "Ok.") // returns "Fails." on bad login
{
_logger.Warn("Login failed, incorrect username or password.");
return false;
}
response.ValidateResponse(client);
return true;
}
private IRestClient BuildClient(QBittorrentSettings settings)
{
var protocol = settings.UseSsl ? "https" : "http";
var url = String.Format(@"{0}://{1}:{2}", protocol, settings.Host, settings.Port);
var client = RestClientFactory.BuildClient(url);
client.Authenticator = new DigestAuthenticator(settings.Username, settings.Password);
client.CookieContainer = _cookieContainer;
return client;
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrentSettingsValidator : AbstractValidator<QBittorrentSettings>
{
public QBittorrentSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(0, 65535);
}
}
public class QBittorrentSettings : IProviderConfig
{
private static readonly QBittorrentSettingsValidator Validator = new QBittorrentSettingsValidator();
public QBittorrentSettings()
{
Host = "localhost";
Port = 9091;
TvCategory = "tv-sonarr";
}
[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")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// torrent properties from the list returned by /query/torrents
public class QBittorrentTorrent
{
public string Hash { get; set; } // Torrent hash
public string Name { get; set; } // Torrent name
public long Size { get; set; } // Torrent size (bytes)
public double Progress { get; set; } // Torrent progress (%/100)
public int Eta { get; set; } // Torrent ETA (seconds)
public string State { get; set; } // Torrent state. See possible values here below
public string Label { get; set; } // Label of the torrent
[JsonProperty(PropertyName = "save_path")]
public string SavePath { get; set; } // Torrent save path
}
}

View File

@@ -3,6 +3,8 @@ using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.TrackedDownloads;
@@ -72,6 +74,13 @@ namespace NzbDrone.Core.Download
return;
}
if ((OsInfo.IsWindows && !downloadItemOutputPath.IsWindowsPath) ||
(OsInfo.IsNotWindows && !downloadItemOutputPath.IsUnixPath))
{
trackedDownload.Warn("[{0}] is not a valid local path. You may need a Remote Path Mapping.", downloadItemOutputPath);
return;
}
var downloadedEpisodesFolder = new OsPath(_configService.DownloadedEpisodesFolder);
if (downloadedEpisodesFolder.Contains(downloadItemOutputPath))

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;

View File

@@ -51,8 +51,8 @@ namespace NzbDrone.Core.Download
string magnetUrl = null;
string torrentUrl = null;
if (remoteEpisode.Release.DownloadUrl.StartsWith("magnet:"))
if (remoteEpisode.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteEpisode.Release.DownloadUrl.StartsWith("magnet:"))
{
magnetUrl = remoteEpisode.Release.DownloadUrl;
}
@@ -76,11 +76,16 @@ namespace NzbDrone.Core.Download
}
catch (NotSupportedException ex)
{
if (torrentUrl.IsNullOrWhiteSpace())
{
throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message);
}
_logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message);
}
}
if (hash == null && !torrentUrl.IsNullOrWhiteSpace())
if (hash == null && torrentUrl.IsNotNullOrWhiteSpace())
{
hash = DownloadFromWebUrl(remoteEpisode, torrentUrl);
}

View File

@@ -75,7 +75,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads
trackedDownload.RemoteEpisode.Series == null ||
trackedDownload.RemoteEpisode.Episodes.Empty())
{
parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle);
// Try parsing the original source title and if that fails, try parsing it as a special
// TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item
parsedEpisodeInfo = Parser.Parser.ParseTitle(firstHistoryItem.SourceTitle) ?? _parsingService.ParseSpecialEpisodeTitle(firstHistoryItem.SourceTitle, 0, 0);
if (parsedEpisodeInfo != null)
{
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, firstHistoryItem.SeriesId, historyItems.Where(v => v.EventType == HistoryEventType.Grabbed).Select(h => h.EpisodeId).Distinct());

View File

@@ -25,17 +25,19 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Warning, "No download client is available");
}
try
foreach (var downloadClient in downloadClients)
{
foreach (var downloadClient in downloadClients)
try
{
downloadClient.GetItems();
}
}
catch (Exception ex)
{
_logger.Error("Unable to communicate with download client: ", ex);
return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to communicate with download client: " + ex.Message);
catch (Exception ex)
{
var message = String.Format("Unable to communicate with {0}.", downloadClient.Definition.Name);
_logger.Error(message, ex);
return new HealthCheck(GetType(), HealthCheckResult.Error, message + " " + ex.Message);
}
}
return new HealthCheck(GetType());

View File

@@ -27,7 +27,8 @@ namespace NzbDrone.Core.HealthCheck.Checks
public override HealthCheck Check()
{
if (OsInfo.IsWindows || _configFileProvider.UpdateAutomatically)
if ((OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) &&
_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn)
{
if (!_diskProvider.FolderWritable(_appFolderInfo.StartUpFolder))
{

View File

@@ -1,4 +1,4 @@
using System;
using System.Linq;
namespace NzbDrone.Core.IndexerSearch.Definitions
{
@@ -8,6 +8,13 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public override string ToString()
{
var episodeTitles = EpisodeQueryTitles.ToList();
if (episodeTitles.Count > 0)
{
return string.Format("[{0}] Specials", Series.Title);
}
return string.Format("[{0} : {1}]", Series.Title, string.Join(",", EpisodeQueryTitles));
}
}

View File

@@ -50,12 +50,30 @@ namespace NzbDrone.Core.IndexerSearch
if (season.Count() > 1)
{
decisions = _nzbSearchService.SeasonSearch(series.Key, season.Key, true);
try
{
decisions = _nzbSearchService.SeasonSearch(series.Key, season.Key, true);
}
catch (Exception ex)
{
var message = String.Format("Unable to search for missing episodes in season {0} of [{1}]", season.Key, series.Key);
_logger.ErrorException(message, ex);
continue;
}
}
else
{
decisions = _nzbSearchService.EpisodeSearch(season.First());
try
{
decisions = _nzbSearchService.EpisodeSearch(season.First());
}
catch (Exception ex)
{
var message = String.Format("Unable to search for missing episode: [{0}]", season.First());
_logger.ErrorException(message, ex);
continue;
}
}
var processed = _processDownloadDecisions.ProcessDecisions(decisions);

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