New: Server Side UI Filtering, Error Boundaries (#501)

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
Qstick 2018-09-22 23:10:50 -04:00 committed by GitHub
commit 64a8d02f77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 1564 additions and 431 deletions

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using NzbDrone.Core.CustomFilters;
using Lidarr.Http;
namespace Lidarr.Api.V1.CustomFilters
{
public class CustomFilterModule : LidarrRestModule<CustomFilterResource>
{
private readonly ICustomFilterService _customFilterService;
public CustomFilterModule(ICustomFilterService customFilterService)
{
_customFilterService = customFilterService;
GetResourceById = GetCustomFilter;
GetResourceAll = GetCustomFilters;
CreateResource = AddCustomFilter;
UpdateResource = UpdateCustomFilter;
DeleteResource = DeleteCustomResource;
}
private CustomFilterResource GetCustomFilter(int id)
{
return _customFilterService.Get(id).ToResource();
}
private List<CustomFilterResource> GetCustomFilters()
{
return _customFilterService.All().ToResource();
}
private int AddCustomFilter(CustomFilterResource resource)
{
var customFilter = _customFilterService.Add(resource.ToModel());
return customFilter.Id;
}
private void UpdateCustomFilter(CustomFilterResource resource)
{
_customFilterService.Update(resource.ToModel());
}
private void DeleteCustomResource(int id)
{
_customFilterService.Delete(id);
}
}
}

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.CustomFilters;
using Lidarr.Http.REST;
namespace Lidarr.Api.V1.CustomFilters
{
public class CustomFilterResource : RestResource
{
public string Type { get; set; }
public string Label { get; set; }
public List<dynamic> Filters { get; set; }
}
public static class CustomFilterResourceMapper
{
public static CustomFilterResource ToResource(this CustomFilter model)
{
if (model == null) return null;
return new CustomFilterResource
{
Id = model.Id,
Type = model.Type,
Label = model.Label,
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
};
}
public static CustomFilter ToModel(this CustomFilterResource resource)
{
if (resource == null) return null;
return new CustomFilter
{
Id = resource.Id,
Type = resource.Type,
Label = resource.Label,
Filters = Json.ToJson(resource.Filters)
};
}
public static List<CustomFilterResource> ToResource(this IEnumerable<CustomFilter> filters)
{
return filters.Select(ToResource).ToList();
}
}
}

View file

@ -97,6 +97,8 @@
<Compile Include="Commands\CommandResource.cs" />
<Compile Include="Config\MetadataProviderConfigModule.cs" />
<Compile Include="Config\MetadataProviderConfigResource.cs" />
<Compile Include="CustomFilters\CustomFilterModule.cs" />
<Compile Include="CustomFilters\CustomFilterResource.cs" />
<Compile Include="ImportLists\ImportListModule.cs" />
<Compile Include="ImportLists\ImportListResource.cs" />
<Compile Include="Profiles\Metadata\MetadataProfileModule.cs" />

View file

@ -54,8 +54,8 @@
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SharpRaven, Version=2.2.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\SharpRaven.2.2.0\lib\net45\SharpRaven.dll</HintPath>
<Reference Include="SharpRaven, Version=2.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\SharpRaven.2.4.0\lib\net45\SharpRaven.dll</HintPath>
</Reference>
<Reference Include="SocksWebProxy, Version=1.3.4.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll</HintPath>
@ -66,6 +66,7 @@
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.ServiceProcess" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Xml" />

View file

@ -4,5 +4,5 @@
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net461" />
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" />
<package id="NLog" version="4.5.4" targetFramework="net461" />
<package id="SharpRaven" version="2.2.0" targetFramework="net461" />
<package id="SharpRaven" version="2.4.0" targetFramework="net461" />
</packages>

View file

@ -0,0 +1,11 @@
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.CustomFilters
{
public class CustomFilter : ModelBase
{
public string Type { get; set; }
public string Label { get; set; }
public string Filters { get; set; }
}
}

View file

@ -0,0 +1,17 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.CustomFilters
{
public interface ICustomFilterRepository : IBasicRepository<CustomFilter>
{
}
public class CustomFilterRepository : BasicRepository<CustomFilter>, ICustomFilterRepository
{
public CustomFilterRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.CustomFilters
{
public interface ICustomFilterService
{
CustomFilter Add(CustomFilter customFilter);
List<CustomFilter> All();
void Delete(int id);
CustomFilter Get(int id);
CustomFilter Update(CustomFilter customFilter);
}
public class CustomFilterService : ICustomFilterService
{
private readonly ICustomFilterRepository _repo;
public CustomFilterService(ICustomFilterRepository repo)
{
_repo = repo;
}
public CustomFilter Add(CustomFilter customFilter)
{
return _repo.Insert(customFilter);
}
public CustomFilter Update(CustomFilter customFilter)
{
return _repo.Update(customFilter);
}
public void Delete(int id)
{
_repo.Delete(id);
}
public CustomFilter Get(int id)
{
return _repo.Get(id);
}
public List<CustomFilter> All()
{
return _repo.All().ToList();
}
}
}

View file

@ -0,0 +1,17 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(021)]
public class add_custom_filters : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("CustomFilters")
.WithColumn("Type").AsString().NotNullable()
.WithColumn("Label").AsString().NotNullable()
.WithColumn("Filters").AsString().NotNullable();
}
}
}

View file

@ -31,6 +31,7 @@ using NzbDrone.Core.ThingiProvider;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.CustomFilters;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
@ -141,6 +142,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<IndexerStatus>().RegisterModel("IndexerStatus");
Mapper.Entity<DownloadClientStatus>().RegisterModel("DownloadClientStatus");
Mapper.Entity<ImportListStatus>().RegisterModel("ImportListStatus");
Mapper.Entity<CustomFilter>().RegisterModel("CustomFilters");
}
private static void RegisterMappers()

View file

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

View file

@ -4,46 +4,39 @@ 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")
@ -54,13 +47,18 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
.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);
@ -73,6 +71,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
{
throw new NzbDroneClientException(HttpStatusCode.BadRequest, "Unable to connect to plex.tv");
}
return response.Content;
}
}

View file

@ -1,39 +1,76 @@
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
{
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl);
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 PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl)
public PlexTvPinUrlResponse GetPinUrl()
{
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");
var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins")
.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())
.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]", "Lidarr")
.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 = url.ToString(),
PinId = pin.Id
OauthUrl = request.Url.ToString(),
PinId = pinId
};
}
public string GetAuthToken(int pinId)

View file

@ -6,31 +6,38 @@ 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)
@ -38,36 +45,62 @@ namespace NzbDrone.Core.Notifications.Plex.Server
_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();
return _plexTvService.GetPinUrl();
}
else if (action == "continueOAuth")
{
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
if (query["callbackUrl"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam callbackUrl invalid.");
}
return _plexTvService.GetSignInUrl(query["callbackUrl"]);
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 { };
}
}

View file

@ -7,6 +7,7 @@ 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
@ -18,22 +19,27 @@ namespace NzbDrone.Core.Notifications.Plex.Server
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)
@ -48,36 +54,45 @@ namespace NzbDrone.Core.Notifications.Plex.Server
})
.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;
@ -86,24 +101,31 @@ namespace NzbDrone.Core.Notifications.Plex.Server
{
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)
@ -115,12 +137,15 @@ namespace NzbDrone.Core.Notifications.Plex.Server
.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";
@ -132,19 +157,26 @@ namespace NzbDrone.Core.Notifications.Plex.Server
.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);
@ -161,23 +193,29 @@ namespace NzbDrone.Core.Notifications.Plex.Server
{
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");
}
}

View file

@ -143,6 +143,9 @@
<Compile Include="Configuration\IConfigService.cs" />
<Compile Include="Configuration\InvalidConfigFileException.cs" />
<Compile Include="Configuration\ResetApiKeyCommand.cs" />
<Compile Include="CustomFilters\CustomFilter.cs" />
<Compile Include="CustomFilters\CustomFilterRepository.cs" />
<Compile Include="CustomFilters\CustomFilterService.cs" />
<Compile Include="Datastore\BasicRepository.cs" />
<Compile Include="Datastore\ConnectionStringFactory.cs" />
<Compile Include="Datastore\Converters\BooleanIntConverter.cs" />
@ -192,6 +195,7 @@
<Compile Include="Datastore\Migration\017_remove_nma.cs" />
<Compile Include="Datastore\Migration\018_album_disambiguation.cs" />
<Compile Include="Datastore\Migration\019_add_ape_quality_in_profiles.cs" />
<Compile Include="Datastore\Migration\021_add_custom_filters.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
@ -891,6 +895,7 @@
<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\PlexTv\PlexTvPinUrlResponse.cs" />
<Compile Include="Notifications\Plex\Server\PlexIdentity.cs" />
<Compile Include="Notifications\Plex\Server\PlexResponse.cs" />
<Compile Include="Notifications\Plex\Server\PlexPreferences.cs" />