diff --git a/IISExpress/AppServer/applicationhost.config b/IISExpress/AppServer/applicationhost.config index 93879e48b..0ae7782f5 100644 --- a/IISExpress/AppServer/applicationhost.config +++ b/IISExpress/AppServer/applicationhost.config @@ -125,8 +125,8 @@ - - + + diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 984b9ca9b..a4208a211 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -62,6 +62,10 @@ ..\packages\NUnit.2.5.10.11092\lib\pnunit.framework.dll + + False + ..\packages\Prowlin 0.9.4163.39219\Prowlin.dll + @@ -81,6 +85,7 @@ + diff --git a/NzbDrone.Core.Test/ProviderTests/ProwlProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/ProwlProviderTest.cs new file mode 100644 index 000000000..c6d6c506e --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/ProwlProviderTest.cs @@ -0,0 +1,147 @@ +using System; +using AutoMoq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; +using Prowlin; + +// ReSharper disable InconsistentNaming + +namespace NzbDrone.Core.Test.ProviderTests +{ + [Explicit] + [TestFixture] + public class ProwlProviderTest : TestBase + { + private const string _apiKey = "c3bdc0f48168f72d546cc6872925b160f5cbffc1"; + private const string _apiKey2 = "46a710a46b111b0b8633819b0d8a1e0272a3affa"; + + private const string _badApiKey = "1234567890abcdefghijklmnopqrstuvwxyz1234"; + + [Test] + public void Verify_should_return_true_for_a_valid_apiKey() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().Verify(_apiKey); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void Verify_should_return_false_for_an_invalid_apiKey() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().Verify(_badApiKey); + + //Assert + ExceptionVerification.ExcpectedWarns(1); + result.Should().BeFalse(); + } + + [Test] + public void SendNotification_should_return_true_for_a_valid_apiKey() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void SendNotification_should_return_false_for_an_invalid_apiKey() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _badApiKey); + + //Assert + ExceptionVerification.ExcpectedWarns(1); + result.Should().BeFalse(); + } + + [Test] + public void SendNotification_should_alert_with_high_priority() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone (High)", _apiKey, NotificationPriority.High); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void SendNotification_should_alert_with_VeryLow_priority() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone (VeryLow)", _apiKey, NotificationPriority.VeryLow); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void SendNotification_should_have_a_call_back_url() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey, NotificationPriority.Normal, "http://www.nzbdrone.com"); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void SendNotification_should_return_true_for_two_valid_apiKey() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey + ", " + _apiKey2); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void SendNotification_should_return_true_for_valid_apiKey_with_bad_apiKey() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + //Act + var result = mocker.Resolve().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey + ", " + _badApiKey); + + //Assert + result.Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 999ed1f31..6801324b6 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -160,6 +160,9 @@ ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + ..\packages\Prowlin 0.9.4163.39219\Prowlin.dll + @@ -225,6 +228,8 @@ + + diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs index 70bdce650..16c69015e 100644 --- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Providers.Core private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IDatabase _database; - [Inject] public ConfigProvider(IDatabase database) { @@ -364,6 +363,32 @@ namespace NzbDrone.Core.Providers.Core set { SetValue("GrowlPassword", value); } } + public virtual Boolean ProwlNotifyOnGrab + { + get { return GetValueBoolean("ProwlNotifyOnGrab"); } + + set { SetValue("ProwlNotifyOnGrab", value); } + } + + public virtual Boolean ProwlNotifyOnDownload + { + get { return GetValueBoolean("ProwlNotifyOnDownload"); } + + set { SetValue("ProwlNotifyOnDownload", value); } + } + + public virtual string ProwlApiKeys + { + get { return GetValue("ProwlApiKeys", String.Empty); } + set { SetValue("ProwlApiKeys", value); } + } + + public virtual int ProwlPriority + { + get { return GetValueInt("ProwlPriority", 0); } + set { SetValue("ProwlPriority", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/NzbDrone.Core/Providers/ExternalNotification/Prowl.cs b/NzbDrone.Core/Providers/ExternalNotification/Prowl.cs new file mode 100644 index 000000000..f43a3b400 --- /dev/null +++ b/NzbDrone.Core/Providers/ExternalNotification/Prowl.cs @@ -0,0 +1,77 @@ +using System; +using NLog; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; +using Prowlin; + +namespace NzbDrone.Core.Providers.ExternalNotification +{ + public class Prowl : ExternalNotificationBase + { + private readonly ProwlProvider _prowlProvider; + + private readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public Prowl(ConfigProvider configProvider, ProwlProvider prowlProvider) + : base(configProvider) + { + _prowlProvider = prowlProvider; + } + + public override string Name + { + get { return "Prowl"; } + } + + public override void OnGrab(string message) + { + try + { + if(_configProvider.GrowlNotifyOnGrab) + { + _logger.Trace("Sending Notification to Prowl"); + const string title = "Episode Grabbed"; + + var apiKeys = _configProvider.ProwlApiKeys; + var priority = _configProvider.ProwlPriority; + + _prowlProvider.SendNotification(title, message, apiKeys, (NotificationPriority)priority); + } + } + + catch (Exception ex) + { + Logger.WarnException(ex.Message, ex); + throw; + } + } + + public override void OnDownload(string message, Series series) + { + try + { + if (_configProvider.GrowlNotifyOnDownload) + { + _logger.Trace("Sending Notification to Prowl"); + const string title = "Episode Downloaded"; + + var apiKeys = _configProvider.ProwlApiKeys; + var priority = _configProvider.ProwlPriority; + + _prowlProvider.SendNotification(title, message, apiKeys, (NotificationPriority)priority); + } + } + + catch (Exception ex) + { + Logger.WarnException(ex.Message, ex); + throw; + } + } + + public override void OnRename(string message, Series series) + { + + } + } +} diff --git a/NzbDrone.Core/Providers/ProwlProvider.cs b/NzbDrone.Core/Providers/ProwlProvider.cs new file mode 100644 index 000000000..a43626ea4 --- /dev/null +++ b/NzbDrone.Core/Providers/ProwlProvider.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using Prowlin; + +namespace NzbDrone.Core.Providers +{ + public class ProwlProvider + { + private readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public ProwlProvider() + { + + } + + public virtual bool Verify(string apiKey) + { + try + { + var verificationRequest = new Verification(); + verificationRequest.ApiKey = apiKey; + + var client = new ProwlClient(); + + Logger.Trace("Verifying API Key: {0}", apiKey); + + var verificationResult = client.SendVerification(verificationRequest); + if (String.IsNullOrWhiteSpace(verificationResult.ErrorMessage) && verificationResult.ResultCode == "200") + return true; + } + + catch(Exception ex) + { + Logger.TraceException(ex.Message, ex); + Logger.Warn("Invalid API Key: {0}", apiKey); + } + + return false; + } + + public virtual bool SendNotification(string title, string message, string apiKeys, NotificationPriority priority = NotificationPriority.Normal, string url = null) + { + try + { + var notification = new Notification + { + Application = "NzbDrone", + Description = message, + Event = title, + Priority = priority, + Url = url + }; + + foreach (var apiKey in apiKeys.Split(',')) + notification.AddApiKey(apiKey.Trim()); + + var client = new ProwlClient(); + + Logger.Trace("Sending Prowl Notification"); + + var notificationResult = client.SendNotification(notification); + + if (String.IsNullOrWhiteSpace(notificationResult.ErrorMessage)) + return true; + } + + catch(Exception ex) + { + Logger.TraceException(ex.Message, ex); + Logger.Warn("Invalid API Key(s): {0}", apiKeys); + } + + return false; + } + + public virtual void TestNotification(string apiKeys) + { + const string title = "Test Notification"; + const string message = "This is a test message from NzbDrone"; + + SendNotification(title, message, apiKeys); + } + } +} diff --git a/NzbDrone.Web/Controllers/SettingsController.cs b/NzbDrone.Web/Controllers/SettingsController.cs index 1b775246c..2eb01cd3e 100644 --- a/NzbDrone.Web/Controllers/SettingsController.cs +++ b/NzbDrone.Web/Controllers/SettingsController.cs @@ -102,7 +102,7 @@ namespace NzbDrone.Web.Controllers SabApiKey = _configProvider.SabApiKey, SabUsername = _configProvider.SabUsername, SabPassword = _configProvider.SabPassword, - SabTvCategory = _configProvider.SabTvCategory, + SabTvCategory = tvCategory, SabTvPriority = _configProvider.SabTvPriority, SabDropDirectory = _configProvider.SabDropDirectory, SabTvCategorySelectList = tvCategorySelectList @@ -178,7 +178,13 @@ namespace NzbDrone.Web.Controllers GrowlNotifyOnGrab = _configProvider.GrowlNotifyOnGrab, GrowlNotifyOnDownload = _configProvider.GrowlNotifyOnDownload, GrowlHost = _configProvider.GrowlHost, - GrowlPassword = _configProvider.GrowlPassword + GrowlPassword = _configProvider.GrowlPassword, + ProwlEnabled = _externalNotificationProvider.GetSettings(typeof(Prowl)).Enable, + ProwlNotifyOnGrab = _configProvider.ProwlNotifyOnGrab, + ProwlNotifyOnDownload = _configProvider.ProwlNotifyOnDownload, + ProwlApiKeys = _configProvider.ProwlApiKeys, + ProwlPriority = _configProvider.ProwlPriority, + ProwlPrioritySelectList = GetProwlPrioritySelectList() }; return View(model); @@ -535,5 +541,17 @@ namespace NzbDrone.Web.Controllers { return Json(new NotificationResult() { Title = "Unable to save setting", Text = "Invalid post data", NotificationType = NotificationType.Error }); } + + private SelectList GetProwlPrioritySelectList() + { + var list = new List(); + list.Add(new ProwlPrioritySelectListModel{ Name = "Very Low", Value = -2 }); + list.Add(new ProwlPrioritySelectListModel { Name = "Moderate", Value = -1 }); + list.Add(new ProwlPrioritySelectListModel { Name = "Normal", Value = 0 }); + list.Add(new ProwlPrioritySelectListModel { Name = "High", Value = 1 }); + list.Add(new ProwlPrioritySelectListModel { Name = "Emergency", Value = 2 }); + + return new SelectList(list, "Value", "Name"); + } } } \ No newline at end of file diff --git a/NzbDrone.Web/Models/NotificationSettingsModel.cs b/NzbDrone.Web/Models/NotificationSettingsModel.cs index 3989d43c9..5abed1911 100644 --- a/NzbDrone.Web/Models/NotificationSettingsModel.cs +++ b/NzbDrone.Web/Models/NotificationSettingsModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; namespace NzbDrone.Web.Models { @@ -131,5 +132,29 @@ namespace NzbDrone.Web.Models [DisplayName("Growl host Password")] [Description("Password is required if Growl is running on another system")] public string GrowlPassword { get; set; } + + + //Prowl + [DisplayName("Enabled")] + [Description("Enable notifications for Prowl?")] + public bool ProwlEnabled { get; set; } + + [DisplayName("Notify on Grab")] + [Description("Send notification when episode is sent to SABnzbd?")] + public bool ProwlNotifyOnGrab { get; set; } + + [DisplayName("Notify on Download")] + [Description("Send notification when episode is downloaded?")] + public bool ProwlNotifyOnDownload { get; set; } + + [DisplayName("API Keys")] + [Description("Comma-Separated list of API Keys")] + public string ProwlApiKeys { get; set; } + + [DisplayName("Priority")] + [Description("Priority to send alerts to Prowl with")] + public int ProwlPriority { get; set; } + + public SelectList ProwlPrioritySelectList { get; set; } } } \ No newline at end of file diff --git a/NzbDrone.Web/Models/ProwlPrioritySelectListModel.cs b/NzbDrone.Web/Models/ProwlPrioritySelectListModel.cs new file mode 100644 index 000000000..d29a98006 --- /dev/null +++ b/NzbDrone.Web/Models/ProwlPrioritySelectListModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace NzbDrone.Web.Models +{ + public class ProwlPrioritySelectListModel + { + public string Name { get; set; } + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index cc49b0956..3357d9f99 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -491,6 +491,7 @@ + @@ -942,6 +943,9 @@ + + +