mirror of https://github.com/jellyfin/jellyfin.git
Merge remote-tracking branch 'remotes/upstream/api-migration' into api-channel
# Conflicts: # Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
This commit is contained in:
commit
88b6c26472
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("System")]
|
||||||
|
[Authorize]
|
||||||
|
public class ConfigurationController : BaseJellyfinApiController
|
||||||
|
{
|
||||||
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
|
public ConfigurationController(
|
||||||
|
IServerConfigurationManager configurationManager,
|
||||||
|
IMediaEncoder mediaEncoder)
|
||||||
|
{
|
||||||
|
_configurationManager = configurationManager;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets application configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">Application configuration returned.</response>
|
||||||
|
/// <returns>Application configuration.</returns>
|
||||||
|
[HttpGet("Configuration")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<ServerConfiguration> GetConfiguration()
|
||||||
|
{
|
||||||
|
return _configurationManager.Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates application configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">Configuration.</param>
|
||||||
|
/// <response code="200">Configuration updated.</response>
|
||||||
|
/// <returns>Update status.</returns>
|
||||||
|
[HttpPost("Configuration")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configurationManager.ReplaceConfiguration(configuration);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a named configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Configuration key.</param>
|
||||||
|
/// <response code="200">Configuration returned.</response>
|
||||||
|
/// <returns>Configuration.</returns>
|
||||||
|
[HttpGet("Configuration/{Key}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
|
||||||
|
{
|
||||||
|
return _configurationManager.GetConfiguration(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates named configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Configuration key.</param>
|
||||||
|
/// <response code="200">Named configuration updated.</response>
|
||||||
|
/// <returns>Update status.</returns>
|
||||||
|
[HttpPost("Configuration/{Key}")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
|
||||||
|
{
|
||||||
|
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||||
|
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType);
|
||||||
|
_configurationManager.SaveConfiguration(key, configuration);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a default MetadataOptions object.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">Metadata options returned.</response>
|
||||||
|
/// <returns>Default MetadataOptions.</returns>
|
||||||
|
[HttpGet("Configuration/MetadataOptions/Default")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
|
||||||
|
{
|
||||||
|
return new MetadataOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the path to the media encoder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
||||||
|
/// <response code="200">Media encoder path updated.</response>
|
||||||
|
/// <returns>Status.</returns>
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using Jellyfin.Api.Models.StartupDtos;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
|
@ -30,22 +31,28 @@ namespace Jellyfin.Api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api endpoint for completing the startup wizard.
|
/// Completes the startup wizard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <response code="200">Startup wizard completed.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> indicating success.</returns>
|
||||||
[HttpPost("Complete")]
|
[HttpPost("Complete")]
|
||||||
public void CompleteWizard()
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult CompleteWizard()
|
||||||
{
|
{
|
||||||
_config.Configuration.IsStartupWizardCompleted = true;
|
_config.Configuration.IsStartupWizardCompleted = true;
|
||||||
_config.SetOptimalValues();
|
_config.SetOptimalValues();
|
||||||
_config.SaveConfiguration();
|
_config.SaveConfiguration();
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Endpoint for getting the initial startup wizard configuration.
|
/// Gets the initial startup wizard configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The initial startup wizard configuration.</returns>
|
/// <response code="200">Initial startup wizard configuration retrieved.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
|
||||||
[HttpGet("Configuration")]
|
[HttpGet("Configuration")]
|
||||||
public StartupConfigurationDto GetStartupConfiguration()
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
|
||||||
{
|
{
|
||||||
var result = new StartupConfigurationDto
|
var result = new StartupConfigurationDto
|
||||||
{
|
{
|
||||||
|
@ -58,13 +65,16 @@ namespace Jellyfin.Api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Endpoint for updating the initial startup wizard configuration.
|
/// Sets the initial startup wizard configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uiCulture">The UI language culture.</param>
|
/// <param name="uiCulture">The UI language culture.</param>
|
||||||
/// <param name="metadataCountryCode">The metadata country code.</param>
|
/// <param name="metadataCountryCode">The metadata country code.</param>
|
||||||
/// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
|
/// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
|
||||||
|
/// <response code="200">Configuration saved.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> indicating success.</returns>
|
||||||
[HttpPost("Configuration")]
|
[HttpPost("Configuration")]
|
||||||
public void UpdateInitialConfiguration(
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult UpdateInitialConfiguration(
|
||||||
[FromForm] string uiCulture,
|
[FromForm] string uiCulture,
|
||||||
[FromForm] string metadataCountryCode,
|
[FromForm] string metadataCountryCode,
|
||||||
[FromForm] string preferredMetadataLanguage)
|
[FromForm] string preferredMetadataLanguage)
|
||||||
|
@ -73,43 +83,51 @@ namespace Jellyfin.Api.Controllers
|
||||||
_config.Configuration.MetadataCountryCode = metadataCountryCode;
|
_config.Configuration.MetadataCountryCode = metadataCountryCode;
|
||||||
_config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
|
_config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
|
||||||
_config.SaveConfiguration();
|
_config.SaveConfiguration();
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Endpoint for (dis)allowing remote access and UPnP.
|
/// Sets remote access and UPnP.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="enableRemoteAccess">Enable remote access.</param>
|
/// <param name="enableRemoteAccess">Enable remote access.</param>
|
||||||
/// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
|
/// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
|
||||||
|
/// <response code="200">Configuration saved.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> indicating success.</returns>
|
||||||
[HttpPost("RemoteAccess")]
|
[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.EnableRemoteAccess = enableRemoteAccess;
|
||||||
_config.Configuration.EnableUPnP = enableAutomaticPortMapping;
|
_config.Configuration.EnableUPnP = enableAutomaticPortMapping;
|
||||||
_config.SaveConfiguration();
|
_config.SaveConfiguration();
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Endpoint for returning the first user.
|
/// Gets the first user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <response code="200">Initial user retrieved.</response>
|
||||||
/// <returns>The first user.</returns>
|
/// <returns>The first user.</returns>
|
||||||
[HttpGet("User")]
|
[HttpGet("User")]
|
||||||
public StartupUserDto GetFirstUser()
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<StartupUserDto> GetFirstUser()
|
||||||
{
|
{
|
||||||
var user = _userManager.Users.First();
|
var user = _userManager.Users.First();
|
||||||
return new StartupUserDto
|
return new StartupUserDto { Name = user.Name, Password = user.Password };
|
||||||
{
|
|
||||||
Name = user.Name,
|
|
||||||
Password = user.Password
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Endpoint for updating the user name and password.
|
/// Sets the user name and password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startupUserDto">The DTO containing username and password.</param>
|
/// <param name="startupUserDto">The DTO containing username and password.</param>
|
||||||
/// <returns>The async task.</returns>
|
/// <response code="200">Updated user name and password.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous update operation.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> indicating success.
|
||||||
|
/// </returns>
|
||||||
[HttpPost("User")]
|
[HttpPost("User")]
|
||||||
public async Task UpdateUser([FromForm] StartupUserDto startupUserDto)
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
|
||||||
{
|
{
|
||||||
var user = _userManager.Users.First();
|
var user = _userManager.Users.First();
|
||||||
|
|
||||||
|
@ -121,6 +139,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
|
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace Jellyfin.Api.Models.ConfigurationDtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Media Encoder Path Dto.
|
||||||
|
/// </summary>
|
||||||
|
public class MediaEncoderPathDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets media encoder path.
|
||||||
|
/// </summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets media encoder path type.
|
||||||
|
/// </summary>
|
||||||
|
public string PathType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Controllers;
|
using Jellyfin.Api.Controllers;
|
||||||
using Jellyfin.Server.Formatters;
|
using Jellyfin.Server.Formatters;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -83,8 +84,20 @@ namespace Jellyfin.Server.Extensions
|
||||||
.AddApplicationPart(typeof(StartupController).Assembly)
|
.AddApplicationPart(typeof(StartupController).Assembly)
|
||||||
.AddJsonOptions(options =>
|
.AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
// Setting the naming policy to null leaves the property names as-is when serializing objects to JSON.
|
// Update all properties that are set in JsonDefaults
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = null;
|
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();
|
.AddControllersAsServices();
|
||||||
}
|
}
|
||||||
|
@ -98,7 +111,7 @@ namespace Jellyfin.Server.Extensions
|
||||||
{
|
{
|
||||||
return serviceCollection.AddSwaggerGen(c =>
|
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.
|
// Add all xml doc files to swagger generator.
|
||||||
var xmlFiles = Directory.GetFiles(
|
var xmlFiles = Directory.GetFiles(
|
||||||
|
@ -119,16 +132,17 @@ namespace Jellyfin.Server.Extensions
|
||||||
c.CustomOperationIds(description =>
|
c.CustomOperationIds(description =>
|
||||||
description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
|
description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
|
||||||
|
|
||||||
// Add types not supported by System.Text.Json
|
// TODO - remove when all types are supported in System.Text.Json
|
||||||
// TODO: Remove this once these types are supported by System.Text.Json and Swashbuckle
|
c.AddSwaggerTypeMappings();
|
||||||
// See: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1667
|
|
||||||
c.MapSwaggerGenTypes();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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<Dictionary<ImageType, string>>(() =>
|
options.MapType<Dictionary<ImageType, string>>(() =>
|
||||||
new OpenApiSchema
|
new OpenApiSchema
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Jellyfin.Server.Models;
|
using MediaBrowser.Common.Json;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
|
/// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase)
|
public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase)
|
||||||
{
|
{
|
||||||
SupportedMediaTypes.Clear();
|
SupportedMediaTypes.Clear();
|
||||||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
|
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\""));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Jellyfin.Server.Models;
|
using MediaBrowser.Common.Json;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
|
/// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase)
|
public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase)
|
||||||
{
|
{
|
||||||
SupportedMediaTypes.Clear();
|
SupportedMediaTypes.Clear();
|
||||||
// Add application/json for default formatter
|
// Add application/json for default formatter
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Models
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json Options.
|
|
||||||
/// </summary>
|
|
||||||
public static class JsonOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets CamelCase json options.
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerOptions CamelCase
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var options = DefaultJsonOptions;
|
|
||||||
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets PascalCase json options.
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerOptions PascalCase
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var options = DefaultJsonOptions;
|
|
||||||
options.PropertyNamingPolicy = null;
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets base Json Serializer Options.
|
|
||||||
/// </summary>
|
|
||||||
private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class GetConfiguration
|
|
||||||
/// </summary>
|
|
||||||
[Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
|
|
||||||
[Authenticated]
|
|
||||||
public class GetConfiguration : IReturn<ServerConfiguration>
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class UpdateConfiguration
|
|
||||||
/// </summary>
|
|
||||||
[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<MetadataOptions>
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The _json serializer
|
|
||||||
/// </summary>
|
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _configuration manager
|
|
||||||
/// </summary>
|
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
|
||||||
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
|
||||||
|
|
||||||
public ConfigurationService(
|
|
||||||
ILogger<ConfigurationService> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public object Get(GetConfiguration request)
|
|
||||||
{
|
|
||||||
return ToOptimizedResult(_configurationManager.Configuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Get(GetNamedConfiguration request)
|
|
||||||
{
|
|
||||||
var result = _configurationManager.GetConfiguration(request.Key);
|
|
||||||
|
|
||||||
return ToOptimizedResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Posts the specified configuraiton.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
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<ServerConfiguration>(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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for Dictionaries without string key.
|
||||||
|
/// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">Type of key.</typeparam>
|
||||||
|
/// <typeparam name="TValue">Type of value.</typeparam>
|
||||||
|
internal sealed class JsonNonStringKeyDictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read JSON.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader">The Utf8JsonReader.</param>
|
||||||
|
/// <param name="typeToConvert">The type to convert.</param>
|
||||||
|
/// <param name="options">The json serializer options.</param>
|
||||||
|
/// <returns>Typed dictionary.</returns>
|
||||||
|
/// <exception cref="NotSupportedException"></exception>
|
||||||
|
public override IDictionary<TKey, TValue> 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<TKey, TValue>)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<TKey, TValue> is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
var element = (KeyValuePair<string?, TValue>)enumerator.Current;
|
||||||
|
instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write dictionary as Json.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">The Utf8JsonWriter.</param>
|
||||||
|
/// <param name="value">The dictionary value.</param>
|
||||||
|
/// <param name="options">The Json serializer options.</param>
|
||||||
|
public override void Write(Utf8JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var convertedDictionary = new Dictionary<string?, TValue>(value.Count);
|
||||||
|
foreach (var (k, v) in value)
|
||||||
|
{
|
||||||
|
convertedDictionary[k?.ToString()] = v;
|
||||||
|
}
|
||||||
|
JsonSerializer.Serialize(writer, convertedDictionary, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972.
|
||||||
|
/// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Only convert objects that implement IDictionary and do not have string keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeToConvert">Type convert.</param>
|
||||||
|
/// <returns>Conversion ability.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create converter for generic dictionary type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeToConvert">Type to convert.</param>
|
||||||
|
/// <param name="options">Json serializer options.</param>
|
||||||
|
/// <returns>JsonConverter for given type.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,10 +12,16 @@ namespace MediaBrowser.Common.Json
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the default <see cref="JsonSerializerOptions" /> options.
|
/// Gets the default <see cref="JsonSerializerOptions" /> options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When changing these options, update
|
||||||
|
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
|
||||||
|
/// -> AddJellyfinApi
|
||||||
|
/// -> AddJsonOptions
|
||||||
|
/// </remarks>
|
||||||
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
|
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
|
||||||
public static JsonSerializerOptions GetOptions()
|
public static JsonSerializerOptions GetOptions()
|
||||||
{
|
{
|
||||||
var options = new JsonSerializerOptions()
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
ReadCommentHandling = JsonCommentHandling.Disallow,
|
ReadCommentHandling = JsonCommentHandling.Disallow,
|
||||||
WriteIndented = false
|
WriteIndented = false
|
||||||
|
@ -23,8 +29,35 @@ namespace MediaBrowser.Common.Json
|
||||||
|
|
||||||
options.Converters.Add(new JsonGuidConverter());
|
options.Converters.Add(new JsonGuidConverter());
|
||||||
options.Converters.Add(new JsonStringEnumConverter());
|
options.Converters.Add(new JsonStringEnumConverter());
|
||||||
|
options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory());
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets CamelCase json options.
|
||||||
|
/// </summary>
|
||||||
|
public static JsonSerializerOptions CamelCase
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var options = GetOptions();
|
||||||
|
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets PascalCase json options.
|
||||||
|
/// </summary>
|
||||||
|
public static JsonSerializerOptions PascalCase
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var options = GetOptions();
|
||||||
|
options.PropertyNamingPolicy = null;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue