From 07be9cf47a53cbe67dc75f33becc0f6434eff9a3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 5 Jun 2018 00:39:15 -0700 Subject: [PATCH] New: Improved Plex Media Server authentication (Manually update settings) --- .../ClientSchemaTests/SchemaBuilderFixture.cs | 12 +- src/NzbDrone.Api/ProviderModuleBase.cs | 5 +- .../PlexClientServiceTest.cs | 6 +- .../Configuration/ConfigService.cs | 2 + .../Configuration/IConfigService.cs | 2 +- .../Plex/{ => HomeTheater}/PlexClient.cs | 5 +- .../{ => HomeTheater}/PlexClientService.cs | 2 +- .../{ => HomeTheater}/PlexClientSettings.cs | 2 +- .../Plex/{ => HomeTheater}/PlexHomeTheater.cs | 3 +- .../PlexHomeTheaterSettings.cs | 2 +- .../Notifications/Plex/PlexServer.cs | 47 --- .../Notifications/Plex/PlexServerProxy.cs | 268 ------------------ .../Plex/PlexTv/PlexTvPinResponse.cs | 9 + .../Plex/PlexTv/PlexTvPinUrlResponse.cs | 11 + .../Notifications/Plex/PlexTv/PlexTvProxy.cs | 79 ++++++ .../Plex/PlexTv/PlexTvService.cs | 84 ++++++ .../Plex/PlexTv/PlexTvSignInUrlResponse.cs | 8 + .../Notifications/Plex/PlexUser.cs | 10 - .../Plex/{ => Server}/PlexError.cs | 2 +- .../Plex/{Models => Server}/PlexIdentity.cs | 2 +- .../{Models => Server}/PlexPreferences.cs | 4 +- .../Plex/{Models => Server}/PlexResponse.cs | 2 +- .../Plex/{Models => Server}/PlexSection.cs | 2 +- .../{Models => Server}/PlexSectionItem.cs | 2 +- .../Notifications/Plex/Server/PlexServer.cs | 102 +++++++ .../Plex/Server/PlexServerProxy.cs | 226 +++++++++++++++ .../Plex/{ => Server}/PlexServerService.cs | 11 +- .../Plex/{ => Server}/PlexServerSettings.cs | 14 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 36 +-- .../ApiTests/DownloadClientFixture.cs | 18 +- .../ApiTests/NotificationFixture.cs | 6 +- .../IntegrationTestBase.cs | 4 +- src/Sonarr.Api.V3/ProviderModuleBase.cs | 5 +- src/Sonarr.Http/ClientSchema/SchemaBuilder.cs | 11 +- 34 files changed, 602 insertions(+), 402 deletions(-) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexClient.cs (91%) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexClientService.cs (97%) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexClientSettings.cs (95%) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexHomeTheater.cs (96%) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexHomeTheaterSettings.cs (95%) delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexServer.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs delete mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexUser.cs rename src/NzbDrone.Core/Notifications/Plex/{ => Server}/PlexError.cs (62%) rename src/NzbDrone.Core/Notifications/Plex/{Models => Server}/PlexIdentity.cs (73%) rename src/NzbDrone.Core/Notifications/Plex/{Models => Server}/PlexPreferences.cs (84%) rename src/NzbDrone.Core/Notifications/Plex/{Models => Server}/PlexResponse.cs (64%) rename src/NzbDrone.Core/Notifications/Plex/{Models => Server}/PlexSection.cs (96%) rename src/NzbDrone.Core/Notifications/Plex/{Models => Server}/PlexSectionItem.cs (91%) create mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs rename src/NzbDrone.Core/Notifications/Plex/{ => Server}/PlexServerService.cs (96%) rename src/NzbDrone.Core/Notifications/Plex/{ => Server}/PlexServerSettings.cs (78%) diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index a49020351..497725c65 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Annotations; using NzbDrone.Test.Common; @@ -28,8 +28,8 @@ namespace NzbDrone.Api.Test.ClientSchemaTests var schema = SchemaBuilder.ToSchema(model); - schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop"); - schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); + schema.Should().Contain(c => c.Order == 1 && c.Name == "lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop"); + schema.Should().Contain(c => c.Order == 0 && c.Name == "firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); } @@ -42,9 +42,9 @@ namespace NzbDrone.Api.Test.ClientSchemaTests var schema = SchemaBuilder.ToSchema(model); - schema.Should().Contain(c => c.Order == 0 && c.Name == "Name.FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); - schema.Should().Contain(c => c.Order == 1 && c.Name == "Name.LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop"); - schema.Should().Contain(c => c.Order == 2 && c.Name == "Quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote"); + schema.Should().Contain(c => c.Order == 0 && c.Name == "name.firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); + schema.Should().Contain(c => c.Order == 1 && c.Name == "name.lastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop"); + schema.Should().Contain(c => c.Order == 2 && c.Name == "quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote"); } } diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index 6705437bf..cdb0cd9a2 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; @@ -8,6 +8,7 @@ using NzbDrone.Common.Reflection; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using Newtonsoft.Json; +using NzbDrone.Common.Serializer; using Sonarr.Http; using Sonarr.Http.ClientSchema; using Sonarr.Http.Mapping; @@ -191,7 +192,7 @@ namespace NzbDrone.Api var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); var data = _providerFactory.RequestAction(providerDefinition, action, query); - Response resp = JsonConvert.SerializeObject(data); + Response resp = data.ToJson(); resp.ContentType = "application/json"; return resp; } diff --git a/src/NzbDrone.Core.Test/NotificationTests/PlexClientServiceTest.cs b/src/NzbDrone.Core.Test/NotificationTests/PlexClientServiceTest.cs index f9b826703..35f7206a0 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/PlexClientServiceTest.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/PlexClientServiceTest.cs @@ -1,7 +1,7 @@ -using Moq; +using Moq; using NUnit.Framework; using NzbDrone.Common.Http; -using NzbDrone.Core.Notifications.Plex; +using NzbDrone.Core.Notifications.Plex.HomeTheater; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.NotificationTests @@ -69,4 +69,4 @@ namespace NzbDrone.Core.Test.NotificationTests fakeHttp.Verify(v => v.DownloadString(expectedUrl, "plex", "plex"), Times.Once()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index f7144cf04..3fea2c560 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -306,6 +306,8 @@ namespace NzbDrone.Core.Configuration set { SetValue("CleanupMetadataImages", value); } } + public string PlexClientIdentifier => GetValue("PlexClientIdentifier", Guid.NewGuid().ToString(), true); + public string RijndaelPassphrase => GetValue("RijndaelPassphrase", Guid.NewGuid().ToString(), true); public string HmacPassphrase => GetValue("HmacPassphrase", Guid.NewGuid().ToString(), true); diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index c60284ee0..10427020a 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Configuration //Internal bool CleanupMetadataImages { get; set; } - + string PlexClientIdentifier { get; } //Forms Auth string RijndaelPassphrase { get; } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClient.cs similarity index 91% rename from src/NzbDrone.Core/Notifications/Plex/PlexClient.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClient.cs index 1294c6a40..1a1758955 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClient.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.HomeTheater { public class PlexClient : NotificationBase { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClientService.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientService.cs similarity index 97% rename from src/NzbDrone.Core/Notifications/Plex/PlexClientService.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientService.cs index 476b9d27d..3d1411ec8 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClientService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientService.cs @@ -3,7 +3,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Http; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.HomeTheater { public interface IPlexClientService { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientSettings.cs similarity index 95% rename from src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientSettings.cs index d10993d79..e1d6be34a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientSettings.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.HomeTheater { public class PlexClientSettingsValidator : AbstractValidator { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheater.cs similarity index 96% rename from src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheater.cs index c90473471..43f840821 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheater.cs @@ -4,9 +4,8 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Notifications.Xbmc; -using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.HomeTheater { public class PlexHomeTheater : NotificationBase { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheaterSettings.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs similarity index 95% rename from src/NzbDrone.Core/Notifications/Plex/PlexHomeTheaterSettings.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs index dab60fa96..589d75f7a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheaterSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs @@ -1,7 +1,7 @@ using NzbDrone.Core.Annotations; using NzbDrone.Core.Notifications.Xbmc; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.HomeTheater { public class PlexHomeTheaterSettings : XbmcSettings { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs deleted file mode 100644 index b691ae282..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Notifications.Plex -{ - public class PlexServer : NotificationBase - { - private readonly IPlexServerService _plexServerService; - - public PlexServer(IPlexServerService plexServerService) - { - _plexServerService = plexServerService; - } - - public override string Link => "https://www.plex.tv/"; - public override string Name => "Plex Media Server"; - - public override void OnDownload(DownloadMessage message) - { - UpdateIfEnabled(message.Series); - } - - public override void OnRename(Series series) - { - UpdateIfEnabled(series); - } - - private void UpdateIfEnabled(Series series) - { - if (Settings.UpdateLibrary) - { - _plexServerService.UpdateLibrary(series, Settings); - } - } - - public override ValidationResult Test() - { - var failures = new List(); - - failures.AddIfNotNull(_plexServerService.Test(Settings)); - - return new ValidationResult(failures); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs deleted file mode 100644 index 10b500b71..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using Newtonsoft.Json.Linq; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Notifications.Plex.Models; -using NzbDrone.Core.Rest; -using RestSharp; -using RestSharp.Authenticators; - -namespace NzbDrone.Core.Notifications.Plex -{ - public interface IPlexServerProxy - { - List GetTvSections(PlexServerSettings settings); - void Update(int sectionId, PlexServerSettings settings); - void UpdateSeries(int metadataId, PlexServerSettings settings); - string Version(PlexServerSettings settings); - List Preferences(PlexServerSettings settings); - int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings); - } - - public class PlexServerProxy : IPlexServerProxy - { - private readonly ICached _authCache; - private readonly Logger _logger; - - public PlexServerProxy(ICacheManager cacheManager, Logger logger) - { - _authCache = cacheManager.GetCache(GetType(), "authCache"); - _logger = logger; - } - - public List GetTvSections(PlexServerSettings settings) - { - var request = GetPlexServerRequest("library/sections", Method.GET, settings); - var client = GetPlexServerClient(settings); - var response = client.Execute(request); - - _logger.Trace("Sections response: {0}", response.Content); - CheckForError(response, settings); - - if (response.Content.Contains("_children")) - { - return Json.Deserialize(response.Content) - .Sections - .Where(d => d.Type == "show") - .Select(s => new PlexSection - { - Id = s.Id, - Language = s.Language, - Locations = s.Locations, - Type = s.Type - }) - .ToList(); - } - - return Json.Deserialize>(response.Content) - .MediaContainer - .Sections - .Where(d => d.Type == "show") - .ToList(); - } - - public void Update(int sectionId, PlexServerSettings settings) - { - var resource = string.Format("library/sections/{0}/refresh", sectionId); - var request = GetPlexServerRequest(resource, Method.GET, settings); - var client = GetPlexServerClient(settings); - var response = client.Execute(request); - - _logger.Trace("Update response: {0}", response.Content); - CheckForError(response, settings); - } - - public void UpdateSeries(int metadataId, PlexServerSettings settings) - { - var resource = string.Format("library/metadata/{0}/refresh", metadataId); - var request = GetPlexServerRequest(resource, Method.PUT, settings); - var client = GetPlexServerClient(settings); - var response = client.Execute(request); - - _logger.Trace("Update Series response: {0}", response.Content); - CheckForError(response, settings); - } - - public string Version(PlexServerSettings settings) - { - var request = GetPlexServerRequest("identity", Method.GET, settings); - var client = GetPlexServerClient(settings); - var response = client.Execute(request); - - _logger.Trace("Version response: {0}", response.Content); - CheckForError(response, settings); - - if (response.Content.Contains("_children")) - { - return Json.Deserialize(response.Content) - .Version; - } - - return Json.Deserialize>(response.Content) - .MediaContainer - .Version; - } - - public List Preferences(PlexServerSettings settings) - { - var request = GetPlexServerRequest(":/prefs", Method.GET, settings); - var client = GetPlexServerClient(settings); - var response = client.Execute(request); - - _logger.Trace("Preferences response: {0}", response.Content); - CheckForError(response, settings); - - if (response.Content.Contains("_children")) - { - return Json.Deserialize(response.Content) - .Preferences; - } - - return Json.Deserialize>(response.Content) - .MediaContainer - .Preferences; - } - - public int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings) - { - var guid = string.Format("com.plexapp.agents.thetvdb://{0}?lang={1}", tvdbId, language); - var resource = string.Format("library/sections/{0}/all?guid={1}", sectionId, System.Web.HttpUtility.UrlEncode(guid)); - var request = GetPlexServerRequest(resource, Method.GET, settings); - var client = GetPlexServerClient(settings); - var response = client.Execute(request); - - _logger.Trace("Sections response: {0}", response.Content); - CheckForError(response, settings); - - List items; - - if (response.Content.Contains("_children")) - { - items = Json.Deserialize(response.Content) - .Items; - } - - else - { - items = Json.Deserialize>(response.Content) - .MediaContainer - .Items; - } - - if (items == null || items.Empty()) - { - return null; - } - - return items.First().Id; - } - - private string Authenticate(PlexServerSettings settings) - { - var request = GetPlexTvRequest("users/sign_in.json", Method.POST); - var client = GetPlexTvClient(settings.Username, settings.Password); - - var response = client.Execute(request); - - _logger.Debug("Authentication Response: {0}", response.Content); - CheckForError(response, settings); - - var user = Json.Deserialize(JObject.Parse(response.Content).SelectToken("user").ToString()); - - return user.AuthenticationToken; - } - - private RestClient GetPlexTvClient(string username, string password) - { - var client = RestClientFactory.BuildClient("https://plex.tv"); - client.Authenticator = new HttpBasicAuthenticator(username, password); - - return client; - } - - private RestRequest GetPlexTvRequest(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-Device-Name", "Sonarr"); - request.AddHeader("X-Plex-Product", "Sonarr"); - request.AddHeader("X-Plex-Version", BuildInfo.Version.ToString()); - - return request; - } - - private RestClient GetPlexServerClient(PlexServerSettings settings) - { - var protocol = settings.UseSsl ? "https" : "http"; - - return RestClientFactory.BuildClient(string.Format("{0}://{1}:{2}", protocol, 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.IsNotNullOrWhiteSpace()) - { - request.AddParameter("X-Plex-Token", GetAuthenticationToken(settings), ParameterType.HttpHeader); - } - - return request; - } - - private string GetAuthenticationToken(PlexServerSettings settings) - { - var token = _authCache.Get(settings.Username + settings.Password, () => Authenticate(settings)); - - if (token.IsNullOrWhiteSpace()) - { - throw new PlexAuthenticationException("Invalid Token - Update your username and password"); - } - - return token; - } - - private void CheckForError(IRestResponse response, PlexServerSettings settings) - { - _logger.Trace("Checking for error"); - - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - if (settings.Username.IsNullOrWhiteSpace()) - { - throw new PlexAuthenticationException("Unauthorized - Username and password required"); - } - - //Set the token to null in the cache so we don't keep trying with bad credentials - _authCache.Set(settings.Username + settings.Password, null); - throw new PlexAuthenticationException("Unauthorized - Username or password is incorrect"); - } - - if (response.Content.IsNullOrWhiteSpace()) - { - _logger.Trace("No response body returned, no error detected"); - return; - } - - var error = response.Content.Contains("_children") ? - Json.Deserialize(response.Content) : - Json.Deserialize>(response.Content).MediaContainer; - - if (error != null && !error.Error.IsNullOrWhiteSpace()) - { - throw new PlexException(error.Error); - } - - _logger.Trace("No error detected"); - } - } -} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs new file mode 100644 index 000000000..aa46edb48 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinResponse.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public class PlexTvPinResponse + { + public int Id { get; set; } + public string Code { get; set; } + public string AuthToken { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs new file mode 100644 index 000000000..4dace5645 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvPinUrlResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public class PlexTvPinUrlResponse + { + public string Url { get; set; } + public string Method => "POST"; + public Dictionary Headers { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs new file mode 100644 index 000000000..ce019093a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs @@ -0,0 +1,79 @@ +using System.Net; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Exceptions; + +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public interface IPlexTvProxy + { + string GetAuthToken(string clientIdentifier, int pinId); + } + + public class PlexTvProxy : IPlexTvProxy + { + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public PlexTvProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public string GetAuthToken(string clientIdentifier, int pinId) + { + var request = BuildRequest(clientIdentifier); + request.ResourceUrl = $"/api/v2/pins/{pinId}"; + + PlexTvPinResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request), out response)) + { + response = new PlexTvPinResponse(); + } + + return response.AuthToken; + } + + private HttpRequestBuilder BuildRequest(string clientIdentifier) + { + var requestBuilder = new HttpRequestBuilder("https://plex.tv") + .Accept(HttpAccept.Json) + .AddQueryParam("X-Plex-Client-Identifier", clientIdentifier) + .AddQueryParam("X-Plex-Product", "Sonarr") + .AddQueryParam("X-Plex-Platform", "Windows") + .AddQueryParam("X-Plex-Platform-Version", "7") + .AddQueryParam("X-Plex-Device-Name", "Sonarr") + .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()); + + return requestBuilder; + } + + private string ProcessRequest(HttpRequestBuilder requestBuilder) + { + var httpRequest = requestBuilder.Build(); + + HttpResponse response; + + _logger.Debug("Url: {0}", httpRequest.Url); + + try + { + response = _httpClient.Execute(httpRequest); + } + catch (HttpException ex) + { + throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv"); + } + catch (WebException ex) + { + throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv"); + } + + return response.Content; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs new file mode 100644 index 000000000..6751e7a5e --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Text; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public interface IPlexTvService + { + PlexTvPinUrlResponse GetPinUrl(); + PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode); + string GetAuthToken(int pinId); + } + + public class PlexTvService : IPlexTvService + { + private readonly IPlexTvProxy _proxy; + private readonly IConfigService _configService; + + public PlexTvService(IPlexTvProxy proxy, IConfigService configService) + { + _proxy = proxy; + _configService = configService; + } + + public PlexTvPinUrlResponse GetPinUrl() + { + var clientIdentifier = _configService.PlexClientIdentifier; + + var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins") + .Accept(HttpAccept.Json) + .AddQueryParam("X-Plex-Client-Identifier", clientIdentifier) + .AddQueryParam("X-Plex-Product", "Sonarr") + .AddQueryParam("X-Plex-Platform", "Windows") + .AddQueryParam("X-Plex-Platform-Version", "7") + .AddQueryParam("X-Plex-Device-Name", "Sonarr") + .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()) + .AddQueryParam("strong", true); + + requestBuilder.Method = HttpMethod.POST; + + var request = requestBuilder.Build(); + + return new PlexTvPinUrlResponse + { + Url = request.Url.ToString(), + Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value) + }; + } + + public PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode) + { + var clientIdentifier = _configService.PlexClientIdentifier; + + var requestBuilder = new HttpRequestBuilder("https://app.plex.tv/auth/hashBang") + .AddQueryParam("clientID", clientIdentifier) + .AddQueryParam("forwardUrl", callbackUrl) + .AddQueryParam("code", pinCode) + .AddQueryParam("context[device][product]", "Sonarr") + .AddQueryParam("context[device][platform]", "Windows") + .AddQueryParam("context[device][platformVersion]", "7") + .AddQueryParam("context[device][version]", BuildInfo.Version.ToString()); + + // #! is stripped out of the URL when building, this works around it. + requestBuilder.Segments.Add("hashBang", "#!"); + + var request = requestBuilder.Build(); + + return new PlexTvSignInUrlResponse + { + OauthUrl = request.Url.ToString(), + PinId = pinId + }; + } + + public string GetAuthToken(int pinId) + { + var authToken = _proxy.GetAuthToken(_configService.PlexClientIdentifier, pinId); + + return authToken; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs new file mode 100644 index 000000000..33bd2a8ff --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvSignInUrlResponse.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public class PlexTvSignInUrlResponse + { + public string OauthUrl { get; set; } + public int PinId { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs b/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs deleted file mode 100644 index 105166227..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs +++ /dev/null @@ -1,10 +0,0 @@ -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/Notifications/Plex/PlexError.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs similarity index 62% rename from src/NzbDrone.Core/Notifications/Plex/PlexError.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs index 9bb7b33a8..3018c080a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexError.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexError.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexError { diff --git a/src/NzbDrone.Core/Notifications/Plex/Models/PlexIdentity.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs similarity index 73% rename from src/NzbDrone.Core/Notifications/Plex/Models/PlexIdentity.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs index 1d2b03c0f..9762421e8 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Models/PlexIdentity.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexIdentity.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Notifications.Plex.Models +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexIdentity { diff --git a/src/NzbDrone.Core/Notifications/Plex/Models/PlexPreferences.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs similarity index 84% rename from src/NzbDrone.Core/Notifications/Plex/Models/PlexPreferences.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs index 1cea5ef58..dc1ebc3a1 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Models/PlexPreferences.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexPreferences.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; -namespace NzbDrone.Core.Notifications.Plex.Models +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexPreferences { diff --git a/src/NzbDrone.Core/Notifications/Plex/Models/PlexResponse.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs similarity index 64% rename from src/NzbDrone.Core/Notifications/Plex/Models/PlexResponse.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs index 7d2214f54..e053b76f3 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Models/PlexResponse.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexResponse.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Core.Notifications.Plex.Models +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexResponse { diff --git a/src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs similarity index 96% rename from src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs index beeb96be3..2062c4057 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Models/PlexSection.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexSection.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace NzbDrone.Core.Notifications.Plex.Models +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexSectionLocation { diff --git a/src/NzbDrone.Core/Notifications/Plex/Models/PlexSectionItem.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs similarity index 91% rename from src/NzbDrone.Core/Notifications/Plex/Models/PlexSectionItem.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs index 1531d677d..70a8aaa3b 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Models/PlexSectionItem.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace NzbDrone.Core.Notifications.Plex.Models +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexSectionItem { diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs new file mode 100644 index 000000000..bf0ee81c3 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Notifications.Plex.PlexTv; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Plex.Server +{ + public class PlexServer : NotificationBase + { + private readonly IPlexServerService _plexServerService; + private readonly IPlexTvService _plexTvService; + + public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService) + { + _plexServerService = plexServerService; + _plexTvService = plexTvService; + } + + public override string Link => "https://www.plex.tv/"; + public override string Name => "Plex Media Server"; + + public override void OnDownload(DownloadMessage message) + { + UpdateIfEnabled(message.Series); + } + + public override void OnRename(Series series) + { + UpdateIfEnabled(series); + } + + private void UpdateIfEnabled(Series series) + { + if (Settings.UpdateLibrary) + { + _plexServerService.UpdateLibrary(series, Settings); + } + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_plexServerService.Test(Settings)); + + return new ValidationResult(failures); + } + + public override object RequestAction(string action, IDictionary query) + { + if (action == "startOAuth") + { + Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError(); + + return _plexTvService.GetPinUrl(); + } + else if (action == "continueOAuth") + { + Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError(); + + if (query["callbackUrl"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam callbackUrl invalid."); + } + + if (query["id"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam id invalid."); + } + + if (query["code"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam code invalid."); + } + + return _plexTvService.GetSignInUrl(query["callbackUrl"], Convert.ToInt32(query["id"]), query["code"]); + } + else if (action == "getOAuthToken") + { + Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError(); + + if (query["pinId"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam pinId invalid."); + } + + var authToken = _plexTvService.GetAuthToken(Convert.ToInt32(query["pinId"])); + + return new + { + authToken + }; + } + + return new { }; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs new file mode 100644 index 000000000..f1fd67db9 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs @@ -0,0 +1,226 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Notifications.Plex.Server +{ + public interface IPlexServerProxy + { + List GetTvSections(PlexServerSettings settings); + void Update(int sectionId, PlexServerSettings settings); + void UpdateSeries(int metadataId, PlexServerSettings settings); + string Version(PlexServerSettings settings); + List Preferences(PlexServerSettings settings); + int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings); + } + + public class PlexServerProxy : IPlexServerProxy + { + private readonly IHttpClient _httpClient; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public PlexServerProxy(IHttpClient httpClient, IConfigService configService,Logger logger) + { + _httpClient = httpClient; + _configService = configService; + _logger = logger; + } + + public List GetTvSections(PlexServerSettings settings) + { + var request = BuildRequest("library/sections", HttpMethod.GET, settings); + var response = ProcessRequest(request); + + CheckForError(response); + + if (response.Contains("_children")) + { + return Json.Deserialize(response) + .Sections + .Where(d => d.Type == "show") + .Select(s => new PlexSection + { + Id = s.Id, + Language = s.Language, + Locations = s.Locations, + Type = s.Type + }) + .ToList(); + } + + return Json.Deserialize>(response) + .MediaContainer + .Sections + .Where(d => d.Type == "show") + .ToList(); + } + + public void Update(int sectionId, PlexServerSettings settings) + { + var resource = $"library/sections/{sectionId}/refresh"; + var request = BuildRequest(resource, HttpMethod.GET, settings); + var response = ProcessRequest(request); + + CheckForError(response); + } + + public void UpdateSeries(int metadataId, PlexServerSettings settings) + { + var resource = $"library/metadata/{metadataId}/refresh"; + var request = BuildRequest(resource, HttpMethod.PUT, settings); + var response = ProcessRequest(request); + + CheckForError(response); + } + + public string Version(PlexServerSettings settings) + { + var request = BuildRequest("identity", HttpMethod.GET, settings); + var response = ProcessRequest(request); + + CheckForError(response); + + if (response.Contains("_children")) + { + return Json.Deserialize(response) + .Version; + } + + return Json.Deserialize>(response) + .MediaContainer + .Version; + } + + public List Preferences(PlexServerSettings settings) + { + var request = BuildRequest(":/prefs", HttpMethod.GET, settings); + var response = ProcessRequest(request); + + CheckForError(response); + + if (response.Contains("_children")) + { + return Json.Deserialize(response) + .Preferences; + } + + return Json.Deserialize>(response) + .MediaContainer + .Preferences; + } + + public int? GetMetadataId(int sectionId, int tvdbId, string language, PlexServerSettings settings) + { + var guid = $"com.plexapp.agents.thetvdb://{tvdbId}?lang={language}"; + var resource = $"library/sections/{sectionId}/all?guid={System.Web.HttpUtility.UrlEncode(guid)}"; + var request = BuildRequest(resource, HttpMethod.GET, settings); + var response = ProcessRequest(request); + + CheckForError(response); + + List items; + + if (response.Contains("_children")) + { + items = Json.Deserialize(response) + .Items; + } + + else + { + items = Json.Deserialize>(response) + .MediaContainer + .Items; + } + + if (items == null || items.Empty()) + { + return null; + } + + return items.First().Id; + } + + private HttpRequestBuilder BuildRequest(string resource, HttpMethod method, PlexServerSettings settings) + { + var scheme = settings.UseSsl ? "https" : "http"; + + var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host}:{settings.Port}") + .Accept(HttpAccept.Json) + .AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier) + .AddQueryParam("X-Plex-Product", "Sonarr") + .AddQueryParam("X-Plex-Platform", "Windows") + .AddQueryParam("X-Plex-Platform-Version", "7") + .AddQueryParam("X-Plex-Device-Name", "Sonarr") + .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()); + + if (settings.AuthToken.IsNotNullOrWhiteSpace()) + { + requestBuilder.AddQueryParam("X-Plex-Token", settings.AuthToken); + } + + requestBuilder.ResourceUrl = resource; + requestBuilder.Method = method; + + return requestBuilder; + } + + private string ProcessRequest(HttpRequestBuilder requestBuilder) + { + var httpRequest = requestBuilder.Build(); + + HttpResponse response; + + _logger.Debug("Url: {0}", httpRequest.Url); + + try + { + response = _httpClient.Execute(httpRequest); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + throw new PlexAuthenticationException("Unauthorized - AuthToken is invalid"); + } + + throw new PlexException("Unable to connect to Plex Media Server"); + } + catch (WebException ex) + { + throw new PlexException("Unable to connect to Plex Media Server"); + } + + return response.Content; + } + + private void CheckForError(string response) + { + _logger.Trace("Checking for error"); + + if (response.IsNullOrWhiteSpace()) + { + _logger.Trace("No response body returned, no error detected"); + return; + } + + var error = response.Contains("_children") ? + Json.Deserialize(response) : + Json.Deserialize>(response).MediaContainer; + + if (error != null && !error.Error.IsNullOrWhiteSpace()) + { + throw new PlexException(error.Error); + } + + _logger.Trace("No error detected"); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs similarity index 96% rename from src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs index 891815529..dfe7de47f 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -6,10 +6,9 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Notifications.Plex.Models; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.Server { public interface IPlexServerService { @@ -109,13 +108,9 @@ namespace NzbDrone.Core.Notifications.Plex var rawVersion = _plexServerProxy.Version(settings); var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-')); - - return version; } - - private List GetPreferences(PlexServerSettings settings) { _logger.Debug("Getting preferences from Plex host: {0}", settings.Host); @@ -176,7 +171,7 @@ namespace NzbDrone.Core.Notifications.Plex catch(PlexAuthenticationException ex) { _logger.Error(ex, "Unable to connect to Plex Server"); - return new ValidationFailure("Username", "Incorrect username or password"); + return new ValidationFailure("AuthToken", "Invalid authentication token"); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs similarity index 78% rename from src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs index 9a5d0587c..fd843ebba 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerSettings.cs @@ -1,9 +1,9 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.Server { public class PlexServerSettingsValidator : AbstractValidator { @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Notifications.Plex { Port = 32400; UpdateLibrary = true; + SignIn = "startOAuth"; } [FieldDefinition(0, Label = "Host")] @@ -30,12 +31,11 @@ namespace NzbDrone.Core.Notifications.Plex [FieldDefinition(1, Label = "Port")] public int Port { get; set; } - //TODO: Change username and password to token and get a plex.tv OAuth token properly - [FieldDefinition(2, Label = "Username")] - public string Username { get; set; } + [FieldDefinition(2, Label = "Auth Token", Type = FieldType.Textbox, Advanced = true)] + public string AuthToken { get; set; } - [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] - public string Password { get; set; } + [FieldDefinition(3, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)] + public string SignIn { get; set; } [FieldDefinition(4, Label = "Update Library", Type = FieldType.Checkbox)] public bool UpdateLibrary { get; set; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index d30f96914..93c112169 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -771,6 +771,11 @@ + + + + + @@ -936,18 +941,18 @@ - - - - - + + + + + - - - + + + @@ -1038,17 +1043,16 @@ - + Code - - + + - - - - - + + + + Code diff --git a/src/NzbDrone.Integration.Test/ApiTests/DownloadClientFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/DownloadClientFixture.cs index 5032fd3c6..302eb780a 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/DownloadClientFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/DownloadClientFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FluentAssertions; using NUnit.Framework; @@ -16,8 +16,8 @@ namespace NzbDrone.Integration.Test.ApiTests var schema = DownloadClients.Schema().First(v => v.Implementation == "UsenetBlackhole"); schema.Enable = true; - schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); - schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); + schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); + schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); DownloadClients.InvalidPost(schema); } @@ -31,7 +31,7 @@ namespace NzbDrone.Integration.Test.ApiTests schema.Enable = true; schema.Name = "Test UsenetBlackhole"; - schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); + schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); DownloadClients.InvalidPost(schema); } @@ -45,7 +45,7 @@ namespace NzbDrone.Integration.Test.ApiTests schema.Enable = true; schema.Name = "Test UsenetBlackhole"; - schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); + schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); DownloadClients.InvalidPost(schema); } @@ -59,8 +59,8 @@ namespace NzbDrone.Integration.Test.ApiTests schema.Enable = true; schema.Name = "Test UsenetBlackhole"; - schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); - schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); + schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); + schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); var result = DownloadClients.Post(schema); @@ -99,7 +99,7 @@ namespace NzbDrone.Integration.Test.ApiTests EnsureNoDownloadClient(); var client = EnsureDownloadClient(); - client.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb2"); + client.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb2"); var result = DownloadClients.Put(client); result.Should().NotBeNull(); @@ -117,4 +117,4 @@ namespace NzbDrone.Integration.Test.ApiTests DownloadClients.All().Should().NotContain(v => v.Id == client.Id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs b/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs index c5ebfa8ef..85ba510ff 100644 --- a/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs +++ b/src/NzbDrone.Integration.Test/ApiTests/NotificationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using FluentAssertions; using NUnit.Framework; @@ -34,10 +34,10 @@ namespace NzbDrone.Integration.Test.ApiTests var xbmc = schema.Single(s => s.Implementation.Equals("Xbmc", StringComparison.InvariantCultureIgnoreCase)); xbmc.Name = "Test XBMC"; - xbmc.Fields.Single(f => f.Name.Equals("Host")).Value = "localhost"; + xbmc.Fields.Single(f => f.Name.Equals("host")).Value = "localhost"; var result = Notifications.Post(xbmc); Notifications.Delete(result.Id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index 3a20d4c70..ecd7ac6e5 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -329,8 +329,8 @@ namespace NzbDrone.Integration.Test schema.Enable = enabled; schema.Name = "Test UsenetBlackhole"; - schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); - schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); + schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); + schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); client = DownloadClients.Post(schema); } diff --git a/src/Sonarr.Api.V3/ProviderModuleBase.cs b/src/Sonarr.Api.V3/ProviderModuleBase.cs index b4c750a79..2b477f16c 100644 --- a/src/Sonarr.Api.V3/ProviderModuleBase.cs +++ b/src/Sonarr.Api.V3/ProviderModuleBase.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; using Nancy; using Newtonsoft.Json; +using NzbDrone.Common.Serializer; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using Sonarr.Http; @@ -172,7 +173,7 @@ namespace Sonarr.Api.V3 var query = ((IDictionary)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); var data = _providerFactory.RequestAction(providerDefinition, action, query); - Response resp = JsonConvert.SerializeObject(data); + Response resp = data.ToJson(); resp.ContentType = "application/json"; return resp; } diff --git a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index 6fc751151..927945b0a 100644 --- a/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -93,7 +93,7 @@ namespace Sonarr.Http.ClientSchema var fieldAttribute = property.Item2; var field = new Field { - Name = prefix + propertyInfo.Name, + Name = prefix + GetCamelCaseName(propertyInfo.Name), Label = fieldAttribute.Label, Unit = fieldAttribute.Unit, HelpText = fieldAttribute.HelpText, @@ -121,7 +121,7 @@ namespace Sonarr.Http.ClientSchema } else { - result.AddRange(GetFieldMapping(propertyInfo.PropertyType, propertyInfo.Name + ".", t => propertyInfo.GetValue(targetSelector(t), null))); + result.AddRange(GetFieldMapping(propertyInfo.PropertyType, GetCamelCaseName(propertyInfo.Name) + ".", t => propertyInfo.GetValue(targetSelector(t), null))); } } @@ -212,5 +212,10 @@ namespace Sonarr.Http.ClientSchema return fieldValue => fieldValue; } } + + private static string GetCamelCaseName(string name) + { + return Char.ToLowerInvariant(name[0]) + name.Substring(1); + } } }