parent
75213c86a1
commit
c4850505b0
@ -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,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,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…
Reference in new issue