New: Add Plex Media Server notifications

pull/2904/head
Bogdan 9 months ago
parent 75213c86a1
commit 722da83984

@ -0,0 +1,15 @@
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexAuthenticationException : PlexException
{
public PlexAuthenticationException(string message)
: base(message)
{
}
public PlexAuthenticationException(string message, params object[] args)
: base(message, args)
{
}
}
}

@ -0,0 +1,23 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexException : NzbDroneException
{
public PlexException(string message)
: base(message)
{
}
public PlexException(string message, params object[] args)
: base(message, args)
{
}
public PlexException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

@ -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,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<string, string> Headers { get; set; }
}
}

@ -0,0 +1,102 @@
using System;
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);
bool Ping(string clientIdentifier, string authToken);
}
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}";
if (!Json.TryDeserialize<PlexTvPinResponse>(ProcessRequest(request), out var response))
{
response = new PlexTvPinResponse();
}
return response.AuthToken;
}
public bool Ping(string clientIdentifier, string authToken)
{
try
{
// Allows us to tell plex.tv that we're still active and tokens should not be expired.
var request = BuildRequest(clientIdentifier);
request.ResourceUrl = "/api/v2/ping";
request.AddQueryParam("X-Plex-Token", authToken);
ProcessRequest(request);
return true;
}
catch (Exception e)
{
// Catch all exceptions and log at trace, this information could be interesting in debugging, but expired tokens will be handled elsewhere.
_logger.Trace(e, "Unable to ping plex.tv");
}
return false;
}
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", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.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)
{
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
}
return response.Content;
}
}
}

@ -0,0 +1,95 @@
using System;
using System.Linq;
using System.Net.Http;
using NzbDrone.Common.Cache;
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);
void Ping(string authToken);
}
public class PlexTvService : IPlexTvService
{
private readonly IPlexTvProxy _proxy;
private readonly IConfigService _configService;
private readonly ICached<bool> _cache;
public PlexTvService(IPlexTvProxy proxy, IConfigService configService, ICacheManager cacheManager)
{
_proxy = proxy;
_configService = configService;
_cache = cacheManager.GetCache<bool>(GetType());
}
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", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.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]", BuildInfo.AppName)
.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;
}
public void Ping(string authToken)
{
// Ping plex.tv if we haven't done so in the last 24 hours for this auth token.
_cache.Get(authToken, () => _proxy.Ping(_configService.PlexClientIdentifier, authToken), TimeSpan.FromHours(24));
}
}
}

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

@ -0,0 +1,17 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Plex
{
public class PlexVersionException : NzbDroneException
{
public PlexVersionException(string message)
: base(message)
{
}
public PlexVersionException(string message, params object[] args)
: base(message, args)
{
}
}
}

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexError
{
public string Error { get; set; }
}
}

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexIdentity
{
public string MachineIdentifier { get; set; }
public string Version { get; set; }
}
}

@ -0,0 +1,24 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexPreferences
{
[JsonProperty("Setting")]
public List<PlexPreference> Preferences { get; set; }
}
public class PlexPreference
{
public string Id { get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
public class PlexPreferencesLegacy
{
[JsonProperty("_children")]
public List<PlexPreference> Preferences { get; set; }
}
}

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexResponse<T>
{
public T MediaContainer { get; set; }
}
}

@ -0,0 +1,57 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexSectionLocation
{
public int Id { get; set; }
public string Path { get; set; }
}
public class PlexSection
{
public PlexSection()
{
Locations = new List<PlexSectionLocation>();
}
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("Location")]
public List<PlexSectionLocation> Locations { get; set; }
}
public class PlexSectionsContainer
{
public PlexSectionsContainer()
{
Sections = new List<PlexSection>();
}
[JsonProperty("Directory")]
public List<PlexSection> Sections { get; set; }
}
public class PlexSectionLegacy
{
[JsonProperty("key")]
public int Id { get; set; }
public string Type { get; set; }
public string Language { get; set; }
[JsonProperty("_children")]
public List<PlexSectionLocation> Locations { get; set; }
}
public class PlexMediaContainerLegacy
{
[JsonProperty("_children")]
public List<PlexSectionLegacy> Sections { get; set; }
}
}

@ -0,0 +1,37 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexSectionItem
{
[JsonProperty("ratingKey")]
public string Id { get; set; }
public string Title { get; set; }
public int Year { get; set; }
public string Guid { get; set; }
}
public class PlexSectionResponse
{
[JsonProperty("Metadata")]
public List<PlexSectionItem> Items { get; set; }
public PlexSectionResponse()
{
Items = new List<PlexSectionItem>();
}
}
public class PlexSectionResponseLegacy
{
[JsonProperty("_children")]
public List<PlexSectionItem> Items { get; set; }
public PlexSectionResponseLegacy()
{
Items = new List<PlexSectionItem>();
}
}
}

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications.Plex.PlexTv;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexServer : NotificationBase<PlexServerSettings>
{
private readonly IPlexServerService _plexServerService;
private readonly IPlexTvService _plexTvService;
private readonly Logger _logger;
private class PlexUpdateQueue
{
public Dictionary<int, Author> Pending { get; } = new ();
public bool Refreshing { get; set; }
}
private readonly ICached<PlexUpdateQueue> _pendingAuthorsCache;
public PlexServer(IPlexServerService plexServerService, IPlexTvService plexTvService, ICacheManager cacheManager, Logger logger)
{
_plexServerService = plexServerService;
_plexTvService = plexTvService;
_logger = logger;
_pendingAuthorsCache = cacheManager.GetRollingCache<PlexUpdateQueue>(GetType(), "pendingAuthors", TimeSpan.FromDays(1));
}
public override string Link => "https://www.plex.tv/";
public override string Name => "Plex Media Server";
public override void OnReleaseImport(BookDownloadMessage message)
{
UpdateIfEnabled(message.Author);
}
public override void OnRename(Author author, List<RenamedBookFile> renamedFiles)
{
UpdateIfEnabled(author);
}
public override void OnBookRetag(BookRetagMessage message)
{
UpdateIfEnabled(message.Author);
}
public override void OnBookDelete(BookDeleteMessage deleteMessage)
{
if (deleteMessage.DeletedFiles)
{
UpdateIfEnabled(deleteMessage.Book.Author);
}
}
public override void OnAuthorDelete(AuthorDeleteMessage deleteMessage)
{
if (deleteMessage.DeletedFiles)
{
UpdateIfEnabled(deleteMessage.Author);
}
}
private void UpdateIfEnabled(Author author)
{
_plexTvService.Ping(Settings.AuthToken);
if (Settings.UpdateLibrary)
{
_logger.Debug("Scheduling library update for author {0} {1}", author.Id, author.Name);
var queue = _pendingAuthorsCache.Get(Settings.Host, () => new PlexUpdateQueue());
lock (queue)
{
queue.Pending[author.Id] = author;
}
}
}
public override void ProcessQueue()
{
var queue = _pendingAuthorsCache.Find(Settings.Host);
if (queue == null)
{
return;
}
lock (queue)
{
if (queue.Refreshing)
{
return;
}
queue.Refreshing = true;
}
try
{
while (true)
{
List<Author> refreshingAuthors;
lock (queue)
{
if (queue.Pending.Empty())
{
queue.Refreshing = false;
return;
}
refreshingAuthors = queue.Pending.Values.ToList();
queue.Pending.Clear();
}
if (Settings.UpdateLibrary)
{
_logger.Debug("Performing library update for {0} authors", refreshingAuthors.Count);
_plexServerService.UpdateLibrary(refreshingAuthors, Settings);
}
}
}
catch
{
lock (queue)
{
queue.Refreshing = false;
}
throw;
}
}
public override ValidationResult Test()
{
_plexTvService.Ping(Settings.AuthToken);
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();
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 { };
}
}
}

@ -0,0 +1,173 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
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> GetTvSections(PlexServerSettings settings);
string Version(PlexServerSettings settings);
void Update(int sectionId, string path, 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> GetTvSections(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, string path, PlexServerSettings settings)
{
var resource = $"library/sections/{sectionId}/refresh";
var request = BuildRequest(resource, HttpMethod.Get, settings);
request.AddQueryParam("path", path);
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;
}
private HttpRequestBuilder BuildRequest(string resource, HttpMethod method, PlexServerSettings settings)
{
var scheme = settings.UseSsl ? "https" : "http";
var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host.ToUrlHost()}:{settings.Port}")
.Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier)
.AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.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. Status Code: {0}", ex.Response.StatusCode);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.TrustFailure)
{
throw new PlexException("Unable to connect to Plex Media Server, certificate validation failed.", ex);
}
throw new PlexException($"Unable to connect to Plex Media Server, {ex.Message}", ex);
}
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");
}
}
}

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public interface IPlexServerService
{
void UpdateLibrary(Author author, PlexServerSettings settings);
void UpdateLibrary(IEnumerable<Author> authors, PlexServerSettings settings);
ValidationFailure Test(PlexServerSettings settings);
}
public class PlexServerService : IPlexServerService
{
private readonly ICached<Version> _versionCache;
private readonly IPlexServerProxy _plexServerProxy;
private readonly IRootFolderService _rootFolderService;
private readonly Logger _logger;
public PlexServerService(ICacheManager cacheManager, IPlexServerProxy plexServerProxy, IRootFolderService rootFolderService, Logger logger)
{
_versionCache = cacheManager.GetCache<Version>(GetType(), "versionCache");
_plexServerProxy = plexServerProxy;
_rootFolderService = rootFolderService;
_logger = logger;
}
public void UpdateLibrary(Author author, PlexServerSettings settings)
{
UpdateLibrary(new[] { author }, settings);
}
public void UpdateLibrary(IEnumerable<Author> authors, PlexServerSettings settings)
{
try
{
_logger.Debug("Sending Update Request to Plex Server");
var watch = Stopwatch.StartNew();
var version = _versionCache.Get(settings.Host, () => GetVersion(settings), TimeSpan.FromHours(2));
ValidateVersion(version);
var sections = GetSections(settings);
foreach (var author in authors)
{
UpdateSections(author, sections, settings);
}
_logger.Debug("Finished sending Update Request to Plex Server (took {0} ms)", watch.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to Update Plex host: " + settings.Host);
throw;
}
}
private List<PlexSection> GetSections(PlexServerSettings settings)
{
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
return _plexServerProxy.GetTvSections(settings).ToList();
}
private void ValidateVersion(Version version)
{
if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1))
{
throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Readarr", version);
}
}
private Version GetVersion(PlexServerSettings settings)
{
_logger.Debug("Getting version from Plex host: {0}", settings.Host);
var rawVersion = _plexServerProxy.Version(settings);
var version = new Version(Regex.Match(rawVersion, @"^(\d+[.-]){4}").Value.Trim('.', '-'));
return version;
}
private void UpdateSections(Author author, List<PlexSection> sections, PlexServerSettings settings)
{
var rootFolderPath = _rootFolderService.GetBestRootFolderPath(author.Path);
var authorRelativePath = rootFolderPath.GetRelativePath(author.Path);
// Try to update a matching section location before falling back to updating all section locations.
foreach (var section in sections)
{
foreach (var location in section.Locations)
{
var rootFolder = new OsPath(rootFolderPath);
var mappedPath = rootFolder;
if (settings.MapTo.IsNotNullOrWhiteSpace())
{
mappedPath = new OsPath(settings.MapTo) + (rootFolder - new OsPath(settings.MapFrom));
_logger.Trace("Mapping Path from {0} to {1} for partial scan", rootFolder, mappedPath);
}
if (location.Path.PathEquals(mappedPath.FullPath))
{
_logger.Debug("Updating matching section location, {0}", location.Path);
UpdateSectionPath(authorRelativePath, section, location, settings);
return;
}
}
}
_logger.Debug("Unable to find matching section location, updating all Music sections");
foreach (var section in sections)
{
foreach (var location in section.Locations)
{
UpdateSectionPath(authorRelativePath, section, location, settings);
}
}
}
private void UpdateSectionPath(string authorRelativePath, PlexSection section, PlexSectionLocation location, PlexServerSettings settings)
{
var separator = location.Path.Contains('\\') ? "\\" : "/";
var locationRelativePath = authorRelativePath.Replace("\\", separator).Replace("/", separator);
// Plex location paths trim trailing extraneous separator characters, so it doesn't need to be trimmed
var pathToUpdate = $"{location.Path}{separator}{locationRelativePath}";
_logger.Debug("Updating section location, {0}", location.Path);
_plexServerProxy.Update(section.Id, pathToUpdate, settings);
}
public ValidationFailure Test(PlexServerSettings settings)
{
try
{
_versionCache.Remove(settings.Host);
var sections = GetSections(settings);
if (sections.Empty())
{
return new ValidationFailure("Host", "At least one Music library is required");
}
}
catch (PlexAuthenticationException ex)
{
_logger.Error(ex, "Unable to connect to Plex Media Server");
return new ValidationFailure("AuthToken", "Invalid authentication token");
}
catch (PlexException ex)
{
return new NzbDroneValidationFailure("Host", ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to connect to Plex Media Server");
return new NzbDroneValidationFailure("Host", "Unable to connect to Plex Media Server")
{
DetailedDescription = ex.Message
};
}
return null;
}
}
}

@ -0,0 +1,62 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Plex.Server
{
public class PlexServerSettingsValidator : AbstractValidator<PlexServerSettings>
{
public PlexServerSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.MapFrom).NotEmpty().Unless(c => c.MapTo.IsNullOrWhiteSpace());
RuleFor(c => c.MapTo).NotEmpty().Unless(c => c.MapFrom.IsNullOrWhiteSpace());
}
}
public class PlexServerSettings : IProviderConfig
{
private static readonly PlexServerSettingsValidator Validator = new PlexServerSettingsValidator();
public PlexServerSettings()
{
Port = 32400;
UpdateLibrary = true;
SignIn = "startOAuth";
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Plex over HTTPS instead of HTTP")]
public bool UseSsl { get; set; }
[FieldDefinition(3, Label = "Auth Token", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)]
public string AuthToken { get; set; }
[FieldDefinition(4, Label = "Authenticate with Plex.tv", Type = FieldType.OAuth)]
public string SignIn { get; set; }
[FieldDefinition(5, Label = "Update Library", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
[FieldDefinition(6, Label = "Map Paths From", Type = FieldType.Textbox, Advanced = true, HelpText = "Readarr path, used to modify author paths when Plex sees library path location differently from Readarr")]
public string MapFrom { get; set; }
[FieldDefinition(7, Label = "Map Paths To", Type = FieldType.Textbox, Advanced = true, HelpText = "Plex path, used to modify author paths when Plex sees library path location differently from Readarr")]
public string MapTo { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Host);
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
Loading…
Cancel
Save