parent
897f3fea99
commit
07be9cf47a
@ -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,79 @@
|
|||||||
|
using System.Net;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||||
|
{
|
||||||
|
public interface IPlexTvProxy
|
||||||
|
{
|
||||||
|
string GetAuthToken(string clientIdentifier, int pinId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlexTvProxy : IPlexTvProxy
|
||||||
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public PlexTvProxy(IHttpClient httpClient, Logger logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetAuthToken(string clientIdentifier, int pinId)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(clientIdentifier);
|
||||||
|
request.ResourceUrl = $"/api/v2/pins/{pinId}";
|
||||||
|
|
||||||
|
PlexTvPinResponse response;
|
||||||
|
|
||||||
|
if (!Json.TryDeserialize<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", "Sonarr")
|
||||||
|
.AddQueryParam("X-Plex-Platform", "Windows")
|
||||||
|
.AddQueryParam("X-Plex-Platform-Version", "7")
|
||||||
|
.AddQueryParam("X-Plex-Device-Name", "Sonarr")
|
||||||
|
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
|
||||||
|
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ProcessRequest(HttpRequestBuilder requestBuilder)
|
||||||
|
{
|
||||||
|
var httpRequest = requestBuilder.Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
|
||||||
|
_logger.Debug("Url: {0}", httpRequest.Url);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(httpRequest);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
throw new NzbDroneClientException(ex.Response.StatusCode, "Unable to connect to plex.tv");
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||||
|
{
|
||||||
|
public interface IPlexTvService
|
||||||
|
{
|
||||||
|
PlexTvPinUrlResponse GetPinUrl();
|
||||||
|
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode);
|
||||||
|
string GetAuthToken(int pinId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlexTvService : IPlexTvService
|
||||||
|
{
|
||||||
|
private readonly IPlexTvProxy _proxy;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
|
||||||
|
public PlexTvService(IPlexTvProxy proxy, IConfigService configService)
|
||||||
|
{
|
||||||
|
_proxy = proxy;
|
||||||
|
_configService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlexTvPinUrlResponse GetPinUrl()
|
||||||
|
{
|
||||||
|
var clientIdentifier = _configService.PlexClientIdentifier;
|
||||||
|
|
||||||
|
var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins")
|
||||||
|
.Accept(HttpAccept.Json)
|
||||||
|
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
|
||||||
|
.AddQueryParam("X-Plex-Product", "Sonarr")
|
||||||
|
.AddQueryParam("X-Plex-Platform", "Windows")
|
||||||
|
.AddQueryParam("X-Plex-Platform-Version", "7")
|
||||||
|
.AddQueryParam("X-Plex-Device-Name", "Sonarr")
|
||||||
|
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString())
|
||||||
|
.AddQueryParam("strong", true);
|
||||||
|
|
||||||
|
requestBuilder.Method = HttpMethod.POST;
|
||||||
|
|
||||||
|
var request = requestBuilder.Build();
|
||||||
|
|
||||||
|
return new PlexTvPinUrlResponse
|
||||||
|
{
|
||||||
|
Url = request.Url.ToString(),
|
||||||
|
Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode)
|
||||||
|
{
|
||||||
|
var clientIdentifier = _configService.PlexClientIdentifier;
|
||||||
|
|
||||||
|
var requestBuilder = new HttpRequestBuilder("https://app.plex.tv/auth/hashBang")
|
||||||
|
.AddQueryParam("clientID", clientIdentifier)
|
||||||
|
.AddQueryParam("forwardUrl", callbackUrl)
|
||||||
|
.AddQueryParam("code", pinCode)
|
||||||
|
.AddQueryParam("context[device][product]", "Sonarr")
|
||||||
|
.AddQueryParam("context[device][platform]", "Windows")
|
||||||
|
.AddQueryParam("context[device][platformVersion]", "7")
|
||||||
|
.AddQueryParam("context[device][version]", BuildInfo.Version.ToString());
|
||||||
|
|
||||||
|
// #! is stripped out of the URL when building, this works around it.
|
||||||
|
requestBuilder.Segments.Add("hashBang", "#!");
|
||||||
|
|
||||||
|
var request = requestBuilder.Build();
|
||||||
|
|
||||||
|
return new PlexTvSignInUrlResponse
|
||||||
|
{
|
||||||
|
OauthUrl = request.Url.ToString(),
|
||||||
|
PinId = pinId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetAuthToken(int pinId)
|
||||||
|
{
|
||||||
|
var authToken = _proxy.GetAuthToken(_configService.PlexClientIdentifier, pinId);
|
||||||
|
|
||||||
|
return authToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
|
using NzbDrone.Core.Notifications.Plex.PlexTv;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.Plex.Server
|
||||||
|
{
|
||||||
|
public class PlexServer : NotificationBase<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(DownloadMessage message)
|
||||||
|
{
|
||||||
|
UpdateIfEnabled(message.Series);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnRename(Series series)
|
||||||
|
{
|
||||||
|
UpdateIfEnabled(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateIfEnabled(Series series)
|
||||||
|
{
|
||||||
|
if (Settings.UpdateLibrary)
|
||||||
|
{
|
||||||
|
_plexServerService.UpdateLibrary(series, Settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValidationResult Test()
|
||||||
|
{
|
||||||
|
var failures = new List<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,226 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Notifications.Plex.Server
|
||||||
|
{
|
||||||
|
public interface IPlexServerProxy
|
||||||
|
{
|
||||||
|
List<PlexSection> GetTvSections(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, int tvdbId, string language, PlexServerSettings settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlexServerProxy : IPlexServerProxy
|
||||||
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public PlexServerProxy(IHttpClient httpClient, IConfigService configService,Logger logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_configService = configService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<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 == "show")
|
||||||
|
.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 == "show")
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(int sectionId, PlexServerSettings settings)
|
||||||
|
{
|
||||||
|
var resource = $"library/sections/{sectionId}/refresh";
|
||||||
|
var request = BuildRequest(resource, HttpMethod.GET, settings);
|
||||||
|
var response = ProcessRequest(request);
|
||||||
|
|
||||||
|
CheckForError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSeries(int metadataId, PlexServerSettings settings)
|
||||||
|
{
|
||||||
|
var resource = $"library/metadata/{metadataId}/refresh";
|
||||||
|
var request = BuildRequest(resource, HttpMethod.PUT, settings);
|
||||||
|
var response = ProcessRequest(request);
|
||||||
|
|
||||||
|
CheckForError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Version(PlexServerSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest("identity", HttpMethod.GET, settings);
|
||||||
|
var response = ProcessRequest(request);
|
||||||
|
|
||||||
|
CheckForError(response);
|
||||||
|
|
||||||
|
if (response.Contains("_children"))
|
||||||
|
{
|
||||||
|
return Json.Deserialize<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, int tvdbId, string language, PlexServerSettings settings)
|
||||||
|
{
|
||||||
|
var guid = $"com.plexapp.agents.thetvdb://{tvdbId}?lang={language}";
|
||||||
|
var resource = $"library/sections/{sectionId}/all?guid={System.Web.HttpUtility.UrlEncode(guid)}";
|
||||||
|
var request = BuildRequest(resource, HttpMethod.GET, settings);
|
||||||
|
var response = ProcessRequest(request);
|
||||||
|
|
||||||
|
CheckForError(response);
|
||||||
|
|
||||||
|
List<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", "Sonarr")
|
||||||
|
.AddQueryParam("X-Plex-Platform", "Windows")
|
||||||
|
.AddQueryParam("X-Plex-Platform-Version", "7")
|
||||||
|
.AddQueryParam("X-Plex-Device-Name", "Sonarr")
|
||||||
|
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
|
||||||
|
|
||||||
|
if (settings.AuthToken.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
requestBuilder.AddQueryParam("X-Plex-Token", settings.AuthToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBuilder.ResourceUrl = resource;
|
||||||
|
requestBuilder.Method = method;
|
||||||
|
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ProcessRequest(HttpRequestBuilder requestBuilder)
|
||||||
|
{
|
||||||
|
var httpRequest = requestBuilder.Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
|
||||||
|
_logger.Debug("Url: {0}", httpRequest.Url);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(httpRequest);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new PlexAuthenticationException("Unauthorized - AuthToken is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PlexException("Unable to connect to Plex Media Server");
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
throw new PlexException("Unable to connect to Plex Media Server");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckForError(string response)
|
||||||
|
{
|
||||||
|
_logger.Trace("Checking for error");
|
||||||
|
|
||||||
|
if (response.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
_logger.Trace("No response body returned, no error detected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = response.Contains("_children") ?
|
||||||
|
Json.Deserialize<PlexError>(response) :
|
||||||
|
Json.Deserialize<PlexResponse<PlexError>>(response).MediaContainer;
|
||||||
|
|
||||||
|
if (error != null && !error.Error.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
throw new PlexException(error.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Trace("No error detected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue