Pārlūkot izejas kodu

Merge pull request #4252 from skyfrk/4214-supported-commands-enum

Convert supportedCommands strings to enums
Bond-009 4 gadi atpakaļ
vecāks
revīzija
07be066180

+ 1 - 0
CONTRIBUTORS.md

@@ -137,6 +137,7 @@
  - [KristupasSavickas](https://github.com/KristupasSavickas)
  - [Pusta](https://github.com/pusta)
  - [nielsvanvelzen](https://github.com/nielsvanvelzen)
+ - [skyfrk](https://github.com/skyfrk)
 
 # Emby Contributors
 

+ 9 - 9
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo
 
                     SupportedCommands = new[]
                     {
-                        GeneralCommandType.VolumeDown.ToString(),
-                        GeneralCommandType.VolumeUp.ToString(),
-                        GeneralCommandType.Mute.ToString(),
-                        GeneralCommandType.Unmute.ToString(),
-                        GeneralCommandType.ToggleMute.ToString(),
-                        GeneralCommandType.SetVolume.ToString(),
-                        GeneralCommandType.SetAudioStreamIndex.ToString(),
-                        GeneralCommandType.SetSubtitleStreamIndex.ToString(),
-                        GeneralCommandType.PlayMediaSource.ToString()
+                        GeneralCommandType.VolumeDown,
+                        GeneralCommandType.VolumeUp,
+                        GeneralCommandType.Mute,
+                        GeneralCommandType.Unmute,
+                        GeneralCommandType.ToggleMute,
+                        GeneralCommandType.SetVolume,
+                        GeneralCommandType.SetAudioStreamIndex,
+                        GeneralCommandType.SetSubtitleStreamIndex,
+                        GeneralCommandType.PlayMediaSource
                     },
 
                     SupportsMediaControl = true

+ 3 - 2
Jellyfin.Api/Controllers/SessionController.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
@@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult PostCapabilities(
             [FromQuery] string? id,
             [FromQuery] string? playableMediaTypes,
-            [FromQuery] string? supportedCommands,
+            [FromQuery] GeneralCommandType[] supportedCommands,
             [FromQuery] bool supportsMediaControl = false,
             [FromQuery] bool supportsSync = false,
             [FromQuery] bool supportsPersistentIdentifier = true)
@@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
             _sessionManager.ReportCapabilities(id, new ClientCapabilities
             {
                 PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
-                SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
+                SupportedCommands = supportedCommands,
                 SupportsMediaControl = supportsMediaControl,
                 SupportsSync = supportsSync,
                 SupportsPersistentIdentifier = supportsPersistentIdentifier

+ 64 - 0
Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs

@@ -0,0 +1,64 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.ModelBinders
+{
+    /// <summary>
+    /// Comma delimited array model binder.
+    /// Returns an empty array of specified type if there is no query parameter.
+    /// </summary>
+    public class CommaDelimitedArrayModelBinder : IModelBinder
+    {
+        /// <inheritdoc/>
+        public Task BindModelAsync(ModelBindingContext bindingContext)
+        {
+            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+            var elementType = bindingContext.ModelType.GetElementType();
+            var converter = TypeDescriptor.GetConverter(elementType);
+
+            if (valueProviderResult == ValueProviderResult.None)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (valueProviderResult.Length > 1)
+            {
+                var result = Array.CreateInstance(elementType, valueProviderResult.Length);
+
+                for (int i = 0; i < valueProviderResult.Length; i++)
+                {
+                    var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim());
+
+                    result.SetValue(value, i);
+                }
+
+                bindingContext.Result = ModelBindingResult.Success(result);
+            }
+            else
+            {
+                var value = valueProviderResult.FirstValue;
+
+                if (value != null)
+                {
+                    var values = Array.ConvertAll(
+                        value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries),
+                        x => converter.ConvertFromString(x?.Trim()));
+
+                    var typedValues = Array.CreateInstance(elementType, values.Length);
+                    values.CopyTo(typedValues, 0);
+
+                    bindingContext.Result = ModelBindingResult.Success(typedValues);
+                }
+                else
+                {
+                    var emptyResult = Array.CreateInstance(elementType, 0);
+                    bindingContext.Result = ModelBindingResult.Success(emptyResult);
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 29 - 0
Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.ModelBinders
+{
+    /// <summary>
+    /// Comma delimited array model binder provider.
+    /// </summary>
+    public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider
+    {
+        private readonly IModelBinder _binder;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinderProvider"/> class.
+        /// </summary>
+        public CommaDelimitedArrayModelBinderProvider()
+        {
+            _binder = new CommaDelimitedArrayModelBinder();
+        }
+
+        /// <inheritdoc />
+        public IModelBinder? GetBinder(ModelBinderProviderContext context)
+        {
+            return context.Metadata.ModelType.IsArray ? _binder : null;
+        }
+    }
+}

+ 3 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -16,6 +16,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Server.Configuration;
 using Jellyfin.Server.Filters;
 using Jellyfin.Server.Formatters;
@@ -166,6 +167,8 @@ namespace Jellyfin.Server.Extensions
 
                     opts.OutputFormatters.Add(new CssOutputFormatter());
                     opts.OutputFormatters.Add(new XmlOutputFormatter());
+
+                    opts.ModelBinderProviders.Insert(0, new CommaDelimitedArrayModelBinderProvider());
                 })
 
                 // Clear app parts to avoid other assemblies being picked up

+ 2 - 2
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -230,8 +230,8 @@ namespace MediaBrowser.Controller.Session
         /// Gets or sets the supported commands.
         /// </summary>
         /// <value>The supported commands.</value>
-        public string[] SupportedCommands
-            => Capabilities == null ? Array.Empty<string>() : Capabilities.SupportedCommands;
+        public GeneralCommandType[] SupportedCommands
+            => Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
 
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {

+ 2 - 2
MediaBrowser.Model/Session/ClientCapabilities.cs

@@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Session
     {
         public string[] PlayableMediaTypes { get; set; }
 
-        public string[] SupportedCommands { get; set; }
+        public GeneralCommandType[] SupportedCommands { get; set; }
 
         public bool SupportsMediaControl { get; set; }
 
@@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Session
         public ClientCapabilities()
         {
             PlayableMediaTypes = Array.Empty<string>();
-            SupportedCommands = Array.Empty<string>();
+            SupportedCommands = Array.Empty<GeneralCommandType>();
             SupportsPersistentIdentifier = true;
         }
     }

+ 229 - 0
tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs

@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.ModelBinders;
+using MediaBrowser.Controller.Entities;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+    public sealed class CommaDelimitedArrayModelBinderTests
+    {
+        [Fact]
+        public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery()
+        {
+            var queryParamName = "test";
+            var queryParamValues = new string[] { "lol", "xd" };
+            var queryParamString = "lol,xd";
+            var queryParamType = typeof(string[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Equal((string[])bindingContextMock.Object.Result.Model, queryParamValues);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery()
+        {
+            var queryParamName = "test";
+            var queryParamValues = new int[] { 42, 0 };
+            var queryParamString = "42,0";
+            var queryParamType = typeof(int[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Equal((int[])bindingContextMock.Object.Result.Model, queryParamValues);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery()
+        {
+            var queryParamName = "test";
+            var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+            var queryParamString = "How,Much";
+            var queryParamType = typeof(TestType[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas()
+        {
+            var queryParamName = "test";
+            var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+            var queryParamString = "How,,Much";
+            var queryParamType = typeof(TestType[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery()
+        {
+            var queryParamName = "test";
+            var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+            var queryParamString1 = "How";
+            var queryParamString2 = "Much";
+            var queryParamType = typeof(TestType[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>()
+                    {
+                        { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) },
+                    }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery()
+        {
+            var queryParamName = "test";
+            var queryParamValues = Array.Empty<TestType>();
+            var queryParamType = typeof(TestType[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>()
+                    {
+                        { queryParamName, new StringValues(value: null) },
+                    }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            Assert.False(bindingContextMock.Object.Result.IsModelSet);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid()
+        {
+            var queryParamName = "test";
+            var queryParamString = "🔥,😢";
+            var queryParamType = typeof(TestType[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            await Assert.ThrowsAsync<FormatException>(act);
+        }
+
+        [Fact]
+        public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2()
+        {
+            var queryParamName = "test";
+            var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+            var queryParamString1 = "How";
+            var queryParamString2 = "😱";
+            var queryParamType = typeof(TestType[]);
+
+            var modelBinder = new CommaDelimitedArrayModelBinder();
+
+            var valueProvider = new QueryStringValueProvider(
+                    new BindingSource(string.Empty, string.Empty, false, false),
+                    new QueryCollection(new Dictionary<string, StringValues>()
+                    {
+                        { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) },
+                    }),
+                    CultureInfo.InvariantCulture);
+            var bindingContextMock = new Mock<ModelBindingContext>();
+            bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+            bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+            bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+            bindingContextMock.SetupProperty(b => b.Result);
+
+            Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+            await Assert.ThrowsAsync<FormatException>(act);
+        }
+    }
+}

+ 17 - 0
tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+    public enum TestType
+    {
+#pragma warning disable SA1602 // Enumeration items should be documented
+        How,
+        Much,
+        Is,
+        The,
+        Fish
+#pragma warning restore SA1602 // Enumeration items should be documented
+    }
+}