diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 2126da70d..9caa28c52 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -115,6 +115,7 @@ + diff --git a/NzbDrone.Core.Test/ProviderTests/PlexProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/PlexProviderTest.cs new file mode 100644 index 000000000..905af8cf2 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/PlexProviderTest.cs @@ -0,0 +1,102 @@ +// ReSharper disable RedundantUsingDirective + +using System; +using System.IO; +using System.Linq; +using System.Text; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Model.Xbmc; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Providers.Xbmc; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common.AutoMoq; + +namespace NzbDrone.Core.Test.ProviderTests +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class PlexProviderTest : CoreTest + { + [Test] + public void GetSectionKeys_should_return_single_section_key_when_only_one_show_section() + { + //Setup + + var response = ""; + Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); + + Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) + .Returns(stream); + + //Act + var result = Mocker.Resolve().GetSectionKeys("localhost:32400"); + + //Assert + result.Should().HaveCount(1); + result.First().Should().Be(5); + } + + [Test] + public void GetSectionKeys_should_return_single_section_key_when_only_one_show_section_with_other_sections() + { + //Setup + + var response = ""; + Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); + + Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) + .Returns(stream); + + //Act + var result = Mocker.Resolve().GetSectionKeys("localhost:32400"); + + //Assert + result.Should().HaveCount(1); + result.First().Should().Be(5); + } + + [Test] + public void GetSectionKeys_should_return_multiple_section_keys_when_there_are_multiple_show_sections() + { + //Setup + + var response = ""; + Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); + + Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) + .Returns(stream); + + //Act + var result = Mocker.Resolve().GetSectionKeys("localhost:32400"); + + //Assert + result.Should().HaveCount(2); + result.First().Should().Be(5); + result.Last().Should().Be(6); + } + + [Test] + public void UpdateSection_should_update_section() + { + //Setup + + var response = ""; + Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); + + Mocker.GetMock().Setup(s => s.DownloadString("http://localhost:32400/library/sections/5/refresh")) + .Returns(response); + + //Act + Mocker.Resolve().UpdateSection("localhost:32400", 5); + + //Assert + + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/CentralDispatch.cs b/NzbDrone.Core/CentralDispatch.cs index e1d474ffa..46b103de6 100644 --- a/NzbDrone.Core/CentralDispatch.cs +++ b/NzbDrone.Core/CentralDispatch.cs @@ -140,6 +140,7 @@ namespace NzbDrone.Core Kernel.Bind().To(); Kernel.Bind().To(); Kernel.Bind().To(); + Kernel.Bind().To(); var notifiers = Kernel.GetAll(); Kernel.Get().InitializeNotifiers(notifiers.ToList()); diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 3b73de5e5..c3344dc16 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -240,7 +240,6 @@ - @@ -268,61 +267,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -330,13 +288,202 @@ - - - - + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + + + Code + @@ -346,24 +493,10 @@ - - - - - - - - - - - - - - @@ -376,9 +509,6 @@ - - - diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs index 3d661c30a..242f831c9 100644 --- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs @@ -442,6 +442,51 @@ namespace NzbDrone.Core.Providers.Core get { return "http://services.nzbdrone.com"; } } + public virtual Boolean PlexNotifyOnGrab + { + get { return GetValueBoolean("PlexNotifyOnGrab"); } + + set { SetValue("PlexNotifyOnGrab", value); } + } + + public virtual Boolean PlexNotifyOnDownload + { + get { return GetValueBoolean("PlexNotifyOnDownload"); } + + set { SetValue("PlexNotifyOnDownload", value); } + } + + public virtual Boolean PlexUpdateLibrary + { + get { return GetValueBoolean("PlexUpdateLibrary"); } + + set { SetValue("PlexUpdateLibrary", value); } + } + + public virtual string PlexServerHost + { + get { return GetValue("PlexServerHost", "localhost:32400"); } + set { SetValue("PlexServerHost", value); } + } + + public virtual string PlexClientHosts + { + get { return GetValue("PlexClientHosts", "localhost:3000"); } + set { SetValue("PlexClientHosts", value); } + } + + public virtual string PlexUsername + { + get { return GetValue("PlexUsername"); } + set { SetValue("PlexUsername", value); } + } + + public virtual string PlexPassword + { + get { return GetValue("PlexPassword"); } + set { SetValue("PlexPassword", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/NzbDrone.Core/Providers/ExternalNotification/Plex.cs b/NzbDrone.Core/Providers/ExternalNotification/Plex.cs new file mode 100644 index 000000000..31fd9ca43 --- /dev/null +++ b/NzbDrone.Core/Providers/ExternalNotification/Plex.cs @@ -0,0 +1,66 @@ +using System; +using NLog; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; + +namespace NzbDrone.Core.Providers.ExternalNotification +{ + public class Plex : ExternalNotificationBase + { + private readonly PlexProvider _plexProvider; + + public Plex(ConfigProvider configProvider, PlexProvider plexProvider) + : base(configProvider) + { + _plexProvider = plexProvider; + } + + public override string Name + { + get { return "Plex"; } + } + + public override void OnGrab(string message) + { + const string header = "NzbDrone [TV] - Grabbed"; + + if (_configProvider.PlexNotifyOnGrab) + { + _logger.Trace("Sending Notification to Plex Clients"); + _plexProvider.Notify(header, message); + } + } + + public override void OnDownload(string message, Series series) + { + const string header = "NzbDrone [TV] - Downloaded"; + + if (_configProvider.PlexNotifyOnDownload) + { + _logger.Trace("Sending Notification to Plex Clients"); + _plexProvider.Notify(header, message); + } + + UpdateIfEnabled(); + } + + public override void OnRename(string message, Series series) + { + + } + + public override void AfterRename(string message, Series series) + { + UpdateIfEnabled(); + } + + private void UpdateIfEnabled() + { + if (_configProvider.PlexUpdateLibrary) + { + _logger.Trace("Sending Update Request to Plex Server"); + _plexProvider.UpdateLibrary(); + } + } + } +} diff --git a/NzbDrone.Core/Providers/PlexProvider.cs b/NzbDrone.Core/Providers/PlexProvider.cs new file mode 100644 index 000000000..621b1ea07 --- /dev/null +++ b/NzbDrone.Core/Providers/PlexProvider.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using NLog; +using Ninject; +using NzbDrone.Common; +using NzbDrone.Core.Providers.Core; + +namespace NzbDrone.Core.Providers +{ + public class PlexProvider + { + private readonly HttpProvider _httpProvider; + private readonly ConfigProvider _configProvider; + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + [Inject] + public PlexProvider(HttpProvider httpProvider, ConfigProvider configProvider) + { + _httpProvider = httpProvider; + _configProvider = configProvider; + } + + public PlexProvider() + { + + } + + public virtual void Notify(string header, string message) + { + //Foreach plex client send a notification + foreach(var host in _configProvider.PlexClientHosts.Split(',')) + { + try + { + var command = String.Format("ExecBuiltIn(Notification({0}, {1}))", header, message); + SendCommand(host, command, _configProvider.PlexUsername, _configProvider.PlexPassword); + } + catch(Exception ex) + { + logger.WarnException("Failed to send notification to Plex Client: " + host, ex); + } + } + } + + public virtual void UpdateLibrary() + { + var host = _configProvider.PlexServerHost; + + try + { + var sections = GetSectionKeys(host); + sections.ForEach(s => UpdateSection(host, s)); + } + + catch(Exception ex) + { + logger.WarnException("Failed to Update Plex host: " + host, ex); + throw; + } + } + + public List GetSectionKeys(string host) + { + logger.Trace("Getting sections from Plex host: {0}", host); + var url = String.Format("http://{0}/library/sections", host); + var xmlStream = _httpProvider.DownloadStream(url, null); + var xDoc = XDocument.Load(xmlStream); + var mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault(); + var directories = mediaContainer.Descendants("Directory").Where(x => x.Attribute("type").Value == "show"); + + return directories.Select(d => Int32.Parse(d.Attribute("key").Value)).ToList(); + } + + public void UpdateSection(string host, int key) + { + logger.Trace("Updating Plex host: {0}, Section: {1}", host, key); + var url = String.Format("http://{0}/library/sections/{1}/refresh", host, key); + _httpProvider.DownloadString(url); + } + + public virtual string SendCommand(string host, string command, string username, string password) + { + var url = String.Format("http://{0}/xbmcCmds/xbmcHttp?command={1}", host, command); + + if (!String.IsNullOrEmpty(username)) + { + return _httpProvider.DownloadString(url, username, password); + } + + return _httpProvider.DownloadString(url); + } + } +} diff --git a/NzbDrone.Web/Controllers/SettingsController.cs b/NzbDrone.Web/Controllers/SettingsController.cs index 0bbf75d9c..fcf0f7f21 100644 --- a/NzbDrone.Web/Controllers/SettingsController.cs +++ b/NzbDrone.Web/Controllers/SettingsController.cs @@ -180,7 +180,15 @@ namespace NzbDrone.Web.Controllers ProwlNotifyOnDownload = _configProvider.ProwlNotifyOnDownload, ProwlApiKeys = _configProvider.ProwlApiKeys, ProwlPriority = _configProvider.ProwlPriority, - ProwlPrioritySelectList = GetProwlPrioritySelectList() + ProwlPrioritySelectList = GetProwlPrioritySelectList(), + PlexEnabled = _externalNotificationProvider.GetSettings(typeof(Plex)).Enable, + PlexNotifyOnGrab = _configProvider.PlexNotifyOnGrab, + PlexNotifyOnDownload = _configProvider.PlexNotifyOnDownload, + PlexUpdateLibrary = _configProvider.PlexUpdateLibrary, + PlexServerHost = _configProvider.PlexServerHost, + PlexClientHosts = _configProvider.PlexClientHosts, + PlexUsername = _configProvider.PlexUsername, + PlexPassword = _configProvider.PlexPassword, }; return View(model); @@ -530,6 +538,19 @@ namespace NzbDrone.Web.Controllers _configProvider.ProwlApiKeys = data.ProwlApiKeys; _configProvider.ProwlPriority = data.ProwlPriority; + //Plex + var plexSettings = _externalNotificationProvider.GetSettings(typeof(Plex)); + plexSettings.Enable = data.PlexEnabled; + _externalNotificationProvider.SaveSettings(plexSettings); + + _configProvider.PlexNotifyOnGrab = data.PlexNotifyOnGrab; + _configProvider.PlexNotifyOnDownload = data.PlexNotifyOnDownload; + _configProvider.PlexUpdateLibrary = data.PlexUpdateLibrary; + _configProvider.PlexServerHost = data.PlexServerHost; + _configProvider.PlexClientHosts = data.PlexClientHosts; + _configProvider.PlexUsername = data.PlexUsername; + _configProvider.PlexPassword = data.PlexPassword; + return GetSuccessResult(); } diff --git a/NzbDrone.Web/Models/NotificationSettingsModel.cs b/NzbDrone.Web/Models/NotificationSettingsModel.cs index caeaf6228..d274cda8f 100644 --- a/NzbDrone.Web/Models/NotificationSettingsModel.cs +++ b/NzbDrone.Web/Models/NotificationSettingsModel.cs @@ -164,5 +164,46 @@ namespace NzbDrone.Web.Models public int ProwlPriority { get; set; } public SelectList ProwlPrioritySelectList { get; set; } + + //Plex + [DisplayName("Enabled")] + [Description("Enable notifications for Plex?")] + public bool PlexEnabled { get; set; } + + [DisplayName("Notify on Grab")] + [Description("Send notification when episode is sent to SABnzbd?")] + public bool PlexNotifyOnGrab { get; set; } + + [DisplayName("Notify on Download")] + [Description("Send notification when episode is downloaded?")] + public bool PlexNotifyOnDownload { get; set; } + + [DisplayName("Update on Download and Rename")] + [Description("Update Plex library after episode is downloaded or renamed?")] + public bool PlexUpdateLibrary { get; set; } + + [DataType(DataType.Text)] + [DisplayName("Server Host")] + [Description("Plex Server host with port")] + [DisplayFormat(ConvertEmptyStringToNull = false)] + public string PlexServerHost { get; set; } + + [DataType(DataType.Text)] + [DisplayName("Client Hosts")] + [Description("Plex client hosts with port, comma separated for multiple clients")] + [DisplayFormat(ConvertEmptyStringToNull = false)] + public string PlexClientHosts { get; set; } + + [DataType(DataType.Text)] + [DisplayName("Username")] + [Description("Plex client webserver username")] + [DisplayFormat(ConvertEmptyStringToNull = false)] + public string PlexUsername { get; set; } + + [DataType(DataType.Text)] + [DisplayName("Password")] + [Description("Plex client webserver password")] + [DisplayFormat(ConvertEmptyStringToNull = false)] + public string PlexPassword { get; set; } } } \ No newline at end of file diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index f80fd647c..0e0e50b35 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -498,6 +498,7 @@ + diff --git a/NzbDrone.Web/Views/Settings/Notifications.cshtml b/NzbDrone.Web/Views/Settings/Notifications.cshtml index b86ea921f..bac84b2ea 100644 --- a/NzbDrone.Web/Views/Settings/Notifications.cshtml +++ b/NzbDrone.Web/Views/Settings/Notifications.cshtml @@ -34,21 +34,23 @@ @using (Html.BeginForm("SaveNotifications", "Settings", FormMethod.Post, new { id = "NotificationForm", name = "NotificationForm", @class = "settingsForm" })) {
-

- XBMC

+

XBMC

@{Html.RenderPartial("Xbmc", Model);} -

- SMTP

+ +

SMTP

@{Html.RenderPartial("Smtp", Model);} -

- Twitter

+ +

Twitter

@{Html.RenderPartial("Twitter", Model);} -

- Growl

+ +

Growl

@{Html.RenderPartial("Growl", Model);} -

- Prowl

+ +

Prowl

@{Html.RenderPartial("Prowl", Model);} + +

Plex

+ @{Html.RenderPartial("Plex", Model);}