New: Improved Plex Media Server authentication (Manually update settings)

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
pull/481/head
Qstick 6 years ago
parent aab70b512d
commit 6b45758fde

@ -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);
}
}
}

@ -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");
}
}

@ -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());
}
}
}
}

@ -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);

@ -60,6 +60,8 @@ namespace NzbDrone.Core.Configuration
//Internal
bool CleanupMetadataImages { get; set; }
string PlexClientIdentifier { get; }
//MetadataSource
string MetadataSource { get; set; }

@ -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<PlexClientSettings>
{

@ -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
{

@ -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<PlexClientSettings>
{

@ -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<PlexHomeTheaterSettings>
{
@ -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
{

@ -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
{

@ -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<PlexServerSettings>
{
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<ValidationFailure>();
failures.AddIfNotNull(_plexServerService.Test(Settings));
return new ValidationResult(failures);
}
}
}

@ -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<PlexSection> GetArtistSections(PlexServerSettings settings);
void Update(int sectionId, PlexServerSettings settings);
void UpdateSeries(int metadataId, PlexServerSettings settings);
string Version(PlexServerSettings settings);
List<PlexPreference> Preferences(PlexServerSettings settings);
int? GetMetadataId(int sectionId, string mdId, string language, PlexServerSettings settings);
}
public class PlexServerProxy : IPlexServerProxy
{
private readonly ICached<string> _authCache;
private readonly Logger _logger;
public PlexServerProxy(ICacheManager cacheManager, Logger logger)
{
_authCache = cacheManager.GetCache<string>(GetType(), "authCache");
_logger = logger;
}
public List<PlexSection> 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<PlexMediaContainerLegacy>(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<PlexResponse<PlexSectionsContainer>>(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<PlexIdentity>(response.Content)
.Version;
}
return Json.Deserialize<PlexResponse<PlexIdentity>>(response.Content)
.MediaContainer
.Version;
}
public List<PlexPreference> 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<PlexPreferencesLegacy>(response.Content)
.Preferences;
}
return Json.Deserialize<PlexResponse<PlexPreferences>>(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<PlexSectionItem> items;
if (response.Content.Contains("_children"))
{
items = Json.Deserialize<PlexSectionResponseLegacy>(response.Content)
.Items;
}
else
{
items = Json.Deserialize<PlexResponse<PlexSectionResponse>>(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<PlexUser>(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<PlexError>(response.Content) :
Json.Deserialize<PlexResponse<PlexError>>(response.Content).MediaContainer;
if (error != null && !error.Error.IsNullOrWhiteSpace())
{
throw new PlexException(error.Error);
}
_logger.Trace("No error detected");
}
}
}

@ -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; }
}
}

@ -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<PlexTvPinResponse>(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<PlexTvPinResponse>(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;
}
}
}

@ -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;
}
}
}

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
public class PlexTvSignInUrlResponse
{
public string OauthUrl { get; set; }
public int PinId { get; set; }
}
}

@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexUser
{
[JsonProperty("authentication_token")]
public string AuthenticationToken { get; set; }
}
}

@ -1,4 +1,4 @@
namespace NzbDrone.Core.Notifications.Plex
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexError
{

@ -1,4 +1,4 @@
namespace NzbDrone.Core.Notifications.Plex.Models
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexIdentity
{

@ -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
{

@ -1,4 +1,4 @@
namespace NzbDrone.Core.Notifications.Plex.Models
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexResponse<T>
{

@ -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
{

@ -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
{

@ -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<PlexServerSettings>
{
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<ValidationFailure>();
failures.AddIfNotNull(_plexServerService.Test(Settings));
return new ValidationResult(failures);
}
public override object RequestAction(string action, IDictionary<string, string> 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 { };
}
}
}

@ -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<PlexSection> GetArtistSections(PlexServerSettings settings);
void Update(int sectionId, PlexServerSettings settings);
void UpdateArtist(int metadataId, PlexServerSettings settings);
string Version(PlexServerSettings settings);
List<PlexPreference> 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<PlexSection> GetArtistSections(PlexServerSettings settings)
{
var request = BuildRequest("library/sections", HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexMediaContainerLegacy>(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<PlexResponse<PlexSectionsContainer>>(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<PlexIdentity>(response)
.Version;
}
return Json.Deserialize<PlexResponse<PlexIdentity>>(response)
.MediaContainer
.Version;
}
public List<PlexPreference> Preferences(PlexServerSettings settings)
{
var request = BuildRequest(":/prefs", HttpMethod.GET, settings);
var response = ProcessRequest(request);
CheckForError(response);
if (response.Contains("_children"))
{
return Json.Deserialize<PlexPreferencesLegacy>(response)
.Preferences;
}
return Json.Deserialize<PlexResponse<PlexPreferences>>(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<PlexSectionItem> items;
if (response.Contains("_children"))
{
items = Json.Deserialize<PlexSectionResponseLegacy>(response)
.Items;
}
else
{
items = Json.Deserialize<PlexResponse<PlexSectionResponse>>(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<PlexError>(response) :
Json.Deserialize<PlexResponse<PlexError>>(response).MediaContainer;
if (error != null && !error.Error.IsNullOrWhiteSpace())
{
throw new PlexException(error.Error);
}
_logger.Trace("No error detected");
}
}
}

@ -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<PlexPreference> 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)
{

@ -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<PlexServerSettings>
{
@ -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; }

@ -882,18 +882,24 @@
<Compile Include="Notifications\MediaBrowser\Model\EmbyMediaFolder.cs" />
<Compile Include="Notifications\MediaBrowser\Model\EmbyMediaFoldersResponse.cs" />
<Compile Include="Notifications\MediaBrowser\Model\EmbyMediaUpdateInfo.cs" />
<Compile Include="Notifications\Plex\Models\PlexIdentity.cs" />
<Compile Include="Notifications\Plex\Models\PlexResponse.cs" />
<Compile Include="Notifications\Plex\Models\PlexPreferences.cs" />
<Compile Include="Notifications\Plex\Models\PlexSectionItem.cs" />
<Compile Include="Notifications\Plex\Models\PlexSection.cs" />
<Compile Include="Notifications\Plex\Server\PlexIdentity.cs" />
<Compile Include="Notifications\Plex\Server\PlexResponse.cs" />
<Compile Include="Notifications\Plex\Server\PlexPreferences.cs" />
<Compile Include="Notifications\Plex\Server\PlexSectionItem.cs" />
<Compile Include="Notifications\Plex\Server\PlexSection.cs" />
<Compile Include="Notifications\Plex\PlexAuthenticationException.cs" />
<Compile Include="Notifications\CustomScript\CustomScript.cs" />
<Compile Include="Notifications\CustomScript\CustomScriptSettings.cs" />
<Compile Include="Notifications\Plex\PlexTv\PlexTvPinResponse.cs" />
<Compile Include="Notifications\Plex\PlexTv\PlexTvProxy.cs" />
<Compile Include="Notifications\Plex\PlexTv\PlexTvService.cs" />
<Compile Include="Notifications\Plex\PlexTv\PlexTvSignInUrlResponse.cs" />
<Compile Include="Notifications\Plex\PlexVersionException.cs" />
<Compile Include="Notifications\Plex\PlexHomeTheater.cs" />
<Compile Include="Notifications\Plex\PlexHomeTheaterSettings.cs" />
<Compile Include="Notifications\Plex\PlexClientService.cs" />
<Compile Include="Notifications\Plex\HomeTheater\PlexHomeTheater.cs" />
<Compile Include="Notifications\Plex\HomeTheater\PlexHomeTheaterSettings.cs" />
<Compile Include="Notifications\Plex\HomeTheater\PlexClientService.cs" />
<Compile Include="Notifications\Plex\Server\PlexServer.cs" />
<Compile Include="Notifications\Plex\Server\PlexServerProxy.cs" />
<Compile Include="Notifications\PushBullet\PushBulletException.cs" />
<Compile Include="Notifications\Slack\Payloads\Attachment.cs" />
<Compile Include="Notifications\Slack\Payloads\SlackPayload.cs" />
@ -998,17 +1004,14 @@
<Compile Include="Notifications\NotificationFactory.cs" />
<Compile Include="Notifications\NotificationRepository.cs" />
<Compile Include="Notifications\NotificationService.cs" />
<Compile Include="Notifications\Plex\PlexClient.cs">
<Compile Include="Notifications\Plex\HomeTheater\PlexClient.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Notifications\Plex\PlexClientSettings.cs" />
<Compile Include="Notifications\Plex\PlexError.cs" />
<Compile Include="Notifications\Plex\HomeTheater\PlexClientSettings.cs" />
<Compile Include="Notifications\Plex\Server\PlexError.cs" />
<Compile Include="Notifications\Plex\PlexException.cs" />
<Compile Include="Notifications\Plex\PlexServer.cs" />
<Compile Include="Notifications\Plex\PlexServerProxy.cs" />
<Compile Include="Notifications\Plex\PlexServerSettings.cs" />
<Compile Include="Notifications\Plex\PlexServerService.cs" />
<Compile Include="Notifications\Plex\PlexUser.cs" />
<Compile Include="Notifications\Plex\Server\PlexServerSettings.cs" />
<Compile Include="Notifications\Plex\Server\PlexServerService.cs" />
<Compile Include="Notifications\Prowl\InvalidApiKeyException.cs" />
<Compile Include="Notifications\Prowl\Prowl.cs">
<SubType>Code</SubType>

@ -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);
}
}
}
}

@ -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);
}
}
}
}

@ -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);
}

Loading…
Cancel
Save