mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-28 18:05:41 -04:00
Compare commits
34 Commits
v2.0.0.364
...
v2.0.0.373
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b72c0e328 | ||
|
|
536aa350f0 | ||
|
|
7c382c0e0c | ||
|
|
dda0d3259f | ||
|
|
a96718f7b3 | ||
|
|
7ca67fe57a | ||
|
|
8373024f9d | ||
|
|
b62ef0c40c | ||
|
|
376481eda5 | ||
|
|
800fa42982 | ||
|
|
4e728c3a02 | ||
|
|
663d254ced | ||
|
|
8753c232c7 | ||
|
|
221f3ef08c | ||
|
|
b59175a87c | ||
|
|
37c621dcdb | ||
|
|
eaf3228bb7 | ||
|
|
c8debbf470 | ||
|
|
8dcd8d17b5 | ||
|
|
cfe121c777 | ||
|
|
b4f83d8a4e | ||
|
|
9039d7e694 | ||
|
|
7a25717da6 | ||
|
|
7e1c444c02 | ||
|
|
dc3f7c9bda | ||
|
|
de754169fb | ||
|
|
2d3c3bbb0c | ||
|
|
15cefe4a43 | ||
|
|
95da301975 | ||
|
|
e03906b294 | ||
|
|
7921dd3f96 | ||
|
|
9f066f7a6b | ||
|
|
81d131e732 | ||
|
|
0552b56b71 |
1
.idea/jsLibraryMappings.xml
generated
1
.idea/jsLibraryMappings.xml
generated
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,10 @@ namespace NzbDrone.Api.Episodes
|
||||
{
|
||||
_episodeService.SetEpisodeMonitored(episodeResource.Id, episodeResource.Monitored);
|
||||
}
|
||||
|
||||
protected override List<EpisodeResource> LoadSeries(List<EpisodeResource> resources)
|
||||
{
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
34
src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs
Normal file
34
src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +349,33 @@ namespace NzbDrone.Common.Test.Http
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class HttpBinResource
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +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 | 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),
|
||||
@@ -27,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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
GivenSpecifications(_pass1, _pass2, _pass3);
|
||||
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
|
||||
|
||||
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.Bluray1080p)}, true);
|
||||
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.SDTV)}, true);
|
||||
|
||||
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
|
||||
}
|
||||
@@ -207,6 +207,22 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||
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);
|
||||
|
||||
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_throw_if_episodes_are_not_found()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -45,6 +45,17 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
265
src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs
Normal file
265
src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
{
|
||||
public class NzbVortexGroup
|
||||
{
|
||||
public string GroupName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
{
|
||||
public enum NzbVortexLoginResultType
|
||||
{
|
||||
Successful,
|
||||
Failed
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
{
|
||||
public enum NzbVortexPriority
|
||||
{
|
||||
Low = -1,
|
||||
Normal = 0,
|
||||
High = 1,
|
||||
}
|
||||
}
|
||||
235
src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs
Normal file
235
src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||
{
|
||||
public enum NzbVortexResultType
|
||||
{
|
||||
Ok,
|
||||
NotLoggedIn,
|
||||
UnknownCommand
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
|
||||
{
|
||||
public class NzbVortexApiVersionResponse : NzbVortexResponseBase
|
||||
{
|
||||
public string ApiLevel { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
|
||||
{
|
||||
public class NzbVortexAuthNonceResponse
|
||||
{
|
||||
public string AuthNonce { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
|
||||
{
|
||||
public class NzbVortexVersionResponse : NzbVortexResponseBase
|
||||
{
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
275
src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrent.cs
Normal file
275
src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
public enum QBittorrentPriority
|
||||
{
|
||||
Last = 0,
|
||||
First = 1
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -44,15 +44,16 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return GetDefinition("Nzbs.org", GetSettings("http://nzbs.org", 5000));
|
||||
yield return GetDefinition("NZBFinder.ws", GetSettings("https://www.nzbfinder.ws"));
|
||||
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"));
|
||||
yield return GetDefinition("Dognzb.cr", GetSettings("https://api.dognzb.cr"));
|
||||
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
|
||||
yield return GetDefinition("nzbplanet.net", GetSettings("https://nzbplanet.net"));
|
||||
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
||||
yield return GetDefinition("PFmonkey", GetSettings("https://www.pfmonkey.com"));
|
||||
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"));
|
||||
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
|
||||
yield return GetDefinition("NZBFinder.ws", GetSettings("https://www.nzbfinder.ws"));
|
||||
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
||||
yield return GetDefinition("nzbplanet.net", GetSettings("https://nzbplanet.net"));
|
||||
yield return GetDefinition("Nzbs.org", GetSettings("http://nzbs.org", 5000));
|
||||
yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com"));
|
||||
yield return GetDefinition("PFmonkey", GetSettings("https://www.pfmonkey.com"));
|
||||
yield return GetDefinition("Usenet Crawler", GetSettings("https://www.usenet-crawler.com"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,17 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
foreach (var queryTitle in searchCriteria.EpisodeQueryTitles)
|
||||
{
|
||||
var query = queryTitle.Replace('+', ' ');
|
||||
query = System.Web.HttpUtility.UrlEncode(query);
|
||||
|
||||
pageableRequests.Add(GetPagedRequests("search", searchCriteria.Series.TvdbId, query));
|
||||
}
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string mode, int? tvdbId, string query, params object[] args)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TitansOfTv
|
||||
{
|
||||
public class TitansOfTv : HttpIndexerBase<TitansOfTvSettings>
|
||||
{
|
||||
public TitansOfTv(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
: base(httpClient, indexerStatusService, configService, parsingService, logger)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get { return "Titans of TV"; }
|
||||
}
|
||||
|
||||
public override DownloadProtocol Protocol { get { return DownloadProtocol.Torrent; } }
|
||||
public override bool SupportsRss { get { return true; } }
|
||||
public override bool SupportsSearch { get { return true; } }
|
||||
public override int PageSize { get { return 100; } }
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new TitansOfTvRequestGenerator() { Settings = Settings, PageSize = PageSize };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new TitansOfTvParser();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TitansOfTv
|
||||
{
|
||||
public class TitansOfTvApiResult
|
||||
{
|
||||
public string code { get; set; }
|
||||
public int http_code { get; set; }
|
||||
public int total { get; set; }
|
||||
public int offset { get; set; }
|
||||
public int limit { get; set; }
|
||||
public List<TitansOfTvTorrent> results { get; set; }
|
||||
}
|
||||
|
||||
public class TitansOfTvTorrent
|
||||
{
|
||||
public string id { get; set; }
|
||||
public string series_id { get; set; }
|
||||
public string episode_id { get; set; }
|
||||
public string season_id { get; set; }
|
||||
public int? seeders { get; set; }
|
||||
public int? leechers { get; set; }
|
||||
public long size { get; set; }
|
||||
public int? snatched { get; set; }
|
||||
public int user_id { get; set; }
|
||||
public string anonymous { get; set; }
|
||||
public string container { get; set; }
|
||||
public string codec { get; set; }
|
||||
public string source { get; set; }
|
||||
public string resolution { get; set; }
|
||||
public string origin { get; set; }
|
||||
public string language { get; set; }
|
||||
public string release_name { get; set; }
|
||||
public string tracker_updated_at { get; set; }
|
||||
public DateTime created_at { get; set; }
|
||||
public DateTime updated_at { get; set; }
|
||||
public string season { get; set; }
|
||||
public string episode { get; set; }
|
||||
public string series { get; set; }
|
||||
public string network { get; set; }
|
||||
public string mediainfo { get; set; }
|
||||
public string download { get; set; }
|
||||
public string additional { get; set; }
|
||||
public string episodeUrl { get; set; }
|
||||
public string commentUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TitansOfTv
|
||||
{
|
||||
public class TitansOfTvParser : IParseIndexerResponse
|
||||
{
|
||||
private static readonly Regex RegexProtocol = new Regex("^https?:", RegexOptions.Compiled);
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var results = new List<ReleaseInfo>();
|
||||
|
||||
switch (indexerResponse.HttpResponse.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new ApiKeyException("API Key invalid or not authorized");
|
||||
case HttpStatusCode.NotFound:
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned NotFound, the Indexer API may have changed.");
|
||||
case HttpStatusCode.ServiceUnavailable:
|
||||
throw new RequestLimitReachedException("Indexer API is temporarily unavailable, try again later");
|
||||
default:
|
||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var content = indexerResponse.HttpResponse.Content;
|
||||
var parsed = Json.Deserialize<TitansOfTvApiResult>(content);
|
||||
var protocol = indexerResponse.HttpRequest.Url.Scheme + ":";
|
||||
|
||||
foreach (var parsedItem in parsed.results)
|
||||
{
|
||||
var release = new TorrentInfo();
|
||||
release.Guid = string.Format("ToTV-{0}", parsedItem.id);
|
||||
release.DownloadUrl = RegexProtocol.Replace(parsedItem.download, protocol);
|
||||
release.InfoUrl = RegexProtocol.Replace(parsedItem.episodeUrl, protocol);
|
||||
if (parsedItem.commentUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
release.CommentUrl = RegexProtocol.Replace(parsedItem.commentUrl, protocol);
|
||||
}
|
||||
release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
release.Title = parsedItem.release_name;
|
||||
release.Size = parsedItem.size;
|
||||
release.Seeders = parsedItem.seeders;
|
||||
release.Peers = parsedItem.leechers + release.Seeders;
|
||||
release.PublishDate = parsedItem.created_at;
|
||||
results.Add(release);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TitansOfTv
|
||||
{
|
||||
public class TitansOfTvRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public int MaxPages { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public TitansOfTvSettings Settings { get; set; }
|
||||
|
||||
public TitansOfTvRequestGenerator()
|
||||
{
|
||||
MaxPages = 30;
|
||||
PageSize = 100;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetRecentRequests()
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages,
|
||||
series_id: searchCriteria.Series.TvdbId,
|
||||
episode: string.Format("S{0:00}E{1:00}", searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber)));
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages,
|
||||
series_id: searchCriteria.Series.TvdbId,
|
||||
season: string.Format("Season {0:00}", searchCriteria.SeasonNumber)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages,
|
||||
series_id: searchCriteria.Series.TvdbId,
|
||||
season: string.Format("Season {0:00}", searchCriteria.SeasonNumber)));
|
||||
|
||||
pageableRequests.AddTier();
|
||||
|
||||
// TODO: Search for all episodes?!?
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(MaxPages,
|
||||
series_id: searchCriteria.Series.TvdbId,
|
||||
air_date: searchCriteria.AirDate));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, int? series_id = null, string episode = null, string season = null, DateTime? air_date = null)
|
||||
{
|
||||
var pageSize = PageSize;
|
||||
|
||||
if (pageSize == 0)
|
||||
{
|
||||
maxPages = 1;
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
for (var page = 0; page < maxPages; page++)
|
||||
{
|
||||
var request = new IndexerRequest(string.Format("{0}/torrents?offset={1}&limit={2}", Settings.BaseUrl.TrimEnd('/'), page * pageSize, pageSize), HttpAccept.Json);
|
||||
request.HttpRequest.Headers.Add("X-Authorization", Settings.ApiKey);
|
||||
|
||||
if (series_id.HasValue)
|
||||
{
|
||||
request.HttpRequest.AddQueryParam("series_id", series_id.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
request.HttpRequest.AddQueryParam("season", season);
|
||||
}
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
request.HttpRequest.AddQueryParam("episode", episode);
|
||||
}
|
||||
|
||||
if (air_date.HasValue)
|
||||
{
|
||||
request.HttpRequest.AddQueryParam("air_date", air_date.Value.ToString("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TitansOfTv
|
||||
{
|
||||
public class TitansOfTvSettingsValidator : AbstractValidator<TitansOfTvSettings>
|
||||
{
|
||||
public TitansOfTvSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class TitansOfTvSettings : IProviderConfig
|
||||
{
|
||||
private static readonly TitansOfTvSettingsValidator Validator = new TitansOfTvSettingsValidator();
|
||||
|
||||
public TitansOfTvSettings()
|
||||
{
|
||||
BaseUrl = "http://titansof.tv/api";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "API key", HelpText = "Enter your ToTV API key. (My Account->API->Site API Key)")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,11 @@ namespace NzbDrone.Core.Jobs
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (interval < 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
|
||||
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
|
||||
{
|
||||
if (folderInfo != null && folderInfo.Quality.Quality != Quality.Unknown && fileQuality.QualitySource == QualitySource.Extension)
|
||||
if (UseFolderQuality(folderInfo, fileQuality, series))
|
||||
{
|
||||
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
|
||||
return folderInfo.Quality;
|
||||
@@ -189,5 +189,30 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||
|
||||
return fileQuality;
|
||||
}
|
||||
|
||||
private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
|
||||
{
|
||||
if (folderInfo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folderInfo.Quality.Quality == Quality.Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fileQuality.QualitySource == QualitySource.Extension)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (new QualityModelComparer(series.Profile).Compare(folderInfo.Quality, fileQuality) > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
_fileExtensions = new Dictionary<string, Quality>
|
||||
{
|
||||
//Unknown
|
||||
{ ".webm", Quality.Unknown },
|
||||
|
||||
//SDTV
|
||||
{ ".m4v", Quality.SDTV },
|
||||
{ ".3gp", Quality.SDTV },
|
||||
{ ".nsv", Quality.SDTV },
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
{
|
||||
nextStep = "step2",
|
||||
action = "openWindow",
|
||||
url = _twitterService.GetOAuthRedirect(query["callbackUrl"].ToString())
|
||||
url = _twitterService.GetOAuthRedirect(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["callbackUrl"].ToString())
|
||||
};
|
||||
}
|
||||
else if (stage == "step2")
|
||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
return new
|
||||
{
|
||||
action = "updateFields",
|
||||
fields = _twitterService.GetOAuthToken(query["oauth_token"].ToString(), query["oauth_verifier"].ToString())
|
||||
fields = _twitterService.GetOAuthToken(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["oauth_token"].ToString(), query["oauth_verifier"].ToString())
|
||||
};
|
||||
}
|
||||
return new {};
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
{
|
||||
void SendNotification(string message, TwitterSettings settings);
|
||||
ValidationFailure Test(TwitterSettings settings);
|
||||
string GetOAuthRedirect(string callbackUrl);
|
||||
object GetOAuthToken(string oauthToken, string oauthVerifier);
|
||||
string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl);
|
||||
object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
|
||||
}
|
||||
|
||||
public class TwitterService : ITwitterService
|
||||
@@ -24,8 +24,8 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private static string _consumerKey = "5jSR8a3cp0ToOqSMLMv5GtMQD";
|
||||
private static string _consumerSecret = "dxoZjyMq4BLsC8KxyhSOrIndhCzJ0Dik2hrLzqyJcqoGk4Pfsp";
|
||||
// private static string _consumerKey = "5jSR8a3cp0ToOqSMLMv5GtMQD";
|
||||
// private static string _consumerSecret = "dxoZjyMq4BLsC8KxyhSOrIndhCzJ0Dik2hrLzqyJcqoGk4Pfsp";
|
||||
|
||||
public TwitterService(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
@@ -43,10 +43,10 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
return HttpUtility.ParseQueryString(response.Content);
|
||||
}
|
||||
|
||||
public object GetOAuthToken(string oauthToken, string oauthVerifier)
|
||||
public object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
|
||||
{
|
||||
// Creating a new instance with a helper method
|
||||
var oAuthRequest = OAuthRequest.ForAccessToken(_consumerKey, _consumerSecret, oauthToken, "", oauthVerifier);
|
||||
var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier);
|
||||
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token";
|
||||
var qscoll = OAuthQuery(oAuthRequest);
|
||||
|
||||
@@ -57,10 +57,10 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
};
|
||||
}
|
||||
|
||||
public string GetOAuthRedirect(string callbackUrl)
|
||||
public string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl)
|
||||
{
|
||||
// Creating a new instance with a helper method
|
||||
var oAuthRequest = OAuthRequest.ForRequestToken(_consumerKey, _consumerSecret, callbackUrl);
|
||||
var oAuthRequest = OAuthRequest.ForRequestToken(consumerKey, consumerSecret, callbackUrl);
|
||||
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/request_token";
|
||||
var qscoll = OAuthQuery(oAuthRequest);
|
||||
|
||||
@@ -73,10 +73,10 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
{
|
||||
var oAuth = new TinyTwitter.OAuthInfo
|
||||
{
|
||||
ConsumerKey = settings.ConsumerKey,
|
||||
ConsumerSecret = settings.ConsumerSecret,
|
||||
AccessToken = settings.AccessToken,
|
||||
AccessSecret = settings.AccessTokenSecret,
|
||||
ConsumerKey = _consumerKey,
|
||||
ConsumerSecret = _consumerSecret
|
||||
AccessSecret = settings.AccessTokenSecret
|
||||
};
|
||||
|
||||
var twitter = new TinyTwitter.TinyTwitter(oAuth);
|
||||
@@ -96,9 +96,9 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
twitter.UpdateStatus(message);
|
||||
}
|
||||
}
|
||||
catch (WebException e)
|
||||
catch (WebException ex)
|
||||
{
|
||||
using (var response = e.Response)
|
||||
using (var response = ex.Response)
|
||||
{
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
|
||||
@@ -107,14 +107,14 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
if (responseStream == null)
|
||||
{
|
||||
_logger.Trace("Status Code: {0}", httpResponse.StatusCode);
|
||||
throw new TwitterException("Error received from Twitter: " + httpResponse.StatusCode, _logger , e);
|
||||
throw new TwitterException("Error received from Twitter: " + httpResponse.StatusCode, ex);
|
||||
}
|
||||
|
||||
using (var reader = new StreamReader(responseStream))
|
||||
{
|
||||
var responseBody = reader.ReadToEnd();
|
||||
_logger.Trace("Reponse: {0} Status Code: {1}", responseBody, httpResponse.StatusCode);
|
||||
throw new TwitterException("Error received from Twitter: " + responseBody, _logger, e);
|
||||
throw new TwitterException("Error received from Twitter: " + responseBody, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
{
|
||||
public TwitterSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.ConsumerKey).NotEmpty();
|
||||
RuleFor(c => c.ConsumerSecret).NotEmpty();
|
||||
RuleFor(c => c.AccessToken).NotEmpty();
|
||||
RuleFor(c => c.AccessTokenSecret).NotEmpty();
|
||||
//TODO: Validate that it is a valid username (numbers, letters and underscores - I think)
|
||||
@@ -30,19 +32,25 @@ namespace NzbDrone.Core.Notifications.Twitter
|
||||
AuthorizeNotification = "step1";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Access Token", Advanced = true)]
|
||||
[FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")]
|
||||
public string ConsumerKey { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Consumer Secret", HelpText = "Consumer secret from a Twitter application", HelpLink = "https://github.com/Sonarr/Sonarr/wiki/Twitter-Notifications")]
|
||||
public string ConsumerSecret { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Access Token", Advanced = true)]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Access Token Secret", Advanced = true)]
|
||||
[FieldDefinition(3, Label = "Access Token Secret", Advanced = true)]
|
||||
public string AccessTokenSecret { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Mention", HelpText = "Mention this user in sent tweets")]
|
||||
[FieldDefinition(4, Label = "Mention", HelpText = "Mention this user in sent tweets")]
|
||||
public string Mention { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")]
|
||||
[FieldDefinition(5, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")]
|
||||
public bool DirectMessage { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Connect to twitter", Type = FieldType.Action)]
|
||||
[FieldDefinition(6, Label = "Connect to twitter", Type = FieldType.Action)]
|
||||
public string AuthorizeNotification { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -273,6 +273,9 @@
|
||||
<Compile Include="Datastore\Migration\093_naming_config_replace_characters.cs" />
|
||||
<Compile Include="Datastore\Migration\092_add_unverifiedscenenumbering.cs" />
|
||||
<Compile Include="Datastore\Migration\094_add_tvmazeid.cs" />
|
||||
<Compile Include="Datastore\Migration\098_remove_titans_of_tv.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
|
||||
@@ -354,9 +357,42 @@
|
||||
<Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" />
|
||||
<Compile Include="Download\Clients\Nzbget\NzbgetResponse.cs" />
|
||||
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\JsonConverters\NzbVortexLoginResultTypeConverter.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\JsonConverters\NzbVortexResultTypeConverter.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortex.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexGroup.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexNotLoggedInException.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexAuthenticationException.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexJsonError.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexPriority.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexProxy.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexFiles.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueue.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexFile.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueueItem.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexLoginResultType.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexStateType.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexResultType.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexSettings.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAddResponse.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthNonceResponse.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthResponse.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexGroupResponse.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexResponseBase.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexRetryResponse.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexApiVersionResponse.cs" />
|
||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexVersionResponse.cs" />
|
||||
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
||||
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" />
|
||||
<Compile Include="Download\Clients\qBittorrent\DigestAuthenticator.cs" />
|
||||
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
||||
<Compile Include="Download\Clients\qBittorrent\QBittorrent.cs" />
|
||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentPriority.cs" />
|
||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentProxy.cs" />
|
||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentSettings.cs" />
|
||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentTorrent.cs" />
|
||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
|
||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" />
|
||||
<Compile Include="Download\Clients\Sabnzbd\Responses\SabnzbdRetryResponse.cs" />
|
||||
@@ -548,11 +584,6 @@
|
||||
<Compile Include="Indexers\RssSyncCommand.cs" />
|
||||
<Compile Include="Indexers\RssSyncCompleteEvent.cs" />
|
||||
<Compile Include="Indexers\RssSyncService.cs" />
|
||||
<Compile Include="Indexers\TitansOfTv\TitansOfTv.cs" />
|
||||
<Compile Include="Indexers\TitansOfTv\TitansOfTvApiResult.cs" />
|
||||
<Compile Include="Indexers\TitansOfTv\TitansOfTvParser.cs" />
|
||||
<Compile Include="Indexers\TitansOfTv\TitansOfTvRequestGenerator.cs" />
|
||||
<Compile Include="Indexers\TitansOfTv\TitansOfTvSettings.cs" />
|
||||
<Compile Include="Indexers\Torrentleech\TorrentleechRequestGenerator.cs" />
|
||||
<Compile Include="Indexers\Torrentleech\Torrentleech.cs" />
|
||||
<Compile Include="Indexers\Torrentleech\TorrentleechSettings.cs" />
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace NzbDrone.Core.Parser
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc)
|
||||
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
|
||||
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_. ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) **
|
||||
@@ -86,6 +86,10 @@ namespace NzbDrone.Core.Parser
|
||||
new Regex(@"(?:.*(?:^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{2}(?!\d+))(?:E(?<episode>(?<!\d+)\d{2}(?!\d+)))+",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//Multi-episode with single episode numbers (S6.E1-E2, S6.E1E2, S6E1E2, etc)
|
||||
new Regex(@"^(?<title>.+?)[-_. ]S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[-_. ]?[ex]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//Single episode season or episode S1E1 or S1-E1
|
||||
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E(?<episode>(?<!\d+)\d{1,2}(?!\d+))",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
@@ -305,14 +305,17 @@ namespace NzbDrone.Core.Parser
|
||||
}
|
||||
|
||||
var series = GetSeries(title);
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
series = _seriesService.FindByTitleInexact(title);
|
||||
}
|
||||
|
||||
if (series == null && tvdbId > 0)
|
||||
{
|
||||
series = _seriesService.FindByTvdbId(tvdbId);
|
||||
}
|
||||
|
||||
if (series == null && tvRageId > 0)
|
||||
{
|
||||
series = _seriesService.FindByTvRageId(tvRageId);
|
||||
|
||||
1
src/UI/.idea/encodings.xml
generated
1
src/UI/.idea/encodings.xml
generated
@@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false">
|
||||
<file url="file://$PROJECT_DIR$/System/Logs/Files/LogFileModel.js" charset="UTF-8" />
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
3
src/UI/.idea/misc.xml
generated
3
src/UI/.idea/misc.xml
generated
@@ -4,5 +4,4 @@
|
||||
<option name="state" value="git@github.com:NzbDrone/NzbDrone.git" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" />
|
||||
</project>
|
||||
|
||||
</project>
|
||||
@@ -56,7 +56,7 @@ module.exports = Marionette.Layout.extend({
|
||||
},
|
||||
{
|
||||
name : 'timeleft',
|
||||
label : 'Timeleft',
|
||||
label : 'Time Left',
|
||||
cell : TimeleftCell,
|
||||
cellValue : 'this'
|
||||
},
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
width : 500px;
|
||||
|
||||
@media (max-width: @screen-xs-min) {
|
||||
width : 400px;
|
||||
width : 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,24 @@ window.NzbDrone.imageError = function(img) {
|
||||
};
|
||||
|
||||
Handlebars.registerHelper('defaultImg', function(src, size) {
|
||||
var endOfPath = /\.jpg($|\?)/g;
|
||||
var errorAttr = 'onerror="window.NzbDrone.imageError(this);"';
|
||||
var srcsetAttr = '';
|
||||
var oneX = src, twoX;
|
||||
|
||||
if (!src) {
|
||||
return new Handlebars.SafeString('onerror="window.NzbDrone.imageError(this);"');
|
||||
return new Handlebars.SafeString(errorAttr);
|
||||
}
|
||||
|
||||
if (size) {
|
||||
src = src.replace(/\.jpg($|\?)/g, '-' + size + '.jpg$1');
|
||||
oneX = src.replace(endOfPath, '-' + size + '.jpg$1');
|
||||
twoX = src.replace(endOfPath, '-' + size * 2 + '.jpg$1');
|
||||
srcsetAttr = 'srcset="{0} 1x, {1} 2x"'.format(oneX, twoX);
|
||||
}
|
||||
|
||||
return new Handlebars.SafeString('src="{0}" onerror="window.NzbDrone.imageError(this);"'.format(src));
|
||||
return new Handlebars.SafeString(
|
||||
'src="{0}" {1} {2}'.format(oneX, srcsetAttr, errorAttr)
|
||||
);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('UrlBase', function() {
|
||||
|
||||
@@ -26,13 +26,8 @@ module.exports = Marionette.Layout.extend({
|
||||
],
|
||||
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
|
||||
this.seriesCollection = SeriesCollection.clone();
|
||||
|
||||
_.each(this.seriesCollection.models, function (model) {
|
||||
model.collection = self.seriesCollection;
|
||||
});
|
||||
this._setModelCollection();
|
||||
|
||||
this.listenTo(this.seriesCollection, 'row:selected', this._onSelected);
|
||||
this.listenTo(this, 'modal:afterShow', this._setFocus);
|
||||
@@ -83,6 +78,7 @@ module.exports = Marionette.Layout.extend({
|
||||
|
||||
_filter : function (term) {
|
||||
this.seriesCollection.setFilter(['title', term, 'contains']);
|
||||
this._setModelCollection();
|
||||
},
|
||||
|
||||
_onSelected : function (e) {
|
||||
@@ -93,5 +89,13 @@ module.exports = Marionette.Layout.extend({
|
||||
|
||||
_setFocus : function () {
|
||||
this.ui.filter.focus();
|
||||
},
|
||||
|
||||
_setModelCollection: function () {
|
||||
var self = this;
|
||||
|
||||
_.each(this.seriesCollection.models, function (model) {
|
||||
model.collection = self.seriesCollection;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,14 +28,17 @@ module.exports = Marionette.Layout.extend({
|
||||
},
|
||||
|
||||
onClick : function(event) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var target = $(event.target);
|
||||
|
||||
//look down for <a/>
|
||||
var href = event.target.getAttribute('href');
|
||||
|
||||
if (href && href.startsWith("http")) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
//if couldn't find it look up'
|
||||
if (!href && target.closest('a') && target.closest('a')[0]) {
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2 col-sm-pull-1">
|
||||
<input type="number" name="rssSyncInterval" class="form-control"/>
|
||||
<input type="number" name="rssSyncInterval" class="form-control" min="0" max="120"/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
var _ = require('underscore');
|
||||
var vent = require('vent');
|
||||
var Marionette = require('marionette');
|
||||
var DeleteView = require('../Delete/NotificationDeleteView');
|
||||
@@ -86,10 +87,20 @@ var view = Marionette.ItemView.extend({
|
||||
},
|
||||
|
||||
_onAuthorizeNotification : function() {
|
||||
this.ui.indicator.show();
|
||||
|
||||
var self = this;
|
||||
var callbackUrl = window.location.origin + '/oauth.html';
|
||||
this.ui.indicator.show();
|
||||
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value') + '?callbackUrl=' + callbackUrl);
|
||||
var fields = this.model.get('fields');
|
||||
var consumerKeyObj = _.findWhere(fields, { name: 'ConsumerKey' });
|
||||
var consumerSecretObj = _.findWhere(fields, { name: 'ConsumerSecret' });
|
||||
var queryParams = {
|
||||
callbackUrl: callbackUrl,
|
||||
consumerKey: (consumerKeyObj ? consumerKeyObj.value : ''),
|
||||
consumerSecret: (consumerSecretObj ? consumerSecretObj.value : '')
|
||||
};
|
||||
|
||||
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value'), queryParams);
|
||||
|
||||
promise.always(function() {
|
||||
self.ui.indicator.hide();
|
||||
|
||||
@@ -4,14 +4,19 @@ var DeepModel = require('backbone.deepmodel');
|
||||
var Messenger = require('../Shared/Messenger');
|
||||
|
||||
module.exports = DeepModel.extend({
|
||||
connectData : function(action, initialQueryString) {
|
||||
connectData : function(action, initialQueryParams) {
|
||||
var self = this;
|
||||
|
||||
this.trigger('connect:sync');
|
||||
|
||||
var promise = $.Deferred();
|
||||
|
||||
var callAction = function(action) {
|
||||
var callAction = function(action, queryParams) {
|
||||
|
||||
if (queryParams) {
|
||||
action = action + '?' + $.param(queryParams, true);
|
||||
}
|
||||
|
||||
var params = {
|
||||
url : self.collection.url + '/connectData/' + action,
|
||||
contentType : 'application/json',
|
||||
@@ -30,11 +35,20 @@ module.exports = DeepModel.extend({
|
||||
{
|
||||
window.open(response.url);
|
||||
var selfWindow = window;
|
||||
|
||||
selfWindow.onCompleteOauth = function(query, callback) {
|
||||
delete selfWindow.onCompleteOauth;
|
||||
|
||||
if (response.nextStep) {
|
||||
callAction(response.nextStep + query);
|
||||
var queryParams = {};
|
||||
var splitQuery = query.substring(1).split('&');
|
||||
|
||||
_.each(splitQuery, function (param) {
|
||||
var paramSplit = param.split('=');
|
||||
queryParams[paramSplit[0]] = paramSplit[1];
|
||||
});
|
||||
|
||||
callAction(response.nextStep, _.extend(initialQueryParams, queryParams));
|
||||
}
|
||||
else {
|
||||
promise.resolve(response);
|
||||
@@ -59,7 +73,7 @@ module.exports = DeepModel.extend({
|
||||
}
|
||||
}
|
||||
if (response.nextStep) {
|
||||
callAction(response.nextStep);
|
||||
callAction(response.nextStep, initialQueryParams);
|
||||
}
|
||||
else {
|
||||
promise.resolve(response);
|
||||
@@ -67,7 +81,7 @@ module.exports = DeepModel.extend({
|
||||
});
|
||||
};
|
||||
|
||||
callAction(action, initialQueryString);
|
||||
callAction(action, initialQueryParams);
|
||||
|
||||
Messenger.monitor({
|
||||
promise : promise,
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
|
||||
<div class="col-sm-4 col-sm-pull-1">
|
||||
<select name="calendarWeekColumnHeader" class="form-control">
|
||||
<option value="ddd M/D">Tue 3/25</option>
|
||||
<option value="ddd MM/DD">Tue 03/25</option>
|
||||
<option value="ddd D/M">Tue 25/3</option>
|
||||
<option value="ddd DD/MM">Tue 25/03</option>
|
||||
<option value="ddd M/D">Tue 3/5</option>
|
||||
<option value="ddd MM/DD">Tue 03/05</option>
|
||||
<option value="ddd D/M">Tue 5/3</option>
|
||||
<option value="ddd DD/MM">Tue 05/03</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,10 +40,10 @@
|
||||
<div class="col-sm-4">
|
||||
<select name="shortDateFormat" class="form-control">
|
||||
<option value="MMM D YYYY">Mar 5 2014</option>
|
||||
<option value="DD MMM YYYY">5 Mar 2014</option>
|
||||
<option value="DD MMM YYYY">05 Mar 2014</option>
|
||||
<option value="MM/D/YYYY">03/5/2014</option>
|
||||
<option value="MM/DD/YYYY">03/05/2014</option>
|
||||
<option value="DD/MM/YYYY">25/03/2014</option>
|
||||
<option value="DD/MM/YYYY">05/03/2014</option>
|
||||
<option value="YYYY-MM-DD">2014-03-05</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -54,8 +54,8 @@
|
||||
|
||||
<div class="col-sm-4">
|
||||
<select name="longDateFormat" class="form-control">
|
||||
<option value="dddd, MMMM D YYYY">Tuesday, March 25, 2014</option>
|
||||
<option value="dddd, D MMMM YYYY">Tuesday, 25 March, 2014</option>
|
||||
<option value="dddd, MMMM D YYYY">Tuesday, March 5, 2014</option>
|
||||
<option value="dddd, D MMMM YYYY">Tuesday, 5 March, 2014</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>oauth landing page</title>
|
||||
<script><!--
|
||||
<script><!--
|
||||
window.opener.onCompleteOauth(window.location.search, function() { window.close(); });
|
||||
--></script>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user