From 348ff5a386f08040be40f78cd5f88f429eea8d33 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Jul 2011 11:19:33 -0700 Subject: [PATCH] XbmcProvider updated to include new Json API methods. EventClient is used for sending CleanLibrary and Notifications (With NzbDrone Logo - Internal Resource). Support for Dharma's HTTP Server (Deprecated), since Dharma doesn't support Json as well. --- NzbDrone.Core.Test/EventClientProviderTest.cs | 103 +++++ NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 2 + NzbDrone.Core.Test/XbmcProviderTest.cs | 399 ++++++++++++++++++ NzbDrone.Core/Model/Xbmc/ActionType.cs | 13 + .../Model/Xbmc/ActivePlayersResult.cs | 14 + NzbDrone.Core/Model/Xbmc/Command.cs | 19 + NzbDrone.Core/Model/Xbmc/ErrorResult.cs | 14 + NzbDrone.Core/Model/Xbmc/IconType.cs | 15 + NzbDrone.Core/Model/Xbmc/Params.cs | 12 + NzbDrone.Core/Model/Xbmc/TvShow.cs | 15 + NzbDrone.Core/Model/Xbmc/TvShowResult.cs | 14 + NzbDrone.Core/Model/Xbmc/VersionResult.cs | 14 + NzbDrone.Core/NzbDrone.Core.csproj | 16 + NzbDrone.Core/NzbDrone.jpg | Bin 0 -> 3299 bytes .../Providers/Core/ConfigProvider.cs | 60 ++- NzbDrone.Core/Providers/Core/HttpProvider.cs | 29 ++ NzbDrone.Core/Providers/Core/UdpProvider.cs | 192 +++++++++ .../ExternalNotificationProviderBase.cs | 24 +- .../XbmcNotificationProvider.cs | 9 +- .../Providers/Xbmc/EventClientProvider.cs | 69 +++ .../Providers/Xbmc/ResourceManager.cs | 55 +++ NzbDrone.Core/Providers/XbmcProvider.cs | 255 +++++++++-- .../Controllers/SettingsController.cs | 44 +- .../Models/NotificationSettingsModel.cs | 40 +- .../Views/Settings/Notifications.cshtml | 42 +- NzbDrone.Web/Views/Settings/Quality.cshtml | 1 - 26 files changed, 1312 insertions(+), 158 deletions(-) create mode 100644 NzbDrone.Core.Test/EventClientProviderTest.cs create mode 100644 NzbDrone.Core.Test/XbmcProviderTest.cs create mode 100644 NzbDrone.Core/Model/Xbmc/ActionType.cs create mode 100644 NzbDrone.Core/Model/Xbmc/ActivePlayersResult.cs create mode 100644 NzbDrone.Core/Model/Xbmc/Command.cs create mode 100644 NzbDrone.Core/Model/Xbmc/ErrorResult.cs create mode 100644 NzbDrone.Core/Model/Xbmc/IconType.cs create mode 100644 NzbDrone.Core/Model/Xbmc/Params.cs create mode 100644 NzbDrone.Core/Model/Xbmc/TvShow.cs create mode 100644 NzbDrone.Core/Model/Xbmc/TvShowResult.cs create mode 100644 NzbDrone.Core/Model/Xbmc/VersionResult.cs create mode 100644 NzbDrone.Core/NzbDrone.jpg create mode 100644 NzbDrone.Core/Providers/Core/UdpProvider.cs create mode 100644 NzbDrone.Core/Providers/Xbmc/EventClientProvider.cs create mode 100644 NzbDrone.Core/Providers/Xbmc/ResourceManager.cs diff --git a/NzbDrone.Core.Test/EventClientProviderTest.cs b/NzbDrone.Core.Test/EventClientProviderTest.cs new file mode 100644 index 000000000..2f0ebfdc3 --- /dev/null +++ b/NzbDrone.Core.Test/EventClientProviderTest.cs @@ -0,0 +1,103 @@ +// ReSharper disable RedundantUsingDirective + +using System; +using System.Collections.Generic; +using AutoMoq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +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.Repository.Quality; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class EventClientProviderTest : TestBase + { + [Test] + public void SendNotification_true() + { + //Setup + var mocker = new AutoMoqer(); + + var header = "NzbDrone Test"; + var message = "Test Message!"; + var address = "localhost"; + + var fakeUdp = mocker.GetMock(); + fakeUdp.Setup(s => s.Send(address, UdpProvider.PacketType.Notification, It.IsAny())).Returns(true); + + //Act + var result = mocker.Resolve().SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", address); + + //Assert + Assert.AreEqual(true, result); + } + + [Test] + public void SendNotification_false() + { + //Setup + var mocker = new AutoMoqer(); + + var header = "NzbDrone Test"; + var message = "Test Message!"; + var address = "localhost"; + + var fakeUdp = mocker.GetMock(); + fakeUdp.Setup(s => s.Send(address, UdpProvider.PacketType.Notification, It.IsAny())).Returns(false); + + //Act + var result = mocker.Resolve().SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", address); + + //Assert + Assert.AreEqual(false, result); + } + + [Test] + public void SendAction_Update_true() + { + //Setup + var mocker = new AutoMoqer(); + + var path = @"C:\Test\TV\30 Rock"; + var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", path); + var address = "localhost"; + + var fakeUdp = mocker.GetMock(); + fakeUdp.Setup(s => s.Send(address, UdpProvider.PacketType.Action, It.IsAny())).Returns(true); + + //Act + var result = mocker.Resolve().SendAction(address, ActionType.ExecBuiltin, command); + + //Assert + Assert.AreEqual(true, result); + } + + [Test] + public void SendAction_Update_false() + { + //Setup + var mocker = new AutoMoqer(); + + var path = @"C:\Test\TV\30 Rock"; + var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", path); + var address = "localhost"; + + var fakeUdp = mocker.GetMock(); + fakeUdp.Setup(s => s.Send(address, UdpProvider.PacketType.Action, It.IsAny())).Returns(false); + + //Act + var result = mocker.Resolve().SendAction(address, ActionType.ExecBuiltin, command); + + //Assert + Assert.AreEqual(false, result); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 8d17c7745..eb2beb327 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -85,6 +85,8 @@ + + diff --git a/NzbDrone.Core.Test/XbmcProviderTest.cs b/NzbDrone.Core.Test/XbmcProviderTest.cs new file mode 100644 index 000000000..579a24b66 --- /dev/null +++ b/NzbDrone.Core.Test/XbmcProviderTest.cs @@ -0,0 +1,399 @@ +// ReSharper disable RedundantUsingDirective + +using System; +using System.Collections.Generic; +using AutoMoq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +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.Repository.Quality; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class XbmcProviderTest : TestBase + { + [Test] + public void JsonEror_true() + { + //Setup + var mocker = new AutoMoqer(); + var response = "{\"error\":{\"code\":-32601,\"message\":\"Method not found.\"},\"id\":10,\"jsonrpc\":\"2.0\"}"; + + //Act + var result = mocker.Resolve().CheckForJsonError(response); + + //Assert + Assert.AreEqual(true, result); + } + + [Test] + public void JsonEror_false() + { + //Setup + var mocker = new AutoMoqer(); + var reposnse = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"version\":3}}"; + + //Act + var result = mocker.Resolve().CheckForJsonError(reposnse); + + //Assert + Assert.AreEqual(false, result); + } + + [TestCase(3)] + [TestCase(2)] + [TestCase(0)] + public void GetJsonVersion(int number) + { + //Setup + var mocker = new AutoMoqer(); + + var message = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"version\":" + number + "}}"; + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.PostCommand("localhost:8080", "xbmc", "xbmc", It.IsAny())) + .Returns(message); + + //Act + var result = mocker.Resolve().GetJsonVersion("localhost:8080", "xbmc", "xbmc"); + + //Assert + Assert.AreEqual(number, result); + } + + [Test] + public void GetJsonVersion_error() + { + //Setup + var mocker = new AutoMoqer(); + + var message = "{\"error\":{\"code\":-32601,\"message\":\"Method not found.\"},\"id\":10,\"jsonrpc\":\"2.0\"}"; + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.PostCommand("localhost:8080", "xbmc", "xbmc", It.IsAny())) + .Returns(message); + + //Act + var result = mocker.Resolve().GetJsonVersion("localhost:8080", "xbmc", "xbmc"); + + //Assert + Assert.AreEqual(0, result); + } + + [TestCase(false, false, false)] + [TestCase(true, true, true)] + [TestCase(true, false, false)] + [TestCase(true, true, false)] + [TestCase(false, true, false)] + [TestCase(false, true, true)] + [TestCase(false, false, true)] + [TestCase(true, false, true)] + public void GetActivePlayers(bool audio, bool picture, bool video) + { + //Setup + var mocker = new AutoMoqer(); + + var message = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"audio\":" + + audio.ToString().ToLower() + + ",\"picture\":" + + picture.ToString().ToLower() + + ",\"video\":" + + video.ToString().ToLower() + + "}}"; + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.PostCommand("localhost:8080", "xbmc", "xbmc", It.IsAny())) + .Returns(message); + + //Act + var result = mocker.Resolve().GetActivePlayers("localhost:8080", "xbmc", "xbmc"); + + //Assert + Assert.AreEqual(audio, result["audio"]); + Assert.AreEqual(picture, result["picture"]); + Assert.AreEqual(video, result["video"]); + } + + [Test] + public void GetTvShowsJson() + { + //Setup + var mocker = new AutoMoqer(); + + var message = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"limits\":{\"end\":5,\"start\":0,\"total\":5},\"tvshows\":[{\"file\":\"smb://HOMESERVER/TV/7th Heaven/\",\"imdbnumber\":\"73928\",\"label\":\"7th Heaven\",\"tvshowid\":3},{\"file\":\"smb://HOMESERVER/TV/8 Simple Rules/\",\"imdbnumber\":\"78461\",\"label\":\"8 Simple Rules\",\"tvshowid\":4},{\"file\":\"smb://HOMESERVER/TV/24-7 Penguins-Capitals- Road to the NHL Winter Classic/\",\"imdbnumber\":\"213041\",\"label\":\"24/7 Penguins/Capitals: Road to the NHL Winter Classic\",\"tvshowid\":1},{\"file\":\"smb://HOMESERVER/TV/30 Rock/\",\"imdbnumber\":\"79488\",\"label\":\"30 Rock\",\"tvshowid\":2},{\"file\":\"smb://HOMESERVER/TV/90210/\",\"imdbnumber\":\"82716\",\"label\":\"90210\",\"tvshowid\":5}]}}"; + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.PostCommand("localhost:8080", "xbmc", "xbmc", It.IsAny())) + .Returns(message); + + //Act + var result = mocker.Resolve().GetTvShowsJson("localhost:8080", "xbmc", "xbmc"); + + //Assert + Assert.AreEqual(5, result.Count); + result.Should().Contain(s => s.ImdbNumber == 79488); + } + + [Test] + public void Notify_true() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + var header = "NzbDrone Test"; + var message = "Test Message!"; + + var fakeConfig = mocker.GetMock(); + fakeConfig.SetupGet(s => s.XbmcHosts).Returns("localhost:8080"); + + //var fakeUdpProvider = mocker.GetMock(); + var fakeEventClient = mocker.GetMock(); + fakeEventClient.Setup(s => s.SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", "localhost")).Returns(true); + + //Act + mocker.Resolve().Notify(header, message); + + //Assert + mocker.VerifyAllMocks(); + } + + [Test] + public void SendCommand() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + var host = "localhost:8080"; + var command = "ExecBuiltIn(CleanLibrary(video))"; + var username = "xbmc"; + var password = "xbmc"; + + var url = String.Format("http://localhost:8080/xbmcCmds/xbmcHttp?command=ExecBuiltIn(CleanLibrary(video))"); + + //var fakeUdpProvider = mocker.GetMock(); + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.DownloadString(url, username, password)).Returns("Ok\n"); + + //Act + var result = mocker.Resolve().SendCommand(host, command, username, username); + + //Assert + mocker.VerifyAllMocks(); + Assert.AreEqual("Ok\n", result); + } + + [Test] + public void GetXbmcSeriesPath_true() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + var queryResult = @"smb://xbmc:xbmc@HOMESERVER/TV/30 Rock/"; + + var host = "localhost:8080"; + var username = "xbmc"; + var password = "xbmc"; + + var setResponseUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=SetResponseFormat(webheader;false;webfooter;false;header;;footer;;opentag;;closetag;;closefinaltag;false)"; + var resetResponseUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=SetResponseFormat()"; + var query = String.Format("http://localhost:8080/xbmcCmds/xbmcHttp?command=QueryVideoDatabase(select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = 79488 and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath)"); + + + //var fakeUdpProvider = mocker.GetMock(); + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.DownloadString(setResponseUrl, username, password)).Returns("OK"); + fakeHttp.Setup(s => s.DownloadString(resetResponseUrl, username, password)).Returns(@" +
  • OK + "); + fakeHttp.Setup(s => s.DownloadString(query, username, password)).Returns(queryResult); + + //Act + var result = mocker.Resolve().GetXbmcSeriesPath(host, 79488, username, username); + + //Assert + mocker.VerifyAllMocks(); + Assert.AreEqual("smb://xbmc:xbmc@HOMESERVER/TV/30 Rock/", result); + } + + [Test] + public void GetXbmcSeriesPath_false() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + var queryResult = @""; + + var host = "localhost:8080"; + var username = "xbmc"; + var password = "xbmc"; + + var setResponseUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=SetResponseFormat(webheader;false;webfooter;false;header;;footer;;opentag;;closetag;;closefinaltag;false)"; + var resetResponseUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=SetResponseFormat()"; + var query = String.Format("http://localhost:8080/xbmcCmds/xbmcHttp?command=QueryVideoDatabase(select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = 79488 and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath)"); + + + //var fakeUdpProvider = mocker.GetMock(); + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.DownloadString(setResponseUrl, username, password)).Returns("OK"); + fakeHttp.Setup(s => s.DownloadString(resetResponseUrl, username, password)).Returns(@" +
  • OK + "); + fakeHttp.Setup(s => s.DownloadString(query, username, password)).Returns(queryResult); + + //Act + var result = mocker.Resolve().GetXbmcSeriesPath(host, 79488, username, username); + + //Assert + mocker.VerifyAllMocks(); + Assert.AreEqual("", result); + } + + [Test] + public void Clean() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Strict); + + var fakeConfig = mocker.GetMock(); + fakeConfig.SetupGet(s => s.XbmcHosts).Returns("localhost:8080"); + + var fakeEventClient = mocker.GetMock(); + fakeEventClient.Setup(s => s.SendAction("localhost", ActionType.ExecBuiltin, "ExecBuiltIn(CleanLibrary(video))")).Returns(true); + + //Act + mocker.Resolve().Clean(); + + //Assert + mocker.VerifyAllMocks(); + } + + [Test] + public void UpdateWithHttp_Single() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Default); + + var host = "localhost:8080"; + var username = "xbmc"; + var password = "xbmc"; + var queryResult = @"smb://xbmc:xbmc@HOMESERVER/TV/30 Rock/"; + var queryUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=QueryVideoDatabase(select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = 79488 and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath)"; + var url = "http://localhost:8080/xbmcCmds/xbmcHttp?command=ExecBuiltIn(UpdateLibrary(video,smb://xbmc:xbmc@HOMESERVER/TV/30 Rock/))"; + + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 79488) + .With(s => s.Title = "30 Rock") + .Build(); + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.DownloadString(queryUrl, username, password)).Returns(queryResult); + fakeHttp.Setup(s => s.DownloadString(url, username, password)); + + //Act + mocker.Resolve().UpdateWithHttp(fakeSeries, host, username, password); + + //Assert + mocker.VerifyAllMocks(); + } + + [Test] + public void UpdateWithHttp_All() + { + //Setup + var mocker = new AutoMoqer(MockBehavior.Default); + + var host = "localhost:8080"; + var username = "xbmc"; + var password = "xbmc"; + var queryResult = @""; + var queryUrl = "http://localhost:8080/xbmcCmds/xbmcHttp?command=QueryVideoDatabase(select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = 79488 and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath)"; + var url = "http://localhost:8080/xbmcCmds/xbmcHttp?command=ExecBuiltIn(UpdateLibrary(video))"; + + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 79488) + .With(s => s.Title = "30 Rock") + .Build(); + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.DownloadString(queryUrl, username, password)).Returns(queryResult); + fakeHttp.Setup(s => s.DownloadString(url, username, password)); + + //Act + mocker.Resolve().UpdateWithHttp(fakeSeries, host, username, password); + + //Assert + mocker.VerifyAllMocks(); + } + + [Test] + public void UpdateWithJson_Single() + { + //Setup + var mocker = new AutoMoqer(); + + var host = "localhost:8080"; + var username = "xbmc"; + var password = "xbmc"; + var serializedQuery = "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTvShows\",\"params\":{\"fields\":[\"file\",\"imdbnumber\"]},\"id\":10}"; + var tvshows = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"limits\":{\"end\":5,\"start\":0,\"total\":5},\"tvshows\":[{\"file\":\"smb://HOMESERVER/TV/7th Heaven/\",\"imdbnumber\":\"73928\",\"label\":\"7th Heaven\",\"tvshowid\":3},{\"file\":\"smb://HOMESERVER/TV/8 Simple Rules/\",\"imdbnumber\":\"78461\",\"label\":\"8 Simple Rules\",\"tvshowid\":4},{\"file\":\"smb://HOMESERVER/TV/24-7 Penguins-Capitals- Road to the NHL Winter Classic/\",\"imdbnumber\":\"213041\",\"label\":\"24/7 Penguins/Capitals: Road to the NHL Winter Classic\",\"tvshowid\":1},{\"file\":\"smb://HOMESERVER/TV/30 Rock/\",\"imdbnumber\":\"79488\",\"label\":\"30 Rock\",\"tvshowid\":2},{\"file\":\"smb://HOMESERVER/TV/90210/\",\"imdbnumber\":\"82716\",\"label\":\"90210\",\"tvshowid\":5}]}}"; + + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 79488) + .With(s => s.Title = "30 Rock") + .Build(); + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.PostCommand(host, username, password, serializedQuery)) + .Returns(tvshows); + + var fakeEventClient = mocker.GetMock(); + fakeEventClient.Setup(s => s.SendAction("localhost", ActionType.ExecBuiltin, "ExecBuiltIn(UpdateLibrary(video,smb://HOMESERVER/TV/30 Rock/))")); + + //Act + mocker.Resolve().UpdateWithJson(fakeSeries, host, username, password); + + //Assert + mocker.VerifyAllMocks(); + } + + [Test] + public void UpdateWithJson_All() + { + //Setup + var mocker = new AutoMoqer(); + + var host = "localhost:8080"; + var username = "xbmc"; + var password = "xbmc"; + var serializedQuery = "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTvShows\",\"params\":{\"fields\":[\"file\",\"imdbnumber\"]},\"id\":10}"; + var tvshows = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"limits\":{\"end\":5,\"start\":0,\"total\":5},\"tvshows\":[{\"file\":\"smb://HOMESERVER/TV/7th Heaven/\",\"imdbnumber\":\"73928\",\"label\":\"7th Heaven\",\"tvshowid\":3},{\"file\":\"smb://HOMESERVER/TV/8 Simple Rules/\",\"imdbnumber\":\"78461\",\"label\":\"8 Simple Rules\",\"tvshowid\":4},{\"file\":\"smb://HOMESERVER/TV/24-7 Penguins-Capitals- Road to the NHL Winter Classic/\",\"imdbnumber\":\"213041\",\"label\":\"24/7 Penguins/Capitals: Road to the NHL Winter Classic\",\"tvshowid\":1},{\"file\":\"smb://HOMESERVER/TV/90210/\",\"imdbnumber\":\"82716\",\"label\":\"90210\",\"tvshowid\":5}]}}"; + + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 79488) + .With(s => s.Title = "30 Rock") + .Build(); + + var fakeHttp = mocker.GetMock(); + fakeHttp.Setup(s => s.PostCommand(host, username, password, serializedQuery)) + .Returns(tvshows); + + var fakeEventClient = mocker.GetMock(); + fakeEventClient.Setup(s => s.SendAction("localhost", ActionType.ExecBuiltin, "ExecBuiltIn(UpdateLibrary(video))")); + + //Act + mocker.Resolve().UpdateWithJson(fakeSeries, host, username, password); + + //Assert + mocker.VerifyAllMocks(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Model/Xbmc/ActionType.cs b/NzbDrone.Core/Model/Xbmc/ActionType.cs new file mode 100644 index 000000000..86ed9a390 --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/ActionType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public enum ActionType + { + ExecBuiltin = 0x01, + Button = 0x02 + } +} diff --git a/NzbDrone.Core/Model/Xbmc/ActivePlayersResult.cs b/NzbDrone.Core/Model/Xbmc/ActivePlayersResult.cs new file mode 100644 index 000000000..f2a293964 --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/ActivePlayersResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class ActivePlayersResult + { + public string Id { get; set; } + public string JsonRpc { get; set; } + public Dictionary Result { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Xbmc/Command.cs b/NzbDrone.Core/Model/Xbmc/Command.cs new file mode 100644 index 000000000..c2987b3c4 --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/Command.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class Command + { + public string jsonrpc + { + get { return "2.0"; } + } + + public string method { get; set; } + public Params @params { get; set; } + public long id { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Xbmc/ErrorResult.cs b/NzbDrone.Core/Model/Xbmc/ErrorResult.cs new file mode 100644 index 000000000..8fea7df9c --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/ErrorResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class ErrorResult + { + public string Id { get; set; } + public string JsonRpc { get; set; } + public Dictionary Error { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Xbmc/IconType.cs b/NzbDrone.Core/Model/Xbmc/IconType.cs new file mode 100644 index 000000000..090271028 --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/IconType.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public enum IconType + { + None = 0x00, + Jpeg = 0x01, + Png = 0x02, + Gif = 0x03 + } +} diff --git a/NzbDrone.Core/Model/Xbmc/Params.cs b/NzbDrone.Core/Model/Xbmc/Params.cs new file mode 100644 index 000000000..d867c1f0b --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/Params.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class Params + { + public string[] fields { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Xbmc/TvShow.cs b/NzbDrone.Core/Model/Xbmc/TvShow.cs new file mode 100644 index 000000000..35f44251c --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/TvShow.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class TvShow + { + public int TvShowId { get; set; } + public string Label { get; set; } + public int ImdbNumber { get; set; } + public string File { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Xbmc/TvShowResult.cs b/NzbDrone.Core/Model/Xbmc/TvShowResult.cs new file mode 100644 index 000000000..98c7af800 --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/TvShowResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class TvShowResult + { + public string Id { get; set; } + public string JsonRpc { get; set; } + public Dictionary> Result { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Xbmc/VersionResult.cs b/NzbDrone.Core/Model/Xbmc/VersionResult.cs new file mode 100644 index 000000000..e7eab621d --- /dev/null +++ b/NzbDrone.Core/Model/Xbmc/VersionResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Xbmc +{ + public class VersionResult + { + public string Id { get; set; } + public string JsonRpc { get; set; } + public Dictionary Result { get; set; } + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 79184f712..154d385c2 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -191,6 +191,18 @@ + + + + + + + + + + + + @@ -213,6 +225,7 @@ + @@ -292,6 +305,9 @@ + + + diff --git a/NzbDrone.Core/NzbDrone.jpg b/NzbDrone.Core/NzbDrone.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e544b00fcccfbedcbf1ef2ccc7aebdfd3b45e3e GIT binary patch literal 3299 zcmd6ndpy+n8ppq5Fi0U3m8MclwWW3=D{B&(WY;W05z|eQF32^F5y>U7q?`zg%3?Js zqA-&rD@lV)Gj20-9T_vObN&7HH*IIndF^@ao;~Mv{y5L?`@DX)@Avz9-k<0BKJYu3 z4K(-ew%-j$1<)&3*p?kE6$Yj{BbQJ9#cRBs45M;(Y9-xXbYgiAm|#GOpjq{M*f} zg2LN(?iT%6T=L*yMdhQa>c>x>H9T*8(e!ik%l3}Ww_S{POcrP0{ov5>$mkeXFf~0h zJ2x*BiRD}HL#ffitIPAe{ivYIvRx}53L1oK;)fdOqu0F z%hikvqUCOG!a4`-34IKMQ@EJ|3(*>Cm8=Li81J7q*8qdPhLrS;LWeON4Awbfp?VGs ziZ3Xpc6Qx^+#T7T*ZWr_gdc{%xYU|G?N&NSx!Awr3|04JuI;cx&XS>NOCY6V zMWeY;Vg5>?A-8O)N2OrVb)|_6K@Y3vu5i$%0a27Vpn=JfwlrY1?^}9BkHqCC#K?xM z`ukGDtx|($NXx}M6V3`peBw}M-Cm-4jmWAsYDYn@juWvcZ&d)XVnq7=#5w34dCWFs z%8qna;z({@#K29&Lm7DDT6cbCI~m%03nz<3((34nE3C!BAkCC0mYB%apok}>8(@%5 z z_qlQ@r0e3=H4$unB%K$|J}|t;uqR5I((Q_WjJs>k)^@#c^t}P@X_lh+#vSv^eOgbs z?c0wz^`;gL1fWc(zn*L?g+USwmRaHg1HLf};D!!#bz*PmPP^xYe%;320(3=T3EoDB z!u{y=r3CE*jk}jJx1Wg!btvc~*xg}?p`{#k$5TaKl8QswLA-X_hUv*}9)Zg;E}%JC z?OcBi20xPLzqPuF6UW0KxU-FOG-Xt1Q3HdIiPGT~sJ_ay*wiu{juI8%`Uw|83{>Xp zS19R1j)VHpj&)5guY$NKx`$@`1>c4cj^|#*vTo)cxcq8M`?DkOTUF1{T$%ThB}H~! zWfyGjSV#I;hM85Zr+%f$N5p0bW9x^@7|ld9)x7t@`nR^)H#Y}+X1?O1r)|m9Tp`y& zYcj^FgdS2#)ExWHn@KQeEqi%3#ny5Q3}&HK{0Pd7dxME~fTh>Pk2T@lo_BT!uVj4xPR^zl3s&*XWIV zN=6e)nhCs~^eGu9z&dO=gNDxENojUR(1&u1{0ljbHv+1qo`0eXFw??m@$yWyj$TnN*8ze+~AZ8{SF5P4qu{`-fIo^eM_rY^3Z?4ggL&it{mxk z4%e%~Z`*v|Frft*3I_b-sMIvcrFz=-lIGwsj^TX@ZLLL zY$8#T4u%n~Y!#k*y=Cy$i5J?bcBU3rr>MonpHWKco_I-Npv>=?`o{E>Vdge3yfL-* z-c}D$ucU;P- zVmKdR*jJ{s_vuO!v=n$Sh$wHyIYz6E@|<U@=HX9bLuIcr_r2I)*j`9*AQar;p5 z_NQuETy8jj#%Hrfz_lRK>C>6nlO3ecZGTon_nebJ^V4x+&M@3Co=lP(fZ~F{-XqD+gzB1eU(7DPIgP2yZm4>__LQ{?g*%&S3_e2031&j(?A z!g>kIVqaEw#c<7RvUhjgsmuZIRl?Y|*6Tw-9e%ALlX}$NeyGq`;r!yzCB|ESSE5y7wltth=UfFlYSmGzCJn|^cF?TC5;z7Lg zPiSSmRVf;k7e*wu(Cem#(^fV4I~{M?N0#o>H>2Jwl;nvuI*J6DSEJNO?yNwJMu)6# zc0J``UjVVC%Za#Xy!T1!WE3_$cH(i+tJmIhZ?eZ_%!oM>DNK-r^ymx%Ygdvn+6Y|+ zz%h0RB_HQYWHKb5TT{mQ#|JqRn^usEc?M1MHPPooEZOLPCr=}vP>=g4kKga*;jH{a zdGdahM=sI-KqedLbt~sZ_Q5xYjR;4n!*4Ot$ny_Ud6DE#I;qy0R17S_7D?*3E=1p2 zHZAj~0;&O{CW_c6#R)LoK|?oh*G=SaXH@u%FlU+Lv}Dw^i(S-9NMZOAKa?xeDD~Ob z;wvfwp`z+AY#$Fb);S%m!DP&ZKrNd7VyXUP^GF9gmKzx#DC4nL<`B%$JsTN+ER05i o)vD=fpNNAMtaDzGFHEY!7(RE0ke1dpGGJ||^Bw9hI0k0^0@p@xH2?qr literal 0 HcmV?d00001 diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs index f070fed0c..5ee924b76 100644 --- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs +++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs @@ -120,13 +120,6 @@ namespace NzbDrone.Core.Providers.Core set { SetValue("DownloadPropers", value); } } - public virtual Int32 Retention - { - get { return GetValueInt("Retention"); } - - set { SetValue("Retention", value); } - } - public virtual String SabHost { get { return GetValue("SabHost", "localhost"); } @@ -259,6 +252,59 @@ namespace NzbDrone.Core.Providers.Core set { SetValue("DefaultQualityProfile", value); } } + public virtual Boolean XbmcEnabled + { + get { return GetValueBoolean("XbmcEnabled"); } + + set { SetValue("XbmcEnabled", value); } + } + + public virtual Boolean XbmcNotifyOnGrab + { + get { return GetValueBoolean("XbmcNotifyOnGrab"); } + + set { SetValue("XbmcNotifyOnGrab", value); } + } + + public virtual Boolean XbmcNotifyOnDownload + { + get { return GetValueBoolean("XbmcNotifyOnDownload"); } + + set { SetValue("XbmcNotifyOnDownload", value); } + } + + public virtual Boolean XbmcUpdateLibrary + { + get { return GetValueBoolean("XbmcUpdateLibrary"); } + + set { SetValue("XbmcUpdateLibrary", value); } + } + + public virtual Boolean XbmcCleanLibrary + { + get { return GetValueBoolean("XbmcCleanLibrary"); } + + set { SetValue("XbmcCleanLibrary", value); } + } + + public virtual string XbmcHosts + { + get { return GetValue("XbmcHosts", "localhost:8080"); } + set { SetValue("XbmcHosts", value); } + } + + public virtual string XbmcUsername + { + get { return GetValue("XbmcUsername", "xbmc"); } + set { SetValue("XbmcUsername", value); } + } + + public virtual string XbmcPassword + { + get { return GetValue("XbmcPassword", String.Empty); } + set { SetValue("XbmcPassword", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/NzbDrone.Core/Providers/Core/HttpProvider.cs b/NzbDrone.Core/Providers/Core/HttpProvider.cs index 37ced57fc..e5dc637aa 100644 --- a/NzbDrone.Core/Providers/Core/HttpProvider.cs +++ b/NzbDrone.Core/Providers/Core/HttpProvider.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Net; +using System.Text; using NLog; namespace NzbDrone.Core.Providers.Core @@ -64,5 +65,33 @@ namespace NzbDrone.Core.Providers.Core return false; } } + + public virtual string PostCommand(string address, string username, string password, string command) + { + address += "/jsonrpc"; + + byte[] byteArray = Encoding.ASCII.GetBytes(command); + + var request = WebRequest.Create(address); + request.Method = "POST"; + request.Credentials = new NetworkCredential(username, password); + request.ContentLength = byteArray.Length; + request.ContentType = "application/x-www-form-urlencoded"; + var dataStream = request.GetRequestStream(); + dataStream.Write(byteArray, 0, byteArray.Length); + dataStream.Close(); + + var response = request.GetResponse(); + dataStream = response.GetResponseStream(); + var reader = new StreamReader(dataStream); + // Read the content. + string responseFromServer = reader.ReadToEnd(); + + reader.Close(); + dataStream.Close(); + response.Close(); + + return responseFromServer.Replace(" ", " "); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Providers/Core/UdpProvider.cs b/NzbDrone.Core/Providers/Core/UdpProvider.cs new file mode 100644 index 000000000..73a8a8903 --- /dev/null +++ b/NzbDrone.Core/Providers/Core/UdpProvider.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Ninject; + +namespace NzbDrone.Core.Providers.Core +{ + public class UdpProvider + { + [Inject] + public UdpProvider() + { + + } + + private const int StandardPort = 9777; + private const int MaxPacketSize = 1024; + private const int HeaderSize = 32; + private const int MaxPayloadSize = MaxPacketSize - HeaderSize; + private const byte MajorVersion = 2; + private const byte MinorVersion = 0; + + public enum PacketType + { + Helo = 0x01, + Bye = 0x02, + Button = 0x03, + Mouse = 0x04, + Ping = 0x05, + Broadcast = 0x06, //Currently not implemented + Notification = 0x07, + Blob = 0x08, + Log = 0x09, + Action = 0x0A, + Debug = 0xFF //Currently not implemented + } + + private byte[] Header(PacketType packetType, int numberOfPackets, int currentPacket, int payloadSize, uint uniqueToken) + { + byte[] header = new byte[HeaderSize]; + + header[0] = (byte)'X'; + header[1] = (byte)'B'; + header[2] = (byte)'M'; + header[3] = (byte)'C'; + + header[4] = MajorVersion; + header[5] = MinorVersion; + + if (currentPacket == 1) + { + header[6] = (byte)(((ushort)packetType & 0xff00) >> 8); + header[7] = (byte)((ushort)packetType & 0x00ff); + } + else + { + header[6] = (byte)(((ushort)PacketType.Blob & 0xff00) >> 8); + header[7] = (byte)((ushort)PacketType.Blob & 0x00ff); + } + + header[8] = (byte)((currentPacket & 0xff000000) >> 24); + header[9] = (byte)((currentPacket & 0x00ff0000) >> 16); + header[10] = (byte)((currentPacket & 0x0000ff00) >> 8); + header[11] = (byte)(currentPacket & 0x000000ff); + + header[12] = (byte)((numberOfPackets & 0xff000000) >> 24); + header[13] = (byte)((numberOfPackets & 0x00ff0000) >> 16); + header[14] = (byte)((numberOfPackets & 0x0000ff00) >> 8); + header[15] = (byte)(numberOfPackets & 0x000000ff); + + header[16] = (byte)((payloadSize & 0xff00) >> 8); + header[17] = (byte)(payloadSize & 0x00ff); + + header[18] = (byte)((uniqueToken & 0xff000000) >> 24); + header[19] = (byte)((uniqueToken & 0x00ff0000) >> 16); + header[20] = (byte)((uniqueToken & 0x0000ff00) >> 8); + header[21] = (byte)(uniqueToken & 0x000000ff); + + return header; + + } + + public virtual bool Send(string address, PacketType packetType, byte[] payload) + { + var uniqueToken = (uint)DateTime.Now.TimeOfDay.Milliseconds; + + var socket = Connect(address, StandardPort); + + if (socket == null || !socket.Connected) + { + return false; + } + + try + { + bool successfull = true; + int packetCount = (payload.Length / MaxPayloadSize) + 1; + int bytesToSend = 0; + int bytesSent = 0; + int bytesLeft = payload.Length; + + for (int Package = 1; Package <= packetCount; Package++) + { + + if (bytesLeft > MaxPayloadSize) + { + bytesToSend = MaxPayloadSize; + bytesLeft -= bytesToSend; + } + else + { + bytesToSend = bytesLeft; + bytesLeft = 0; + } + + byte[] header = Header(packetType, packetCount, Package, bytesToSend, uniqueToken); + byte[] packet = new byte[MaxPacketSize]; + + Array.Copy(header, 0, packet, 0, header.Length); + Array.Copy(payload, bytesSent, packet, header.Length, bytesToSend); + + int sendSize = socket.Send(packet, header.Length + bytesToSend, SocketFlags.None); + + if (sendSize != (header.Length + bytesToSend)) + { + successfull = false; + break; + } + + bytesSent += bytesToSend; + } + Disconnect(socket); + return successfull; + } + + catch + { + Disconnect(socket); + return false; + } + } + + private Socket Connect(string address, int port) + { + try + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + IPAddress ip; + if (!IPAddress.TryParse(address, out ip)) + { + IPHostEntry ipHostEntry = Dns.GetHostEntry(address); + foreach (IPAddress ipAddress in ipHostEntry.AddressList) + { + if (ipAddress.AddressFamily == AddressFamily.InterNetwork) + { + ip = ipAddress; + break; + } + } + } + + socket.Connect(new IPEndPoint(ip, port)); + return socket; + } + + catch (Exception exc) + { + Console.WriteLine(exc); + return null; + } + } + + private void Disconnect(Socket socket) + { + try + { + if (socket != null) + { + socket.Shutdown(SocketShutdown.Both); + socket.Close(); + } + } + catch + { + } + } + } +} diff --git a/NzbDrone.Core/Providers/ExternalNotification/ExternalNotificationProviderBase.cs b/NzbDrone.Core/Providers/ExternalNotification/ExternalNotificationProviderBase.cs index 506a75bb8..d21d8bf35 100644 --- a/NzbDrone.Core/Providers/ExternalNotification/ExternalNotificationProviderBase.cs +++ b/NzbDrone.Core/Providers/ExternalNotification/ExternalNotificationProviderBase.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Core.Model; using NzbDrone.Core.Providers.Core; using NzbDrone.Core.Repository; @@ -37,10 +38,19 @@ namespace NzbDrone.Core.Providers.ExternalNotification OnGrab(message); else if (type == ExternalNotificationType.Download) - OnDownload(message, seriesId); + { + throw new NotImplementedException(); + var series = new Series(); + OnDownload(message, series); + } + else if (type == ExternalNotificationType.Rename) - OnRename(message, seriesId); + { + throw new NotImplementedException(); + var series = new Series(); + OnRename(message, series); + } } /// @@ -53,14 +63,14 @@ namespace NzbDrone.Core.Providers.ExternalNotification /// Performs the on download action /// /// The message to send to the receiver - /// The Series ID for the new download - public abstract void OnDownload(string message, int seriesId); + /// The Series for the new download + public abstract void OnDownload(string message, Series series); /// /// Performs the on rename action /// /// The message to send to the receiver - /// The Series ID for the new download - public abstract void OnRename(string message, int seriesId); + /// The Series for the new download + public abstract void OnRename(string message, Series series); } } diff --git a/NzbDrone.Core/Providers/ExternalNotification/XbmcNotificationProvider.cs b/NzbDrone.Core/Providers/ExternalNotification/XbmcNotificationProvider.cs index 8edcade41..458efcec3 100644 --- a/NzbDrone.Core/Providers/ExternalNotification/XbmcNotificationProvider.cs +++ b/NzbDrone.Core/Providers/ExternalNotification/XbmcNotificationProvider.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; namespace NzbDrone.Core.Providers.ExternalNotification { @@ -37,7 +38,7 @@ namespace NzbDrone.Core.Providers.ExternalNotification _logger.Trace("XBMC Notifier is not enabled"); } - public override void OnDownload(string message, int seriesId) + public override void OnDownload(string message, Series series) { const string header = "NzbDrone [TV] - Downloaded"; @@ -52,7 +53,7 @@ namespace NzbDrone.Core.Providers.ExternalNotification if (Convert.ToBoolean(_configProvider.GetValue("XbmcUpdateOnDownload", false))) { _logger.Trace("Sending Update Request to XBMC"); - _xbmcProvider.Update(seriesId); + _xbmcProvider.Update(series); } if (Convert.ToBoolean(_configProvider.GetValue("XbmcCleanOnDownload", false))) @@ -65,7 +66,7 @@ namespace NzbDrone.Core.Providers.ExternalNotification _logger.Trace("XBMC Notifier is not enabled"); } - public override void OnRename(string message, int seriesId) + public override void OnRename(string message, Series series) { const string header = "NzbDrone [TV] - Renamed"; @@ -78,7 +79,7 @@ namespace NzbDrone.Core.Providers.ExternalNotification if (Convert.ToBoolean(_configProvider.GetValue("XbmcUpdateOnRename", false))) { _logger.Trace("Sending Update Request to XBMC"); - _xbmcProvider.Update(seriesId); + _xbmcProvider.Update(series); } if (Convert.ToBoolean(_configProvider.GetValue("XbmcCleanOnRename", false))) diff --git a/NzbDrone.Core/Providers/Xbmc/EventClientProvider.cs b/NzbDrone.Core/Providers/Xbmc/EventClientProvider.cs new file mode 100644 index 000000000..a9d93a7cd --- /dev/null +++ b/NzbDrone.Core/Providers/Xbmc/EventClientProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Ninject; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Model.Xbmc; + +namespace NzbDrone.Core.Providers.Xbmc +{ + public class EventClientProvider + { + private readonly UdpProvider _udpProvider; + + [Inject] + public EventClientProvider(UdpProvider udpProvider) + { + _udpProvider = udpProvider; + } + + public EventClientProvider() + { + } + + public virtual bool SendNotification(string caption, string message, IconType iconType, string iconFile, string address) + { + byte[] icon = new byte[0]; + if (iconType != IconType.None) + { + icon = ResourceManager.GetRawLogo(iconFile); + } + + byte[] payload = new byte[caption.Length + message.Length + 7 + icon.Length]; + + int offset = 0; + + for (int i = 0; i < caption.Length; i++) + payload[offset++] = (byte)caption[i]; + payload[offset++] = (byte)'\0'; + + for (int i = 0; i < message.Length; i++) + payload[offset++] = (byte)message[i]; + payload[offset++] = (byte)'\0'; + + payload[offset++] = (byte)iconType; + + for (int i = 0; i < 4; i++) + payload[offset++] = (byte)0; + + Array.Copy(icon, 0, payload, caption.Length + message.Length + 7, icon.Length); + + return _udpProvider.Send(address, UdpProvider.PacketType.Notification, payload); + } + + public virtual bool SendAction(string address, ActionType action, string messages) + { + var payload = new byte[messages.Length + 2]; + int offset = 0; + payload[offset++] = (byte)action; + + for (int i = 0; i < messages.Length; i++) + payload[offset++] = (byte)messages[i]; + + payload[offset++] = (byte)'\0'; + + return _udpProvider.Send(address, UdpProvider.PacketType.Action, payload); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Providers/Xbmc/ResourceManager.cs b/NzbDrone.Core/Providers/Xbmc/ResourceManager.cs new file mode 100644 index 000000000..0e8e7c548 --- /dev/null +++ b/NzbDrone.Core/Providers/Xbmc/ResourceManager.cs @@ -0,0 +1,55 @@ +namespace NzbDrone.Core.Providers.Xbmc +{ + public class ResourceManager + { + public static System.Drawing.Icon GetIcon(string Name) + { + System.IO.Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}.ico", Name)); + if (stm == null) return null; + return new System.Drawing.Icon(stm); + } + + public static byte[] GetRawData(string Name) + { + byte[] data; + using (System.IO.Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}.ico", Name))) + { + if (stm == null) return null; + data = new byte[stm.Length]; + stm.Read(data, 0, data.Length); + } + + return data; + } + + public static byte[] GetRawLogo(string Name) + { + byte[] data; + using (System.IO.Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}", Name))) + { + if (stm == null) return null; + data = new byte[stm.Length]; + stm.Read(data, 0, data.Length); + } + + return data; + } + + public static System.Drawing.Bitmap GetIconAsImage(string Name) + { + System.IO.Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("{0}.Icons.{1}.ico", typeof(ResourceManager).Namespace, Name)); + if (stm == null) return null; + System.Drawing.Bitmap bmp; + using (System.Drawing.Icon ico = new System.Drawing.Icon(stm)) + { + bmp = new System.Drawing.Bitmap(ico.Width, ico.Height); + using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp)) + { + g.DrawIcon(ico, 0, 0); + } + } + + return bmp; + } + } +} diff --git a/NzbDrone.Core/Providers/XbmcProvider.cs b/NzbDrone.Core/Providers/XbmcProvider.cs index ab766ad97..b8eb5af3a 100644 --- a/NzbDrone.Core/Providers/XbmcProvider.cs +++ b/NzbDrone.Core/Providers/XbmcProvider.cs @@ -1,10 +1,15 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Web.Script.Serialization; using System.Xml.Linq; using Ninject; using NLog; +using NzbDrone.Core.Model.Xbmc; using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Providers.Xbmc; +using NzbDrone.Core.Repository; namespace NzbDrone.Core.Providers { @@ -13,72 +18,134 @@ namespace NzbDrone.Core.Providers private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly ConfigProvider _configProvider; private readonly HttpProvider _httpProvider; + private readonly EventClientProvider _eventClientProvider; [Inject] - public XbmcProvider(ConfigProvider configProvider, HttpProvider httpProvider) + public XbmcProvider(ConfigProvider configProvider, HttpProvider httpProvider, EventClientProvider eventClientProvider) { _configProvider = configProvider; _httpProvider = httpProvider; + _eventClientProvider = eventClientProvider; } public virtual void Notify(string header, string message) { - //Get time in seconds and convert to ms - var time = Convert.ToInt32(_configProvider.GetValue("XbmcDisplayTime", "3")) * 1000; - var command = String.Format("ExecBuiltIn(Notification({0},{1},{2}))", header, message, time); - - if (Convert.ToBoolean(_configProvider.GetValue("XbmcNotificationImage", false))) - { - //Todo: Get the actual port that NzbDrone is running on... - var serverInfo = String.Format("http://{0}:{1}", Environment.MachineName, "8989"); - - var imageUrl = String.Format("{0}/Content/XbmcNotification.png", serverInfo); - command = String.Format("ExecBuiltIn(Notification({0},{1},{2}, {3}))", header, message, time, imageUrl); - } - - foreach (var host in _configProvider.GetValue("XbmcHosts", "localhost:80").Split(',')) + //Always use EventServer, until Json has real support for it + foreach (var host in _configProvider.XbmcHosts.Split(',')) { Logger.Trace("Sending Notifcation to XBMC Host: {0}", host); - SendCommand(host, command); + _eventClientProvider.SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", GetHostWithoutPort(host)); } } - public virtual void Update(int seriesId) + public XbmcProvider() { - foreach (var host in _configProvider.GetValue("XbmcHosts", "localhost:80").Split(',')) - { - Logger.Trace("Sending Update DB Request to XBMC Host: {0}", host); - var xbmcSeriesPath = GetXbmcSeriesPath(host, seriesId); + + } - //If the path is not found & the user wants to update the entire library, do it now. - if (String.IsNullOrEmpty(xbmcSeriesPath) && - Convert.ToBoolean(_configProvider.GetValue("XbmcFullUpdate", false))) + public virtual void Update(Series series) + { + //Use Json for Eden/Nightly or depricated HTTP for 10.x (Dharma) to get the proper path + //Perform update with EventServer (Json currently doesn't support updating a specific path only - July 2011) + + var username = _configProvider.XbmcUsername; + var password = _configProvider.XbmcPassword; + + foreach (var host in _configProvider.XbmcHosts.Split(',')) + { + Logger.Trace("Determining version of XBMC Host: {0}", host); + var version = GetJsonVersion(host, username, password); + + //If Dharma + if (version == 2) + UpdateWithHttp(series, host, username, password); + + //If Eden or newer (attempting to make it future compatible) + else if (version >= 3) + UpdateWithJson(series, password, host, username); + } + } + + public virtual bool UpdateWithJson(Series series, string host, string username, string password) + { + try + { + //Use Json! + var xbmcShows = GetTvShowsJson(host, username, password); + var path = xbmcShows.Where(s => s.ImdbNumber == series.SeriesId || s.Label == series.Title).FirstOrDefault(); + + var hostOnly = GetHostWithoutPort(host); + + if (path != null) { - //Update the entire library - Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", seriesId, host); - SendCommand(host, "ExecBuiltIn(UpdateLibrary(video))"); - return; + Logger.Trace("Updating series [{0}] on XBMC host: {1}", series.Title, host); + var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", path.File); + _eventClientProvider.SendAction(hostOnly, ActionType.ExecBuiltin, command); } - var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", xbmcSeriesPath); - SendCommand(host, command); + else + { + Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host); + var command = String.Format("ExecBuiltIn(UpdateLibrary(video))"); + _eventClientProvider.SendAction(hostOnly, ActionType.ExecBuiltin, command); + } } + + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + return false; + } + + return true; + } + + public virtual bool UpdateWithHttp(Series series, string host, string username, string password) + { + try + { + Logger.Trace("Sending Update DB Request to XBMC Host: {0}", host); + var xbmcSeriesPath = GetXbmcSeriesPath(host, series.SeriesId, username, password); + + //If the path is found update it, else update the whole library + if (!String.IsNullOrEmpty(xbmcSeriesPath)) + { + Logger.Trace("Updating series [{0}] on XBMC host: {1}", series.Title, host); + var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", xbmcSeriesPath); + SendCommand(host, command, username, password); + } + + else + { + //Update the entire library + Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host); + SendCommand(host, "ExecBuiltIn(UpdateLibrary(video))", username, password); + } + } + + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + return false; + } + + return true; } public virtual void Clean() { - foreach (var host in _configProvider.GetValue("XbmcHosts", "localhost:80").Split(',')) + //Use EventServer, once Dharma is extinct use Json? + + foreach (var host in _configProvider.XbmcHosts.Split(',')) { Logger.Trace("Sending DB Clean Request to XBMC Host: {0}", host); - var command = String.Format("ExecBuiltIn(CleanLibrary(video))"); - SendCommand(host, command); + var command = "ExecBuiltIn(CleanLibrary(video))"; + _eventClientProvider.SendAction(GetHostWithoutPort(host), ActionType.ExecBuiltin, command); } } - private string SendCommand(string host, string command) + public virtual string SendCommand(string host, string command, string username, string password) { - var username = _configProvider.GetValue("XbmcUsername", String.Empty); - var password = _configProvider.GetValue("XbmcPassword", String.Empty); var url = String.Format("http://{0}/xbmcCmds/xbmcHttp?command={1}", host, command); if (!String.IsNullOrEmpty(username)) @@ -89,7 +156,7 @@ namespace NzbDrone.Core.Providers return _httpProvider.DownloadString(url); } - private string GetXbmcSeriesPath(string host, int seriesId) + public virtual string GetXbmcSeriesPath(string host, int seriesId, string username, string password) { var query = String.Format( @@ -97,13 +164,13 @@ namespace NzbDrone.Core.Providers seriesId); var command = String.Format("QueryVideoDatabase({0})", query); - var setResponseCommand = + const string setResponseCommand = "SetResponseFormat(webheader;false;webfooter;false;header;;footer;;opentag;;closetag;;closefinaltag;false)"; - var resetResponseCommand = "SetResponseFormat()"; + const string resetResponseCommand = "SetResponseFormat()"; - SendCommand(host, setResponseCommand); - var response = SendCommand(host, command); - SendCommand(host, resetResponseCommand); + SendCommand(host, setResponseCommand, username, password); + var response = SendCommand(host, command, username, password); + SendCommand(host, resetResponseCommand, username, password); if (String.IsNullOrEmpty(response)) return String.Empty; @@ -121,5 +188,109 @@ namespace NzbDrone.Core.Providers return field.Value; } + + public virtual int GetJsonVersion(string host, string username, string password) + { + //2 = Dharma + //3 = Eden/Nightly (as of July 2011) + + var version = 0; + + try + { + var command = new Command { id = 10, method = "JSONRPC.Version" }; + var serializer = new JavaScriptSerializer(); + var serialized = serializer.Serialize(command); + var response = _httpProvider.PostCommand(host, username, password, serialized); + + if (CheckForJsonError(response)) + return version; + + var result = serializer.Deserialize(response); + result.Result.TryGetValue("version", out version); + } + + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + } + + return version; + } + + public virtual Dictionary GetActivePlayers(string host, string username, string password) + { + //2 = Dharma + //3 = Eden/Nightly (as of July 2011) + + try + { + var command = new Command { id = 10, method = "Player.GetActivePlayers" }; + var serializer = new JavaScriptSerializer(); + var serialized = serializer.Serialize(command); + var response = _httpProvider.PostCommand(host, username, password, serialized); + + if (CheckForJsonError(response)) + return null; + + var result = serializer.Deserialize(response); + + return result.Result; + } + + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + } + + return null; + } + + public virtual List GetTvShowsJson(string host, string username, string password) + { + try + { + var fields = new string[] { "file", "imdbnumber" }; + var xbmcParams = new Params { fields = fields }; + var command = new Command { id = 10, method = "VideoLibrary.GetTvShows", @params = xbmcParams }; + var serializer = new JavaScriptSerializer(); + var serialized = serializer.Serialize(command); + var response = _httpProvider.PostCommand(host, username, password, serialized); + + if (CheckForJsonError(response)) + return null; + + var result = serializer.Deserialize(response); + var shows = result.Result["tvshows"]; + + return shows; + } + catch (Exception ex) + { + Logger.DebugException(ex.Message, ex); + } + return null; + } + + public virtual bool CheckForJsonError(string response) + { + if (response.StartsWith("{\"error\"")) + { + var serializer = new JavaScriptSerializer(); + var error = serializer.Deserialize(response); + var code = error.Error["code"]; + var message = error.Error["message"]; + + Logger.Debug("XBMC Json Error. Code = {0}, Message: {1}", code, message); + return true; + } + + return false; + } + + private string GetHostWithoutPort(string address) + { + return address.Split(':')[0]; + } } } \ No newline at end of file diff --git a/NzbDrone.Web/Controllers/SettingsController.cs b/NzbDrone.Web/Controllers/SettingsController.cs index 74a7a8d21..d2b9a42e8 100644 --- a/NzbDrone.Web/Controllers/SettingsController.cs +++ b/NzbDrone.Web/Controllers/SettingsController.cs @@ -140,20 +140,14 @@ namespace NzbDrone.Web.Controllers { var model = new NotificationSettingsModel { - XbmcEnabled = Convert.ToBoolean(_configProvider.GetValue("XbmcEnabled", false)), - XbmcNotifyOnGrab = Convert.ToBoolean(_configProvider.GetValue("XbmcNotifyOnGrab", false)), - XbmcNotifyOnDownload = Convert.ToBoolean(_configProvider.GetValue("XbmcNotifyOnDownload", false)), - XbmcNotifyOnRename = Convert.ToBoolean(_configProvider.GetValue("XbmcNotifyOnRename", false)), - XbmcNotificationImage = Convert.ToBoolean(_configProvider.GetValue("XbmcNotificationImage", false)), - XbmcDisplayTime = Convert.ToInt32(_configProvider.GetValue("XbmcDisplayTime", 3)), - XbmcUpdateOnDownload = Convert.ToBoolean(_configProvider.GetValue("XbmcUpdateOnDownload ", false)), - XbmcUpdateOnRename = Convert.ToBoolean(_configProvider.GetValue("XbmcUpdateOnRename", false)), - XbmcFullUpdate = Convert.ToBoolean(_configProvider.GetValue("XbmcFullUpdate", false)), - XbmcCleanOnDownload = Convert.ToBoolean(_configProvider.GetValue("XbmcCleanOnDownload", false)), - XbmcCleanOnRename = Convert.ToBoolean(_configProvider.GetValue("XbmcCleanOnRename", false)), - XbmcHosts = _configProvider.GetValue("XbmcHosts", "localhost:80"), - XbmcUsername = _configProvider.GetValue("XbmcUsername", String.Empty), - XbmcPassword = _configProvider.GetValue("XbmcPassword", String.Empty) + XbmcEnabled = _configProvider.XbmcEnabled, + XbmcNotifyOnGrab = _configProvider.XbmcNotifyOnGrab, + XbmcNotifyOnDownload = _configProvider.XbmcNotifyOnDownload, + XbmcUpdateLibrary = _configProvider.XbmcUpdateLibrary, + XbmcCleanLibrary = _configProvider.XbmcCleanLibrary, + XbmcHosts = _configProvider.XbmcHosts, + XbmcUsername = _configProvider.XbmcUsername, + XbmcPassword = _configProvider.XbmcPassword }; return View(model); @@ -401,20 +395,14 @@ namespace NzbDrone.Web.Controllers if (ModelState.IsValid) { - _configProvider.SetValue("XbmcEnabled", data.XbmcEnabled.ToString()); - _configProvider.SetValue("XbmcNotifyOnGrab", data.XbmcNotifyOnGrab.ToString()); - _configProvider.SetValue("XbmcNotifyOnDownload", data.XbmcNotifyOnDownload.ToString()); - _configProvider.SetValue("XbmcNotifyOnRename", data.XbmcNotifyOnRename.ToString()); - _configProvider.SetValue("XbmcNotificationImage", data.XbmcNotificationImage.ToString()); - _configProvider.SetValue("XbmcDisplayTime", data.XbmcDisplayTime.ToString()); - _configProvider.SetValue("XbmcUpdateOnDownload", data.XbmcUpdateOnDownload.ToString()); - _configProvider.SetValue("XbmcUpdateOnRename", data.XbmcUpdateOnRename.ToString()); - _configProvider.SetValue("XbmcFullUpdate", data.XbmcFullUpdate.ToString()); - _configProvider.SetValue("XbmcCleanOnDownload", data.XbmcCleanOnDownload.ToString()); - _configProvider.SetValue("XbmcCleanOnRename", data.XbmcCleanOnRename.ToString()); - _configProvider.SetValue("XbmcHosts", data.XbmcHosts); - _configProvider.SetValue("XbmcUsername", data.XbmcUsername); - _configProvider.SetValue("XbmcPassword", data.XbmcPassword); + _configProvider.XbmcEnabled = data.XbmcEnabled; + _configProvider.XbmcNotifyOnGrab = data.XbmcNotifyOnGrab; + _configProvider.XbmcNotifyOnDownload = data.XbmcNotifyOnDownload; + _configProvider.XbmcUpdateLibrary = data.XbmcUpdateLibrary; + _configProvider.XbmcCleanLibrary = data.XbmcCleanLibrary; + _configProvider.XbmcHosts = data.XbmcHosts; + _configProvider.XbmcUsername = data.XbmcUsername; + _configProvider.XbmcPassword = data.XbmcPassword; basicNotification.Title = SETTINGS_SAVED; _notificationProvider.Register(basicNotification); diff --git a/NzbDrone.Web/Models/NotificationSettingsModel.cs b/NzbDrone.Web/Models/NotificationSettingsModel.cs index f17c3e851..27e2026b6 100644 --- a/NzbDrone.Web/Models/NotificationSettingsModel.cs +++ b/NzbDrone.Web/Models/NotificationSettingsModel.cs @@ -17,43 +17,17 @@ namespace NzbDrone.Web.Models [Description("Send notification when episode is downloaded?")] public bool XbmcNotifyOnDownload { get; set; } - [DisplayName("Notify on Rename")] - [Description("Send notification when episode is renamed?")] - public bool XbmcNotifyOnRename { get; set; } + [DisplayName("Update on Download and Rename")] + [Description("Update XBMC library after episode is downloaded or renamed?")] + public bool XbmcUpdateLibrary { get; set; } - [DisplayName("Image with Notification")] - [Description("Display NzbDrone image on notifications?")] - public bool XbmcNotificationImage { get; set; } - - [Required] - [Range(3, 10, ErrorMessage = "Must be between 3 and 10 seconds")] - [DisplayName("Display Time")] - [Description("How long the notification should be displayed")] - public int XbmcDisplayTime { get; set; } - - [DisplayName("Update on Download")] - [Description("Update XBMC library after episode download?")] - public bool XbmcUpdateOnDownload { get; set; } - - [DisplayName("Update on Rename")] - [Description("Update XBMC library after episode is renamed?")] - public bool XbmcUpdateOnRename { get; set; } - - [DisplayName("Full Update")] - [Description("Perform a full update is series update fails?")] - public bool XbmcFullUpdate { get; set; } - - [DisplayName("Clean on Download")] - [Description("Clean XBMC library after episode download?")] - public bool XbmcCleanOnDownload { get; set; } - - [DisplayName("Clean on Rename")] - [Description("Clean XBMC library after episode is renamed?")] - public bool XbmcCleanOnRename { get; set; } + [DisplayName("Clean on Download/Rename")] + [Description("Clean XBMC library after an episode is downloaded or renamed?")] + public bool XbmcCleanLibrary { get; set; } [DataType(DataType.Text)] [DisplayName("Hosts")] - [Description("XBMC hosts with port, comma separ")] + [Description("XBMC hosts with port, comma separated")] [DisplayFormat(ConvertEmptyStringToNull = false)] public string XbmcHosts { get; set; } diff --git a/NzbDrone.Web/Views/Settings/Notifications.cshtml b/NzbDrone.Web/Views/Settings/Notifications.cshtml index 36dce09fd..1b7776cff 100644 --- a/NzbDrone.Web/Views/Settings/Notifications.cshtml +++ b/NzbDrone.Web/Views/Settings/Notifications.cshtml @@ -66,45 +66,15 @@ @Html.CheckBoxFor(m => m.XbmcNotifyOnDownload, new { @class = "inputClass checkClass" }) -