diff --git a/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs b/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs index bb1dea1f0..90b0d2c5f 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs @@ -33,79 +33,6 @@ namespace NzbDrone.Core.Test.NotificationTests }; } - [Test] - public void GetSectionKeys_should_return_single_section_key_when_only_one_show_section() - { - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) - .Returns(stream); - - var result = Mocker.Resolve().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 }); - - - 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() - { - - - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) - .Returns(stream); - - - var result = Mocker.Resolve().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 }); - - - result.Should().HaveCount(1); - result.First().Should().Be(5); - } - - [Test] - public void GetSectionKeys_should_return_multiple_section_keys_when_there_are_multiple_show_sections() - { - - - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) - .Returns(stream); - - - var result = Mocker.Resolve().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 }); - - - result.Should().HaveCount(2); - result.First().Should().Be(5); - result.Last().Should().Be(6); - } - - [Test] - public void UpdateSection_should_update_section() - { - - - 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); - - - Mocker.Resolve().UpdateSection(new PlexServerSettings { Host = "localhost", Port = 32400 }, 5); - - - - } - [Test] public void Notify_should_send_notification() { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexError.cs b/src/NzbDrone.Core/Notifications/Plex/PlexError.cs new file mode 100644 index 000000000..78e733050 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexError.cs @@ -0,0 +1,9 @@ +using System; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexError + { + public String Error { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexException.cs b/src/NzbDrone.Core/Notifications/Plex/PlexException.cs new file mode 100644 index 000000000..5447c563b --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexException : NzbDroneException + { + public PlexException(string message) : base(message) + { + } + + public PlexException(string message, params object[] args) : base(message, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexSection.cs b/src/NzbDrone.Core/Notifications/Plex/PlexSection.cs new file mode 100644 index 000000000..d43fb53f6 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexSection.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexSection + { + public Int32 Id { get; set; } + public String Path { get; set; } + } + + public class PlexDirectory + { + public String Type { get; set; } + + [JsonProperty("_children")] + public List Sections { get; set; } + } + + public class PlexMediaContainer + { + [JsonProperty("_children")] + public List Directories { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs new file mode 100644 index 000000000..e0cf3e061 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Serializer; +using RestSharp; + +namespace NzbDrone.Core.Notifications.Plex +{ + public interface IPlexServerProxy + { + List GetTvSections(PlexServerSettings settings); + void Update(int sectionId, PlexServerSettings settings); + } + + public class PlexServerProxy : IPlexServerProxy + { + private readonly ICached _authCache; + + public PlexServerProxy(ICacheManager cacheManager) + { + _authCache = cacheManager.GetCache(GetType(), "authCache"); + } + + public List GetTvSections(PlexServerSettings settings) + { + var request = GetPlexServerRequest("library/sections", Method.GET, settings); + var client = GetPlexServerClient(settings); + + var response = client.Execute(request); + + return Json.Deserialize(response.Content) + .Directories + .Where(d => d.Type == "show") + .SelectMany(d => d.Sections) + .ToList(); + } + + public void Update(int sectionId, PlexServerSettings settings) + { + var resource = String.Format("library/sections/{2}/refresh"); + var request = GetPlexServerRequest(resource, Method.GET, settings); + var client = GetPlexServerClient(settings); + + var response = client.Execute(request); + } + + private String Authenticate(string username, string password) + { + var request = GetMyPlexRequest("users/sign_in.json", Method.POST); + var client = GetMyPlexClient(username, password); + + var response = client.Execute(request); + CheckForError(response.Content); + + var user = Json.Deserialize(JObject.Parse(response.Content).SelectToken("user").ToString()); + + _authCache.Set(username, user.AuthenticationToken); + + return user.AuthenticationToken; + } + + private RestClient GetMyPlexClient(string username, string password) + { + var client = new RestClient("https://my.plexapp.com"); + client.Authenticator = new HttpBasicAuthenticator(username, password); + + return client; + } + + private RestRequest GetMyPlexRequest(string resource, Method method) + { + var request = new RestRequest(resource, method); + request.AddHeader("X-Plex-Platform", "Windows"); + request.AddHeader("X-Plex-Platform-Version", "7"); + request.AddHeader("X-Plex-Provides", "player"); + request.AddHeader("X-Plex-Client-Identifier", "AB6CCCC7-5CF5-4523-826A-B969E0FFD8A0"); + request.AddHeader("X-Plex-Product", "PlexWMC"); + request.AddHeader("X-Plex-Version", "0"); + + return request; + + } + + private RestClient GetPlexServerClient(PlexServerSettings settings) + { + return new RestClient(String.Format("http://{0}:{1}", settings.Host, settings.Port)); + } + + private RestRequest GetPlexServerRequest(string resource, Method method, PlexServerSettings settings) + { + var request = new RestRequest(resource, method); + request.AddHeader("Accept", "application/json"); + + if (!settings.Username.IsNullOrWhiteSpace()) + { + request.AddParameter("X-Plex-Token", GetAuthenticationToken(settings.Username, settings.Password)); + } + + return request; + } + + private string GetAuthenticationToken(string username, string password) + { + return _authCache.Get(username, () => Authenticate(username, password)); + } + + private void CheckForError(string response) + { + var error = Json.Deserialize(response); + + if (error != null && !error.Error.IsNullOrWhiteSpace()) + { + throw new PlexException(error.Error); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs index 272d0efcc..89be29eb0 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Notifications.Plex [FieldDefinition(2, Label = "Username")] public String Username { get; set; } - [FieldDefinition(3, Label = "Password")] + [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public String Password { get; set; } [FieldDefinition(4, Label = "Update Library", Type = FieldType.Checkbox)] diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexService.cs index 0d4c6608b..a27b55d3a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexService.cs @@ -18,11 +18,13 @@ namespace NzbDrone.Core.Notifications.Plex public class PlexService : IPlexService, IExecute, IExecute { private readonly IHttpProvider _httpProvider; + private readonly IPlexServerProxy _plexServerProxy; private readonly Logger _logger; - public PlexService(IHttpProvider httpProvider, Logger logger) + public PlexService(IHttpProvider httpProvider, IPlexServerProxy plexServerProxy, Logger logger) { _httpProvider = httpProvider; + _plexServerProxy = plexServerProxy; _logger = logger; } @@ -45,7 +47,7 @@ namespace NzbDrone.Core.Notifications.Plex { _logger.Debug("Sending Update Request to Plex Server"); var sections = GetSectionKeys(settings); - sections.ForEach(s => UpdateSection(settings, s)); + sections.ForEach(s => UpdateSection(s, settings)); } catch(Exception ex) @@ -55,26 +57,21 @@ namespace NzbDrone.Core.Notifications.Plex } } - public List GetSectionKeys(PlexServerSettings settings) + private List GetSectionKeys(PlexServerSettings settings) { _logger.Debug("Getting sections from Plex host: {0}", settings.Host); - var url = String.Format("http://{0}:{1}/library/sections", settings.Host, settings.Port); - var xmlStream = _httpProvider.DownloadStream(url, GetCredentials(settings)); - 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(); + return _plexServerProxy.GetTvSections(settings).Select(s => s.Id).ToList(); } - public void UpdateSection(PlexServerSettings settings, int key) + private void UpdateSection(int key, PlexServerSettings settings) { _logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, key); - var url = String.Format("http://{0}:{1}/library/sections/{2}/refresh", settings.Host, settings.Port, key); - _httpProvider.DownloadString(url, GetCredentials(settings)); + + _plexServerProxy.Update(key, settings); } - public string SendCommand(string host, int port, string command, string username, string password) + private string SendCommand(string host, int port, string command, string username, string password) { var url = String.Format("http://{0}:{1}/xbmcCmds/xbmcHttp?command={2}", host, port, command); @@ -86,13 +83,6 @@ namespace NzbDrone.Core.Notifications.Plex return _httpProvider.DownloadString(url); } - private NetworkCredential GetCredentials(PlexServerSettings settings) - { - if (settings.Username.IsNullOrWhiteSpace()) return null; - - return new NetworkCredential(settings.Username, settings.Password); - } - public void Execute(TestPlexClientCommand message) { _logger.Debug("Sending Test Notifcation to Plex Client: {0}", message.Host); diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs b/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs new file mode 100644 index 000000000..4ca9a642a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs @@ -0,0 +1,11 @@ +using System; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexUser + { + [JsonProperty("authentication_token")] + public String AuthenticationToken { get; set; } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9ec718804..a27327dba 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -377,6 +377,11 @@ + + + + +