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 @@
+
+
+