From 6b45758fde376478ee2f5883afdee6f83066f73d Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 2 Sep 2018 00:42:03 -0400 Subject: [PATCH] New: Improved Plex Media Server authentication (Manually update settings) Co-Authored-By: Mark McDowall --- src/Lidarr.Http/ClientSchema/SchemaBuilder.cs | 9 +- .../ClientSchemaTests/SchemaBuilderFixture.cs | 10 +- .../PlexClientServiceTest.cs | 6 +- .../Configuration/ConfigService.cs | 2 + .../Configuration/IConfigService.cs | 2 + .../Plex/{ => HomeTheater}/PlexClient.cs | 2 +- .../{ => HomeTheater}/PlexClientService.cs | 4 +- .../{ => HomeTheater}/PlexClientSettings.cs | 2 +- .../Plex/{ => HomeTheater}/PlexHomeTheater.cs | 8 +- .../PlexHomeTheaterSettings.cs | 4 +- .../Notifications/Plex/PlexServer.cs | 52 ---- .../Notifications/Plex/PlexServerProxy.cs | 268 ------------------ .../Plex/PlexTv/PlexTvPinResponse.cs | 9 + .../Notifications/Plex/PlexTv/PlexTvProxy.cs | 79 ++++++ .../Plex/PlexTv/PlexTvService.cs | 45 +++ .../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 | 4 +- .../Notifications/Plex/Server/PlexServer.cs | 74 +++++ .../Plex/Server/PlexServerProxy.cs | 184 ++++++++++++ .../Plex/{ => Server}/PlexServerService.cs | 11 +- .../Plex/{ => Server}/PlexServerSettings.cs | 14 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 35 +-- .../ApiTests/DownloadClientFixture.cs | 18 +- .../ApiTests/NotificationFixture.cs | 6 +- .../IntegrationTestBase.cs | 4 +- 31 files changed, 479 insertions(+), 403 deletions(-) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexClient.cs (96%) 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 (84%) rename src/NzbDrone.Core/Notifications/Plex/{ => HomeTheater}/PlexHomeTheaterSettings.cs (93%) 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/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 (84%) 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/Lidarr.Http/ClientSchema/SchemaBuilder.cs b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs index d09fa18b0..b9d0769a7 100644 --- a/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs @@ -90,7 +90,7 @@ namespace Lidarr.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, @@ -117,7 +117,7 @@ namespace Lidarr.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))); } } @@ -208,5 +208,10 @@ namespace Lidarr.Http.ClientSchema return fieldValue => fieldValue; } } + + private static string GetCamelCaseName(string name) + { + return Char.ToLowerInvariant(name[0]) + name.Substring(1); + } } } diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index f8fbbb002..8d3f040ea 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -29,10 +29,10 @@ 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" && + 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" && + c.Order == 0 && c.Name == "firstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); } @@ -47,9 +47,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.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 b45ed8e5a..4293f0da1 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -314,6 +314,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 fcf41e31f..e49744337 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -60,6 +60,8 @@ namespace NzbDrone.Core.Configuration //Internal bool CleanupMetadataImages { get; set; } + string PlexClientIdentifier { get; } + //MetadataSource string MetadataSource { get; set; } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClient.cs similarity index 96% rename from src/NzbDrone.Core/Notifications/Plex/PlexClient.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClient.cs index c29a73a84..f6ed23f5a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClient.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -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..7844e3243 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClientService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexClientService.cs @@ -1,9 +1,9 @@ -using System; +using System; 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 203e83f85..3235592b0 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 84% rename from src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheater.cs index d343c90dc..4dc3e3226 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs +++ b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheater.cs @@ -5,7 +5,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Notifications.Xbmc; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.HomeTheater { public class PlexHomeTheater : NotificationBase { @@ -23,12 +23,12 @@ namespace NzbDrone.Core.Notifications.Plex public override void OnGrab(GrabMessage grabMessage) { - Notify(Settings, ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message); + Notify(ALBUM_GRABBED_TITLE_BRANDED, grabMessage.Message); } public override void OnDownload(TrackDownloadMessage message) { - Notify(Settings, TRACK_DOWNLOADED_TITLE_BRANDED, message.Message); + Notify(TRACK_DOWNLOADED_TITLE_BRANDED, message.Message); } public override ValidationResult Test() @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Notifications.Plex return new ValidationResult(failures); } - private void Notify(XbmcSettings settings, string header, string message) + private void Notify(string header, string message) { try { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheaterSettings.cs b/src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs similarity index 93% rename from src/NzbDrone.Core/Notifications/Plex/PlexHomeTheaterSettings.cs rename to src/NzbDrone.Core/Notifications/Plex/HomeTheater/PlexHomeTheaterSettings.cs index dab60fa96..c23d409df 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.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 b57e63c58..000000000 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Music; - -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(TrackDownloadMessage message) - { - UpdateIfEnabled(message.Artist); - } - - public override void OnAlbumDownload(AlbumDownloadMessage message) - { - UpdateIfEnabled(message.Artist); - } - - public override void OnRename(Artist artist) - { - UpdateIfEnabled(artist); - } - - private void UpdateIfEnabled(Artist artist) - { - if (Settings.UpdateLibrary) - { - _plexServerService.UpdateLibrary(artist, 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 402e68866..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 GetArtistSections(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, string mdId, 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 GetArtistSections(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 == "artist") - .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 == "artist") - .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, string mbId, string language, PlexServerSettings settings) - { - var guid = string.Format("com.plexapp.agents.lastfm://{0}?lang={1}", mbId, language); // TODO Plex Route for MB? LastFM? - 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", "Lidarr"); - request.AddHeader("X-Plex-Product", "Lidarr"); - 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/PlexTvProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvProxy.cs new file mode 100644 index 000000000..2179bf058 --- /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 + { + PlexTvPinResponse GetPinCode(string clientIdentifier); + 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 PlexTvPinResponse GetPinCode(string clientIdentifier) + { + var request = BuildRequest(clientIdentifier); + request.Method = HttpMethod.POST; + request.ResourceUrl = "/api/v2/pins"; + request.AddQueryParam("strong", true); + PlexTvPinResponse response; + if (!Json.TryDeserialize(ProcessRequest(request), out response)) + { + response = new PlexTvPinResponse(); + } + return response; + } + 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", "Lidarr") + .AddQueryParam("X-Plex-Platform", "Windows") + .AddQueryParam("X-Plex-Platform-Version", "7") + .AddQueryParam("X-Plex-Device-Name", "Lidarr") + .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..80f7b629c --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs @@ -0,0 +1,45 @@ +using System.Text; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; +namespace NzbDrone.Core.Notifications.Plex.PlexTv +{ + public interface IPlexTvService + { + PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl); + 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 PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl) + { + var clientIdentifier = _configService.PlexClientIdentifier; + var pin = _proxy.GetPinCode(clientIdentifier); + var url = new StringBuilder(); + url.Append("https://app.plex.tv/auth/#!"); + url.Append($"?clientID={clientIdentifier}"); + url.Append($"&forwardUrl={callbackUrl}"); + url.Append($"&code={pin.Code}"); + url.Append($"&context[device][version]=${BuildInfo.Version.ToString()}"); + url.Append("&context[device][product]=Lidarr"); + url.Append("&context[device][platform]=Windows"); + url.Append("&context[device][platformVersion]=7"); + return new PlexTvSignInUrlResponse + { + OauthUrl = url.ToString(), + PinId = pin.Id + }; + } + 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..af57abc50 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 74f5dd4db..f366b246b 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 84% rename from src/NzbDrone.Core/Notifications/Plex/Models/PlexSectionItem.cs rename to src/NzbDrone.Core/Notifications/Plex/Server/PlexSectionItem.cs index 1531d677d..dfd381465 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 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..e3e9ff5ac --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServer.cs @@ -0,0 +1,74 @@ +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.Music; +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(TrackDownloadMessage message) + { + UpdateIfEnabled(message.Artist); + } + public override void OnAlbumDownload(AlbumDownloadMessage message) + { + UpdateIfEnabled(message.Artist); + } + public override void OnRename(Artist artist) + { + UpdateIfEnabled(artist); + } + private void UpdateIfEnabled(Artist artist) + { + if (Settings.UpdateLibrary) + { + _plexServerService.UpdateLibrary(artist, 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(); + if (query["callbackUrl"].IsNullOrWhiteSpace()) + { + throw new BadRequestException("QueryParam callbackUrl invalid."); + } + return _plexTvService.GetSignInUrl(query["callbackUrl"]); + } + 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..8e604c9bd --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerProxy.cs @@ -0,0 +1,184 @@ +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 GetArtistSections(PlexServerSettings settings); + void Update(int sectionId, PlexServerSettings settings); + void UpdateArtist(int metadataId, PlexServerSettings settings); + string Version(PlexServerSettings settings); + List Preferences(PlexServerSettings settings); + int? GetMetadataId(int sectionId, string mbId, 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 GetArtistSections(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 == "artist") + .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 == "artist") + .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 UpdateArtist(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, string mbId, string language, PlexServerSettings settings) + { + var guid = string.Format("com.plexapp.agents.lastfm://{0}?lang={1}", mbId, language); // TODO Plex Route for MB? LastFM? + 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", "Lidarr") + .AddQueryParam("X-Plex-Platform", "Windows") + .AddQueryParam("X-Plex-Platform-Version", "7") + .AddQueryParam("X-Plex-Device-Name", "Lidarr") + .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 aeeb85905..924978220 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs @@ -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.Music; -namespace NzbDrone.Core.Notifications.Plex +namespace NzbDrone.Core.Notifications.Plex.Server { public interface IPlexServerService { @@ -108,14 +107,10 @@ 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); @@ -141,7 +136,7 @@ namespace NzbDrone.Core.Notifications.Plex if (metadataId.HasValue) { _logger.Debug("Updating Plex host: {0}, Section: {1}, Artist: {2}", settings.Host, section.Id, artist); - _plexServerProxy.UpdateSeries(metadataId.Value, settings); + _plexServerProxy.UpdateArtist(metadataId.Value, settings); partiallyUpdated = true; } @@ -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 f5dcc1b87..3e751dc56 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -882,18 +882,24 @@ - - - - - + + + + + + + + + - - - + + + + + @@ -998,17 +1004,14 @@ - + 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 6400e2aee..1bb66bacc 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -324,8 +324,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); }