diff options
| -rw-r--r-- | Jellyfin.Api/Controllers/ConfigurationController.cs | 5 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PluginsController.cs | 5 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SyncPlayController.cs | 205 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/TimeSyncController.cs | 39 | ||||
| -rw-r--r-- | MediaBrowser.Api/SyncPlay/SyncPlayService.cs | 262 | ||||
| -rw-r--r-- | MediaBrowser.Api/SyncPlay/TimeSyncService.cs | 52 | ||||
| -rw-r--r-- | MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs | 56 | ||||
| -rw-r--r-- | MediaBrowser.Common/Json/JsonDefaults.cs | 1 | ||||
| -rw-r--r-- | MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs | 8 |
9 files changed, 316 insertions, 317 deletions
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 13933cb33..7d262ed59 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; @@ -22,6 +23,8 @@ namespace Jellyfin.Api.Controllers private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; + private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + /// <summary> /// Initializes a new instance of the <see cref="ConfigurationController"/> class. /// </summary> @@ -87,7 +90,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key) { var configurationType = _configurationManager.GetConfigurationType(key); - var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); + var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false); _configurationManager.SaveConfiguration(key, configuration); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 48bb867ac..770d74838 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; using MediaBrowser.Common; +using MediaBrowser.Common.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Plugins; @@ -25,6 +26,8 @@ namespace Jellyfin.Api.Controllers private readonly IApplicationHost _appHost; private readonly IInstallationManager _installationManager; + private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + /// <summary> /// Initializes a new instance of the <see cref="PluginsController"/> class. /// </summary> @@ -117,7 +120,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType) + var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) .ConfigureAwait(false); plugin.UpdateConfiguration(configuration); diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs new file mode 100644 index 000000000..55ed42227 --- /dev/null +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.SyncPlay; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// The sync play controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class SyncPlayController : BaseJellyfinApiController + { + private readonly ISessionManager _sessionManager; + private readonly IAuthorizationContext _authorizationContext; + private readonly ISyncPlayManager _syncPlayManager; + + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayController"/> class. + /// </summary> + /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> + /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> + /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param> + public SyncPlayController( + ISessionManager sessionManager, + IAuthorizationContext authorizationContext, + ISyncPlayManager syncPlayManager) + { + _sessionManager = sessionManager; + _authorizationContext = authorizationContext; + _syncPlayManager = syncPlayManager; + } + + /// <summary> + /// Create a new SyncPlay group. + /// </summary> + /// <response code="204">New group created.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("New")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult CreateNewGroup() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + _syncPlayManager.NewGroup(currentSession, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Join an existing SyncPlay group. + /// </summary> + /// <param name="groupId">The sync play group id.</param> + /// <response code="204">Group join successful.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Join")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult JoinGroup([FromQuery, Required] Guid groupId) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + + var joinRequest = new JoinGroupRequest() + { + GroupId = groupId + }; + + _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Leave the joined SyncPlay group. + /// </summary> + /// <response code="204">Group leave successful.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Leave")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult LeaveGroup() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Gets all SyncPlay groups. + /// </summary> + /// <param name="filterItemId">Optional. Filter by item id.</param> + /// <response code="200">Groups returned.</response> + /// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns> + [HttpGet("List")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<IEnumerable<GroupInfoView>> GetSyncPlayGroups([FromQuery] Guid? filterItemId) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty)); + } + + /// <summary> + /// Request play in SyncPlay group. + /// </summary> + /// <response code="204">Play request sent to all group members.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Play")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Play() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Play + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Request pause in SyncPlay group. + /// </summary> + /// <response code="204">Pause request sent to all group members.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Pause")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Pause() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Pause + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Request seek in SyncPlay group. + /// </summary> + /// <param name="positionTicks">The playback position in ticks.</param> + /// <response code="204">Seek request sent to all group members.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Seek")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Seek([FromQuery] long positionTicks) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Seek, + PositionTicks = positionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Request group wait in SyncPlay group while buffering. + /// </summary> + /// <param name="when">When the request has been made by the client.</param> + /// <param name="positionTicks">The playback position in ticks.</param> + /// <param name="bufferingDone">Whether the buffering is done.</param> + /// <response code="204">Buffering request sent to all group members.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Buffering")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer, + When = when, + PositionTicks = positionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// <summary> + /// Update session ping. + /// </summary> + /// <param name="ping">The ping.</param> + /// <response code="204">Ping updated.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + [HttpPost("Ping")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Ping([FromQuery] double ping) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Ping, + Ping = Convert.ToInt64(ping) + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + } +} diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs new file mode 100644 index 000000000..57a720b26 --- /dev/null +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using MediaBrowser.Model.SyncPlay; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// The time sync controller. + /// </summary> + [Route("/GetUtcTime")] + public class TimeSyncController : BaseJellyfinApiController + { + /// <summary> + /// Gets the current utc time. + /// </summary> + /// <response code="200">Time returned.</response> + /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns> + [HttpGet] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public ActionResult<UtcTimeResponse> GetUtcTime() + { + // Important to keep the following line at the beginning + var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo); + + var response = new UtcTimeResponse(); + response.RequestReceptionTime = requestReceptionTime; + + // Important to keep the following two lines at the end + var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo); + response.ResponseTransmissionTime = responseTransmissionTime; + + // Implementing NTP on such a high level results in this useless + // information being sent. On the other hand it enables future additions. + return response; + } + } +} diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs deleted file mode 100644 index daa1b521f..000000000 --- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System.Threading; -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.SyncPlay; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.SyncPlay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.SyncPlay -{ - [Route("/SyncPlay/New", "POST", Summary = "Create a new SyncPlay group")] - [Authenticated] - public class SyncPlayNew : IReturnVoid - { - } - - [Route("/SyncPlay/Join", "POST", Summary = "Join an existing SyncPlay group")] - [Authenticated] - public class SyncPlayJoin : IReturnVoid - { - /// <summary> - /// Gets or sets the Group id. - /// </summary> - /// <value>The Group id to join.</value> - [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string GroupId { get; set; } - } - - [Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")] - [Authenticated] - public class SyncPlayLeave : IReturnVoid - { - } - - [Route("/SyncPlay/List", "GET", Summary = "List SyncPlay groups")] - [Authenticated] - public class SyncPlayList : IReturnVoid - { - /// <summary> - /// Gets or sets the filter item id. - /// </summary> - /// <value>The filter item id.</value> - [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string FilterItemId { get; set; } - } - - [Route("/SyncPlay/Play", "POST", Summary = "Request play in SyncPlay group")] - [Authenticated] - public class SyncPlayPlay : IReturnVoid - { - } - - [Route("/SyncPlay/Pause", "POST", Summary = "Request pause in SyncPlay group")] - [Authenticated] - public class SyncPlayPause : IReturnVoid - { - } - - [Route("/SyncPlay/Seek", "POST", Summary = "Request seek in SyncPlay group")] - [Authenticated] - public class SyncPlaySeek : IReturnVoid - { - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - } - - [Route("/SyncPlay/Buffering", "POST", Summary = "Request group wait in SyncPlay group while buffering")] - [Authenticated] - public class SyncPlayBuffering : IReturnVoid - { - /// <summary> - /// Gets or sets the date used to pin PositionTicks in time. - /// </summary> - /// <value>The date related to PositionTicks.</value> - [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string When { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - - /// <summary> - /// Gets or sets whether this is a buffering or a ready request. - /// </summary> - /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value> - [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool BufferingDone { get; set; } - } - - [Route("/SyncPlay/Ping", "POST", Summary = "Update session ping")] - [Authenticated] - public class SyncPlayPing : IReturnVoid - { - [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] - public double Ping { get; set; } - } - - /// <summary> - /// Class SyncPlayService. - /// </summary> - public class SyncPlayService : BaseApiService - { - /// <summary> - /// The session context. - /// </summary> - private readonly ISessionContext _sessionContext; - - /// <summary> - /// The SyncPlay manager. - /// </summary> - private readonly ISyncPlayManager _syncPlayManager; - - public SyncPlayService( - ILogger<SyncPlayService> logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionContext sessionContext, - ISyncPlayManager syncPlayManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionContext = sessionContext; - _syncPlayManager = syncPlayManager; - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayNew request) - { - var currentSession = GetSession(_sessionContext); - _syncPlayManager.NewGroup(currentSession, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayJoin request) - { - var currentSession = GetSession(_sessionContext); - - Guid groupId; - if (!Guid.TryParse(request.GroupId, out groupId)) - { - Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId); - return; - } - - var joinRequest = new JoinGroupRequest() - { - GroupId = groupId - }; - - _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayLeave request) - { - var currentSession = GetSession(_sessionContext); - _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <value>The requested list of groups.</value> - public List<GroupInfoView> Get(SyncPlayList request) - { - var currentSession = GetSession(_sessionContext); - var filterItemId = Guid.Empty; - - if (!string.IsNullOrEmpty(request.FilterItemId) && !Guid.TryParse(request.FilterItemId, out filterItemId)) - { - Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); - } - - return _syncPlayManager.ListGroups(currentSession, filterItemId); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayPlay request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Play - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayPause request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Pause - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlaySeek request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Seek, - PositionTicks = request.PositionTicks - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayBuffering request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer, - When = DateTime.Parse(request.When), - PositionTicks = request.PositionTicks - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - public void Post(SyncPlayPing request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Ping, - Ping = Convert.ToInt64(request.Ping) - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - } -} diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs deleted file mode 100644 index 4a9307e62..000000000 --- a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.SyncPlay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.SyncPlay -{ - [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] - public class GetUtcTime : IReturnVoid - { - // Nothing - } - - /// <summary> - /// Class TimeSyncService. - /// </summary> - public class TimeSyncService : BaseApiService - { - public TimeSyncService( - ILogger<TimeSyncService> logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory) - : base(logger, serverConfigurationManager, httpResultFactory) - { - // Do nothing - } - - /// <summary> - /// Handles the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <value>The current UTC time response.</value> - public UtcTimeResponse Get(GetUtcTime request) - { - // Important to keep the following line at the beginning - var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - - var response = new UtcTimeResponse(); - response.RequestReceptionTime = requestReceptionTime; - - // Important to keep the following two lines at the end - var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - response.ResponseTransmissionTime = responseTransmissionTime; - - // Implementing NTP on such a high level results in this useless - // information being sent. On the other hand it enables future additions. - return response; - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs new file mode 100644 index 000000000..e5e9f28da --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Double to String JSON converter. + /// Web client send quoted doubles. + /// </summary> + public class JsonDoubleConverter : JsonConverter<double> + { + /// <summary> + /// Read JSON string as double. + /// </summary> + /// <param name="reader"><see cref="Utf8JsonReader"/>.</param> + /// <param name="typeToConvert">Type.</param> + /// <param name="options">Options.</param> + /// <returns>Parsed value.</returns> + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + // try to parse number directly from bytes + var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (Utf8Parser.TryParse(span, out double number, out var bytesConsumed) && span.Length == bytesConsumed) + { + return number; + } + + // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters + if (double.TryParse(reader.GetString(), out number)) + { + return number; + } + } + + // fallback to default handling + return reader.GetDouble(); + } + + /// <summary> + /// Write double to JSON string. + /// </summary> + /// <param name="writer"><see cref="Utf8JsonWriter"/>.</param> + /// <param name="value">Value to write.</param> + /// <param name="options">Options.</param> + public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(NumberFormatInfo.InvariantInfo)); + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 13f2f060b..36ab6d900 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); options.Converters.Add(new JsonInt64Converter()); + options.Converters.Add(new JsonDoubleConverter()); return options; } diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs index 66f3e1a94..b00d2fffb 100644 --- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs +++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs @@ -52,7 +52,13 @@ namespace MediaBrowser.Model.Configuration public string PreviousVersionStr { get => PreviousVersion?.ToString(); - set => PreviousVersion = Version.Parse(value); + set + { + if (Version.TryParse(value, out var version)) + { + PreviousVersion = version; + } + } } } } |
