From b8fd9c785e107b6d2ae8125d6e6b6374f36fe9a3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:42:48 -0600 Subject: [PATCH 01/13] Convert StartupController to IActionResult --- Jellyfin.Api/Controllers/StartupController.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index afc9b8f3da..b0b26c1762 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Models.StartupDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -32,12 +33,15 @@ namespace Jellyfin.Api.Controllers /// /// Api endpoint for completing the startup wizard. /// + /// Status. [HttpPost("Complete")] - public void CompleteWizard() + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); _config.SaveConfiguration(); + return Ok(); } /// @@ -45,7 +49,8 @@ namespace Jellyfin.Api.Controllers /// /// The initial startup wizard configuration. [HttpGet("Configuration")] - public StartupConfigurationDto GetStartupConfiguration() + [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)] + public IActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -54,7 +59,7 @@ namespace Jellyfin.Api.Controllers PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - return result; + return Ok(result); } /// @@ -63,8 +68,10 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. + /// Status. [HttpPost("Configuration")] - public void UpdateInitialConfiguration( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -73,6 +80,7 @@ namespace Jellyfin.Api.Controllers _config.Configuration.MetadataCountryCode = metadataCountryCode; _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; _config.SaveConfiguration(); + return Ok(); } /// @@ -80,12 +88,15 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. + /// Status. [HttpPost("RemoteAccess")] - public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; _config.SaveConfiguration(); + return Ok(); } /// @@ -93,14 +104,11 @@ namespace Jellyfin.Api.Controllers /// /// The first user. [HttpGet("User")] - public StartupUserDto GetFirstUser() + [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)] + public IActionResult GetFirstUser() { var user = _userManager.Users.First(); - return new StartupUserDto - { - Name = user.Name, - Password = user.Password - }; + return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); } /// @@ -109,7 +117,8 @@ namespace Jellyfin.Api.Controllers /// The DTO containing username and password. /// The async task. [HttpPost("User")] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); @@ -121,6 +130,8 @@ namespace Jellyfin.Api.Controllers { await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); } + + return Ok(); } } } From 7db3b035a6d1f7e6f4886c4497b98b7a6af6c679 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:25:03 -0600 Subject: [PATCH 02/13] move to ActionResult --- Jellyfin.Api/Controllers/StartupController.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index b0b26c1762..2db7e32aad 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult CompleteWizard() + public ActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); @@ -49,8 +49,8 @@ namespace Jellyfin.Api.Controllers /// /// The initial startup wizard configuration. [HttpGet("Configuration")] - [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)] - public IActionResult GetStartupConfiguration() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult UpdateInitialConfiguration( + public ActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; @@ -104,8 +104,8 @@ namespace Jellyfin.Api.Controllers /// /// The first user. [HttpGet("User")] - [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)] - public IActionResult GetFirstUser() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetFirstUser() { var user = _userManager.Users.First(); return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); @@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// The async task. [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); From a06d271725f6e746d9a970f29283ab8f3ebae607 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 22 Apr 2020 13:07:21 -0600 Subject: [PATCH 03/13] Move ConfigurationService to Jellyfin.Api --- .../Controllers/ConfigurationController.cs | 128 +++++++++++++++ .../ConfigurationDtos/MediaEncoderPathDto.cs | 18 +++ MediaBrowser.Api/ConfigurationService.cs | 146 ------------------ 3 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ConfigurationController.cs create mode 100644 Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs delete mode 100644 MediaBrowser.Api/ConfigurationService.cs diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs new file mode 100644 index 0000000000..14e45833f0 --- /dev/null +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -0,0 +1,128 @@ +#nullable enable + +using System.Threading.Tasks; +using Jellyfin.Api.Models.ConfigurationDtos; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Configuration Controller. + /// + [Route("System")] + [Authenticated] + public class ConfigurationController : BaseJellyfinApiController + { + private readonly IServerConfigurationManager _configurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ConfigurationController( + IServerConfigurationManager configurationManager, + IMediaEncoder mediaEncoder, + IJsonSerializer jsonSerializer) + { + _configurationManager = configurationManager; + _mediaEncoder = mediaEncoder; + _jsonSerializer = jsonSerializer; + } + + /// + /// Gets application configuration. + /// + /// Application configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetConfiguration() + { + return Ok(_configurationManager.Configuration); + } + + /// + /// Updates application configuration. + /// + /// Configuration. + /// Status. + [HttpPost("Configuration")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) + { + _configurationManager.ReplaceConfiguration(configuration); + return Ok(); + } + + /// + /// Gets a named configuration. + /// + /// Configuration key. + /// Configuration. + [HttpGet("Configuration/{Key}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNamedConfiguration([FromRoute] string key) + { + return Ok(_configurationManager.GetConfiguration(key)); + } + + /// + /// Updates named configuration. + /// + /// Configuration key. + /// Status. + [HttpPost("Configuration/{Key}")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateNamedConfiguration([FromRoute] string key) + { + var configurationType = _configurationManager.GetConfigurationType(key); + /* + // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed. + var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); + */ + + var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType) + .ConfigureAwait(false); + _configurationManager.SaveConfiguration(key, configuration); + return Ok(); + } + + /// + /// Gets a default MetadataOptions object. + /// + /// MetadataOptions. + [HttpGet("Configuration/MetadataOptions/Default")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultMetadataOptions() + { + return Ok(new MetadataOptions()); + } + + /// + /// Updates the path to the media encoder. + /// + /// Media encoder path form body. + /// Status. + [HttpPost("MediaEncoder/Path")] + [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) + { + _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + return Ok(); + } + } +} diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs new file mode 100644 index 0000000000..b05e0cdf5a --- /dev/null +++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.ConfigurationDtos +{ + /// + /// Media Encoder Path Dto. + /// + public class MediaEncoderPathDto + { + /// + /// Gets or sets media encoder path. + /// + public string Path { get; set; } + + /// + /// Gets or sets media encoder path type. + /// + public string PathType { get; set; } + } +} diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs deleted file mode 100644 index 316be04a03..0000000000 --- a/MediaBrowser.Api/ConfigurationService.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetConfiguration - /// - [Route("/System/Configuration", "GET", Summary = "Gets application configuration")] - [Authenticated] - public class GetConfiguration : IReturn - { - - } - - [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")] - [Authenticated(AllowBeforeStartupWizard = true)] - public class GetNamedConfiguration - { - [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Key { get; set; } - } - - /// - /// Class UpdateConfiguration - /// - [Route("/System/Configuration", "POST", Summary = "Updates application configuration")] - [Authenticated(Roles = "Admin")] - public class UpdateConfiguration : ServerConfiguration, IReturnVoid - { - } - - [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")] - [Authenticated(Roles = "Admin")] - public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream - { - [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Key { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")] - [Authenticated(Roles = "Admin")] - public class GetDefaultMetadataOptions : IReturn - { - - } - - [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")] - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] - public class UpdateMediaEncoderPath : IReturnVoid - { - [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Path { get; set; } - [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string PathType { get; set; } - } - - public class ConfigurationService : BaseApiService - { - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _configuration manager - /// - private readonly IServerConfigurationManager _configurationManager; - - private readonly IMediaEncoder _mediaEncoder; - - public ConfigurationService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _jsonSerializer = jsonSerializer; - _configurationManager = configurationManager; - _mediaEncoder = mediaEncoder; - } - - public void Post(UpdateMediaEncoderPath request) - { - _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetConfiguration request) - { - return ToOptimizedResult(_configurationManager.Configuration); - } - - public object Get(GetNamedConfiguration request) - { - var result = _configurationManager.GetConfiguration(request.Key); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified configuraiton. - /// - /// The request. - public void Post(UpdateConfiguration request) - { - // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration - var json = _jsonSerializer.SerializeToString(request); - - var config = _jsonSerializer.DeserializeFromString(json); - - _configurationManager.ReplaceConfiguration(config); - } - - public async Task Post(UpdateNamedConfiguration request) - { - var key = GetPathValue(2).ToString(); - - var configurationType = _configurationManager.GetConfigurationType(key); - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, configurationType).ConfigureAwait(false); - - _configurationManager.SaveConfiguration(key, configuration); - } - - public object Get(GetDefaultMetadataOptions request) - { - return ToOptimizedResult(new MetadataOptions()); - } - } -} From c7c2f9da90a7cf7a452de0ab1adf7e36f422bbe1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:51:04 -0600 Subject: [PATCH 04/13] Apply suggestions --- Jellyfin.Api/Controllers/StartupController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 2db7e32aad..14c59593fb 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - return Ok(result); + return result; } /// @@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetFirstUser() { var user = _userManager.Users.First(); - return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); + return new StartupUserDto { Name = user.Name, Password = user.Password }; } /// From bb8e738a0817be2e13a8b21929d0f0aeb0c6a461 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:03:54 -0600 Subject: [PATCH 05/13] Fix Authorize attributes --- .../Controllers/ConfigurationController.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 14e45833f0..b508ac0547 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,13 @@ #nullable enable using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -17,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// Configuration Controller. /// [Route("System")] - [Authenticated] + [Authorize] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; @@ -48,7 +49,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetConfiguration() { - return Ok(_configurationManager.Configuration); + return _configurationManager.Configuration; } /// @@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers /// Configuration. /// Status. [HttpPost("Configuration")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) { @@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNamedConfiguration([FromRoute] string key) { - return Ok(_configurationManager.GetConfiguration(key)); + return _configurationManager.GetConfiguration(key); } /// @@ -83,7 +84,7 @@ namespace Jellyfin.Api.Controllers /// Configuration key. /// Status. [HttpPost("Configuration/{Key}")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateNamedConfiguration([FromRoute] string key) { @@ -104,11 +105,11 @@ namespace Jellyfin.Api.Controllers /// /// MetadataOptions. [HttpGet("Configuration/MetadataOptions/Default")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultMetadataOptions() { - return Ok(new MetadataOptions()); + return new MetadataOptions(); } /// @@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// Media encoder path form body. /// Status. [HttpPost("MediaEncoder/Path")] - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) { From 97ecffceb7fe655010c1f415fd688b3ee0f9d48d Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 08:59:34 -0600 Subject: [PATCH 06/13] Add response code descriptions --- Jellyfin.Api/Controllers/StartupController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 14c59593fb..d60e46a01b 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -33,6 +33,7 @@ namespace Jellyfin.Api.Controllers /// /// Api endpoint for completing the startup wizard. /// + /// Startup wizard completed. /// Status. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -47,6 +48,7 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting the initial startup wizard configuration. /// + /// Initial startup wizard configuration retrieved. /// The initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -68,6 +70,7 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. + /// Configuration saved. /// Status. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -88,6 +91,7 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. + /// Configuration saved. /// Status. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -102,6 +106,7 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for returning the first user. /// + /// Initial user retrieved. /// The first user. [HttpGet("User")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -115,6 +120,7 @@ namespace Jellyfin.Api.Controllers /// Endpoint for updating the user name and password. /// /// The DTO containing username and password. + /// Updated user name and password. /// The async task. [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] From 7a3925b863a12bea492a93f41cda4eb92dc9c183 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 09:41:12 -0600 Subject: [PATCH 07/13] Fix docs --- Jellyfin.Api/Controllers/StartupController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index d60e46a01b..66e4774aa0 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Api endpoint for completing the startup wizard. + /// Completes the startup wizard. /// /// Startup wizard completed. /// Status. @@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting the initial startup wizard configuration. + /// Gets the initial startup wizard configuration. /// /// Initial startup wizard configuration retrieved. /// The initial startup wizard configuration. @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for updating the initial startup wizard configuration. + /// Sets the initial startup wizard configuration. /// /// The UI language culture. /// The metadata country code. @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for (dis)allowing remote access and UPnP. + /// Sets remote access and UPnP. /// /// Enable remote access. /// Enable UPnP. @@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for returning the first user. + /// Gets the first user. /// /// Initial user retrieved. /// The first user. @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for updating the user name and password. + /// Sets the user name and password. /// /// The DTO containing username and password. /// Updated user name and password. From 37f55b5c217db5343ab196094f67dc84e71d4ef0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 17 May 2020 19:56:02 -0600 Subject: [PATCH 08/13] apply doc suggestions --- Jellyfin.Api/Controllers/StartupController.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 66e4774aa0..ed1dc1ede3 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Api.Controllers /// Completes the startup wizard. /// /// Startup wizard completed. - /// Status. + /// An indicating success. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CompleteWizard() @@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers /// Gets the initial startup wizard configuration. /// /// Initial startup wizard configuration retrieved. - /// The initial startup wizard configuration. + /// An containing the initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetStartupConfiguration() @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// The metadata country code. /// The preferred language for metadata. /// Configuration saved. - /// Status. + /// An indicating success. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateInitialConfiguration( @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// Enable remote access. /// Enable UPnP. /// Configuration saved. - /// Status. + /// An indicating success. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) @@ -121,7 +121,10 @@ namespace Jellyfin.Api.Controllers /// /// The DTO containing username and password. /// Updated user name and password. - /// The async task. + /// + /// A that represents the asynchronous update operation. + /// The task result contains an indicating success. + /// [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) From 6dbbfcbfbef28e8866aa0144170e1edfff1a2bcb Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:29:59 -0600 Subject: [PATCH 09/13] update xml docs --- Jellyfin.Api/Controllers/ConfigurationController.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index b508ac0547..992cb00874 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -44,6 +44,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets application configuration. /// + /// Application configuration returned. /// Application configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -56,7 +57,8 @@ namespace Jellyfin.Api.Controllers /// Updates application configuration. /// /// Configuration. - /// Status. + /// Configuration updated. + /// Update status. [HttpPost("Configuration")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -70,6 +72,7 @@ namespace Jellyfin.Api.Controllers /// Gets a named configuration. /// /// Configuration key. + /// Configuration returned. /// Configuration. [HttpGet("Configuration/{Key}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -82,7 +85,8 @@ namespace Jellyfin.Api.Controllers /// Updates named configuration. /// /// Configuration key. - /// Status. + /// Named configuration updated. + /// Update status. [HttpPost("Configuration/{Key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -103,7 +107,8 @@ namespace Jellyfin.Api.Controllers /// /// Gets a default MetadataOptions object. /// - /// MetadataOptions. + /// Metadata options returned. + /// Default MetadataOptions. [HttpGet("Configuration/MetadataOptions/Default")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -116,6 +121,7 @@ namespace Jellyfin.Api.Controllers /// Updates the path to the media encoder. /// /// Media encoder path form body. + /// Media encoder path updated. /// Status. [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] From 4fe0beec162d4554f1d6cc3c658b672eafbfa307 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 21 May 2020 08:44:15 -0600 Subject: [PATCH 10/13] Fix Json Enum conversion, map all JsonDefaults properties to API --- .../ApiServiceCollectionExtensions.cs | 22 +++++++--- .../CamelCaseJsonProfileFormatter.cs | 4 +- .../PascalCaseJsonProfileFormatter.cs | 4 +- Jellyfin.Server/Models/JsonOptions.cs | 41 ------------------- MediaBrowser.Common/Json/JsonDefaults.cs | 34 ++++++++++++++- 5 files changed, 53 insertions(+), 52 deletions(-) delete mode 100644 Jellyfin.Server/Models/JsonOptions.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 344ef6a5ff..b9f55e2008 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; -using System.Text.Json.Serialization; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; @@ -11,6 +8,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -83,8 +81,20 @@ namespace Jellyfin.Server.Extensions .AddApplicationPart(typeof(StartupController).Assembly) .AddJsonOptions(options => { - // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. - options.JsonSerializerOptions.PropertyNamingPolicy = null; + // Update all properties that are set in JsonDefaults + var jsonOptions = JsonDefaults.PascalCase; + + // From JsonDefaults + options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; + options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; + options.JsonSerializerOptions.Converters.Clear(); + foreach (var converter in jsonOptions.Converters) + { + options.JsonSerializerOptions.Converters.Add(converter); + } + + // From JsonDefaults.PascalCase + options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; }) .AddControllersAsServices(); } @@ -98,7 +108,7 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { - c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" }); + c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index e6ad6dfb13..989c8ecea2 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using Jellyfin.Server.Models; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase) + public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase) { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 675f4c79ee..69963b3fb3 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using Jellyfin.Server.Models; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase) + public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs deleted file mode 100644 index 2f0df3d2c7..0000000000 --- a/Jellyfin.Server/Models/JsonOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text.Json; - -namespace Jellyfin.Server.Models -{ - /// - /// Json Options. - /// - public static class JsonOptions - { - /// - /// Gets CamelCase json options. - /// - public static JsonSerializerOptions CamelCase - { - get - { - var options = DefaultJsonOptions; - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - return options; - } - } - - /// - /// Gets PascalCase json options. - /// - public static JsonSerializerOptions PascalCase - { - get - { - var options = DefaultJsonOptions; - options.PropertyNamingPolicy = null; - return options; - } - } - - /// - /// Gets base Json Serializer Options. - /// - private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions(); - } -} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 4a6ee0a793..326f04eea1 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -12,10 +12,16 @@ namespace MediaBrowser.Common.Json /// /// Gets the default options. /// + /// + /// When changing these options, update + /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs + /// -> AddJellyfinApi + /// -> AddJsonOptions + /// /// The default options. public static JsonSerializerOptions GetOptions() { - var options = new JsonSerializerOptions() + var options = new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Disallow, WriteIndented = false @@ -26,5 +32,31 @@ namespace MediaBrowser.Common.Json return options; } + + /// + /// Gets CamelCase json options. + /// + public static JsonSerializerOptions CamelCase + { + get + { + var options = GetOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; + } + } + + /// + /// Gets PascalCase json options. + /// + public static JsonSerializerOptions PascalCase + { + get + { + var options = GetOptions(); + options.PropertyNamingPolicy = null; + return options; + } + } } } From 0e41c4727d84edbf4d7b96c59e0a3d3bec87b633 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 10:36:32 -0600 Subject: [PATCH 11/13] revert to System.Text.JsonSerializer --- .../Controllers/ConfigurationController.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 992cb00874..8243bfce4c 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,12 @@ #nullable enable +using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -23,22 +23,18 @@ namespace Jellyfin.Api.Controllers { private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _jsonSerializer; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. public ConfigurationController( IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder, - IJsonSerializer jsonSerializer) + IMediaEncoder mediaEncoder) { _configurationManager = configurationManager; _mediaEncoder = mediaEncoder; - _jsonSerializer = jsonSerializer; } /// @@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers public async Task UpdateNamedConfiguration([FromRoute] string key) { var configurationType = _configurationManager.GetConfigurationType(key); - /* - // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed. var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); - */ - - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType) - .ConfigureAwait(false); _configurationManager.SaveConfiguration(key, configuration); return Ok(); } From 01a5103fef83bbbef230faf2303d16648981a5d2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 2 Jun 2020 11:47:00 -0600 Subject: [PATCH 12/13] Add Dictionary with non-string keys to System.Text.Json --- .../ApiServiceCollectionExtensions.cs | 26 ++++++ .../JsonNonStringKeyDictionaryConverter.cs | 79 +++++++++++++++++++ ...nNonStringKeyDictionaryConverterFactory.cs | 60 ++++++++++++++ MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 4 files changed, 166 insertions(+) create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b9f55e2008..cb4189587d 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Jellyfin.Api; using Jellyfin.Api.Auth; @@ -9,6 +11,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; using MediaBrowser.Common.Json; +using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -128,7 +131,30 @@ namespace Jellyfin.Server.Extensions // Use method name as operationId c.CustomOperationIds(description => description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); + + // TODO - remove when all types are supported in System.Text.Json + c.AddSwaggerTypeMappings(); }); } + + private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) + { + /* + * TODO remove when System.Text.Json supports non-string keys. + * Used in Jellyfin.Api.Controller.GetChannels. + */ + options.MapType>(() => + new OpenApiSchema + { + Type = "object", + Properties = typeof(ImageType).GetEnumNames().ToDictionary( + name => name, + name => new OpenApiSchema + { + Type = "string", + Format = "string" + }) + }); + } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs new file mode 100644 index 0000000000..f2ddd7fea2 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -0,0 +1,79 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// Converter for Dictionaries without string key. + /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. + /// + /// Type of key. + /// Type of value. + internal sealed class JsonNonStringKeyDictionaryConverter : JsonConverter> + { + /// + /// Read JSON. + /// + /// The Utf8JsonReader. + /// The type to convert. + /// The json serializer options. + /// Typed dictionary. + /// + public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]); + var value = JsonSerializer.Deserialize(ref reader, convertedType, options); + var instance = (Dictionary)Activator.CreateInstance( + typeToConvert, + BindingFlags.Instance | BindingFlags.Public, + null, + null, + CultureInfo.CurrentCulture); + var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null); + var parse = typeof(TKey).GetMethod( + "Parse", + 0, + BindingFlags.Public | BindingFlags.Static, + null, + CallingConventions.Any, + new[] { typeof(string) }, + null); + if (parse == null) + { + throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary is not supported."); + } + + while (enumerator.MoveNext()) + { + var element = (KeyValuePair)enumerator.Current; + instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value); + } + + return instance; + } + + /// + /// Write dictionary as Json. + /// + /// The Utf8JsonWriter. + /// The dictionary value. + /// The Json serializer options. + public override void Write(Utf8JsonWriter writer, IDictionary value, JsonSerializerOptions options) + { + var convertedDictionary = new Dictionary(value.Count); + foreach (var (k, v) in value) + { + convertedDictionary[k?.ToString()] = v; + } + JsonSerializer.Serialize(writer, convertedDictionary, options); + convertedDictionary.Clear(); + } + } +} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs new file mode 100644 index 0000000000..d9795a189a --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs @@ -0,0 +1,60 @@ +#nullable enable + +using System; +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972. + /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. + /// + internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory + { + /// + /// Only convert objects that implement IDictionary and do not have string keys. + /// + /// Type convert. + /// Conversion ability. + public override bool CanConvert(Type typeToConvert) + { + + if (!typeToConvert.IsGenericType) + { + return false; + } + + // Let built in converter handle string keys + if (typeToConvert.GenericTypeArguments[0] == typeof(string)) + { + return false; + } + + // Only support objects that implement IDictionary + return typeToConvert.GetInterface(nameof(IDictionary)) != null; + } + + /// + /// Create converter for generic dictionary type. + /// + /// Type to convert. + /// Json serializer options. + /// JsonConverter for given type. + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>) + .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]); + var converter = (JsonConverter)Activator.CreateInstance( + converterType, + BindingFlags.Instance | BindingFlags.Public, + null, + null, + CultureInfo.CurrentCulture); + return converter; + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 326f04eea1..f38e2893ec 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -29,6 +29,7 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); return options; } From 8b59934ccb53fda0ccfda2e914192ac3d1d11ad7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 3 Jun 2020 09:12:12 -0600 Subject: [PATCH 13/13] remove extra Clear call --- .../Json/Converters/JsonNonStringKeyDictionaryConverter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs index f2ddd7fea2..636ef5372f 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -73,7 +73,6 @@ namespace MediaBrowser.Common.Json.Converters convertedDictionary[k?.ToString()] = v; } JsonSerializer.Serialize(writer, convertedDictionary, options); - convertedDictionary.Clear(); } } }