diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs new file mode 100644 index 0000000000..8243bfce4c --- /dev/null +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -0,0 +1,125 @@ +#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 Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Configuration Controller. + /// + [Route("System")] + [Authorize] + public class ConfigurationController : BaseJellyfinApiController + { + private readonly IServerConfigurationManager _configurationManager; + private readonly IMediaEncoder _mediaEncoder; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public ConfigurationController( + IServerConfigurationManager configurationManager, + IMediaEncoder mediaEncoder) + { + _configurationManager = configurationManager; + _mediaEncoder = mediaEncoder; + } + + /// + /// Gets application configuration. + /// + /// Application configuration returned. + /// Application configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetConfiguration() + { + return _configurationManager.Configuration; + } + + /// + /// Updates application configuration. + /// + /// Configuration. + /// Configuration updated. + /// Update status. + [HttpPost("Configuration")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) + { + _configurationManager.ReplaceConfiguration(configuration); + return Ok(); + } + + /// + /// Gets a named configuration. + /// + /// Configuration key. + /// Configuration returned. + /// Configuration. + [HttpGet("Configuration/{Key}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNamedConfiguration([FromRoute] string key) + { + return _configurationManager.GetConfiguration(key); + } + + /// + /// Updates named configuration. + /// + /// Configuration key. + /// Named configuration updated. + /// Update status. + [HttpPost("Configuration/{Key}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateNamedConfiguration([FromRoute] string key) + { + var configurationType = _configurationManager.GetConfigurationType(key); + var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); + _configurationManager.SaveConfiguration(key, configuration); + return Ok(); + } + + /// + /// Gets a default MetadataOptions object. + /// + /// Metadata options returned. + /// Default MetadataOptions. + [HttpGet("Configuration/MetadataOptions/Default")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultMetadataOptions() + { + return new MetadataOptions(); + } + + /// + /// Updates the path to the media encoder. + /// + /// Media encoder path form body. + /// Media encoder path updated. + /// Status. + [HttpPost("MediaEncoder/Path")] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) + { + _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + return Ok(); + } + } +} diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index afc9b8f3da..ed1dc1ede3 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 @@ -30,22 +31,28 @@ namespace Jellyfin.Api.Controllers } /// - /// Api endpoint for completing the startup wizard. + /// Completes the startup wizard. /// + /// Startup wizard completed. + /// An indicating success. [HttpPost("Complete")] - public void CompleteWizard() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); _config.SaveConfiguration(); + return Ok(); } /// - /// Endpoint for getting the initial startup wizard configuration. + /// Gets the initial startup wizard configuration. /// - /// The initial startup wizard configuration. + /// Initial startup wizard configuration retrieved. + /// An containing the initial startup wizard configuration. [HttpGet("Configuration")] - public StartupConfigurationDto GetStartupConfiguration() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -58,13 +65,16 @@ 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. /// The preferred language for metadata. + /// Configuration saved. + /// An indicating success. [HttpPost("Configuration")] - public void UpdateInitialConfiguration( + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -73,43 +83,51 @@ namespace Jellyfin.Api.Controllers _config.Configuration.MetadataCountryCode = metadataCountryCode; _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; _config.SaveConfiguration(); + return Ok(); } /// - /// Endpoint for (dis)allowing remote access and UPnP. + /// Sets remote access and UPnP. /// /// Enable remote access. /// Enable UPnP. + /// Configuration saved. + /// An indicating success. [HttpPost("RemoteAccess")] - public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; _config.SaveConfiguration(); + return Ok(); } /// - /// Endpoint for returning the first user. + /// Gets the first user. /// + /// Initial user retrieved. /// The first user. [HttpGet("User")] - public StartupUserDto GetFirstUser() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetFirstUser() { var user = _userManager.Users.First(); - return new StartupUserDto - { - Name = user.Name, - Password = user.Password - }; + return new StartupUserDto { Name = user.Name, Password = user.Password }; } /// - /// Endpoint for updating the user name and password. + /// Sets the user name and password. /// /// The DTO containing username and password. - /// The async task. + /// Updated user name and password. + /// + /// A that represents the asynchronous update operation. + /// The task result contains an indicating success. + /// [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 +139,8 @@ namespace Jellyfin.Api.Controllers { await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); } + + 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/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 7bb659ecee..cb4189587d 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; 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; @@ -83,8 +84,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 +111,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( @@ -119,16 +132,17 @@ namespace Jellyfin.Server.Extensions c.CustomOperationIds(description => description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); - // Add types not supported by System.Text.Json - // TODO: Remove this once these types are supported by System.Text.Json and Swashbuckle - // See: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1667 - c.MapSwaggerGenTypes(); + // TODO - remove when all types are supported in System.Text.Json + c.AddSwaggerTypeMappings(); }); } - private static void MapSwaggerGenTypes(this SwaggerGenOptions options) + private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) { - // BaseItemDto.ImageTags + /* + * TODO remove when System.Text.Json supports non-string keys. + * Used in Jellyfin.Api.Controller.GetChannels. + */ options.MapType>(() => new OpenApiSchema { 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.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()); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs new file mode 100644 index 0000000000..636ef5372f --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -0,0 +1,78 @@ +#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); + } + } +} 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 4a6ee0a793..f38e2893ec 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 @@ -23,8 +29,35 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); 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; + } + } } }