Procházet zdrojové kódy

Merge branch 'master' into fix_and_mildly_improve_similar

Claus Vium před 4 roky
rodič
revize
a7b3880d0e
54 změnil soubory, kde provedl 568 přidání a 375 odebrání
  1. 0 19
      .ci/azure-pipelines-api-client.yml
  2. 5 0
      Emby.Naming/Video/CleanDateTimeParser.cs
  3. 30 54
      Emby.Server.Implementations/ApplicationHost.cs
  4. 3 2
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  5. 12 13
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  6. 8 2
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  7. 3 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  8. 3 1
      Emby.Server.Implementations/Localization/Core/es.json
  9. 23 6
      Emby.Server.Implementations/Localization/Core/fil.json
  10. 3 1
      Emby.Server.Implementations/Localization/Core/nl.json
  11. 2 1
      Emby.Server.Implementations/Localization/Core/tr.json
  12. 3 1
      Emby.Server.Implementations/Localization/Core/vi.json
  13. 2 1
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  14. 5 5
      Emby.Server.Implementations/Updates/InstallationManager.cs
  15. 1 1
      Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
  16. 8 10
      Jellyfin.Api/Controllers/ArtistsController.cs
  17. 6 8
      Jellyfin.Api/Controllers/ChannelsController.cs
  18. 5 5
      Jellyfin.Api/Controllers/GenresController.cs
  19. 29 35
      Jellyfin.Api/Controllers/InstantMixController.cs
  20. 7 9
      Jellyfin.Api/Controllers/ItemsController.cs
  21. 2 1
      Jellyfin.Api/Controllers/LibraryController.cs
  22. 22 26
      Jellyfin.Api/Controllers/LiveTvController.cs
  23. 3 3
      Jellyfin.Api/Controllers/MoviesController.cs
  24. 5 5
      Jellyfin.Api/Controllers/MusicGenresController.cs
  25. 5 6
      Jellyfin.Api/Controllers/PersonsController.cs
  26. 5 5
      Jellyfin.Api/Controllers/PlaylistsController.cs
  27. 5 5
      Jellyfin.Api/Controllers/StudiosController.cs
  28. 97 0
      Jellyfin.Api/Controllers/SubtitleController.cs
  29. 3 3
      Jellyfin.Api/Controllers/TrailersController.cs
  30. 15 18
      Jellyfin.Api/Controllers/TvShowsController.cs
  31. 5 5
      Jellyfin.Api/Controllers/UserLibraryController.cs
  32. 5 5
      Jellyfin.Api/Controllers/YearsController.cs
  33. 0 36
      Jellyfin.Api/Extensions/DtoExtensions.cs
  34. 0 27
      Jellyfin.Api/Helpers/RequestHelpers.cs
  35. 4 1
      Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
  36. 1 1
      Jellyfin.Server/Jellyfin.Server.csproj
  37. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  38. 46 0
      Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
  39. 113 0
      MediaBrowser.Common/Plugins/LocalPlugin.cs
  40. 9 1
      MediaBrowser.Controller/IServerApplicationHost.cs
  41. 5 0
      MediaBrowser.Controller/Net/AuthorizationInfo.cs
  42. 5 0
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  43. 0 2
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  44. 34 0
      MediaBrowser.Model/Subtitles/FontFile.cs
  45. 2 7
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  46. 7 25
      MediaBrowser.Providers/Manager/MetadataService.cs
  47. 1 9
      apiclient/templates/typescript/axios/generate.sh
  48. 3 2
      tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
  49. 1 1
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  50. 1 1
      tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
  51. 1 1
      tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
  52. 1 1
      tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
  53. 1 1
      tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
  54. 1 1
      tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj

+ 0 - 19
.ci/azure-pipelines-api-client.yml

@@ -35,14 +35,6 @@ jobs:
         customEndpoint: 'jellyfin-bot for NPM'
         customEndpoint: 'jellyfin-bot for NPM'
 
 
 ## Generate npm api client
 ## Generate npm api client
-# Unstable
-    - task: CmdLine@2
-      displayName: 'Build unstable typescript axios client'
-      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
-      inputs:
-        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
-
-# Stable
     - task: CmdLine@2
     - task: CmdLine@2
       displayName: 'Build stable typescript axios client'
       displayName: 'Build stable typescript axios client'
       condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
       condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
@@ -57,17 +49,6 @@ jobs:
         workingDir: ./apiclient/generated/typescript/axios
         workingDir: ./apiclient/generated/typescript/axios
 
 
 ## Publish npm packages
 ## Publish npm packages
-# Unstable
-    - task: Npm@1
-      displayName: 'Publish unstable typescript axios client'
-      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
-      inputs:
-        command: publish
-        publishRegistry: useFeed
-        publishFeed: 'jellyfin/unstable'
-        workingDir: ./apiclient/generated/typescript/axios
-
-# Stable
     - task: Npm@1
     - task: Npm@1
       displayName: 'Publish stable typescript axios client'
       displayName: 'Publish stable typescript axios client'
       condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
       condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

+ 5 - 0
Emby.Naming/Video/CleanDateTimeParser.cs

@@ -15,6 +15,11 @@ namespace Emby.Naming.Video
         public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
         public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
         {
         {
             CleanDateTimeResult result = new CleanDateTimeResult(name);
             CleanDateTimeResult result = new CleanDateTimeResult(name);
+            if (string.IsNullOrEmpty(name))
+            {
+                return result;
+            }
+
             var len = cleanDateTimeRegexes.Count;
             var len = cleanDateTimeRegexes.Count;
             for (int i = 0; i < len; i++)
             for (int i = 0; i < len; i++)
             {
             {

+ 30 - 54
Emby.Server.Implementations/ApplicationHost.cs

@@ -4,7 +4,6 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
-using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
@@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Devices;
 using Emby.Server.Implementations.Devices;
 using Emby.Server.Implementations.Dto;
 using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer.Security;
 using Emby.Server.Implementations.HttpServer.Security;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
@@ -993,62 +991,36 @@ namespace Emby.Server.Implementations
 
 
         protected abstract void RestartInternal();
         protected abstract void RestartInternal();
 
 
-        /// <summary>
-        /// Comparison function used in <see cref="GetPlugins" />.
-        /// </summary>
-        /// <param name="a">Item to compare.</param>
-        /// <param name="b">Item to compare with.</param>
-        /// <returns>Boolean result of the operation.</returns>
-        private static int VersionCompare(
-            (Version PluginVersion, string Name, string Path) a,
-            (Version PluginVersion, string Name, string Path) b)
-        {
-            int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
-
-            if (compare == 0)
-            {
-                return a.PluginVersion.CompareTo(b.PluginVersion);
-            }
-
-            return compare;
-        }
-
-        /// <summary>
-        /// Returns a list of plugins to install.
-        /// </summary>
-        /// <param name="path">Path to check.</param>
-        /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
-        /// <returns>Enumerable list of dlls to load.</returns>
-        private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+        /// <inheritdoc/>
+        public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
         {
         {
-            var dllList = new List<string>();
-            var versions = new List<(Version PluginVersion, string Name, string Path)>();
+            var minimumVersion = new Version(0, 0, 0, 1);
+            var versions = new List<LocalPlugin>();
             var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
             var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
-            string metafile;
 
 
             foreach (var dir in directories)
             foreach (var dir in directories)
             {
             {
                 try
                 try
                 {
                 {
-                    metafile = Path.Combine(dir, "meta.json");
+                    var metafile = Path.Combine(dir, "meta.json");
                     if (File.Exists(metafile))
                     if (File.Exists(metafile))
                     {
                     {
                         var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
                         var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
 
 
                         if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
                         if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
                         {
                         {
-                            targetAbi = new Version(0, 0, 0, 1);
+                            targetAbi = minimumVersion;
                         }
                         }
 
 
                         if (!Version.TryParse(manifest.Version, out var version))
                         if (!Version.TryParse(manifest.Version, out var version))
                         {
                         {
-                            version = new Version(0, 0, 0, 1);
+                            version = minimumVersion;
                         }
                         }
 
 
                         if (ApplicationVersion >= targetAbi)
                         if (ApplicationVersion >= targetAbi)
                         {
                         {
                             // Only load Plugins if the plugin is built for this version or below.
                             // Only load Plugins if the plugin is built for this version or below.
-                            versions.Add((version, manifest.Name, dir));
+                            versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
                         }
                         }
                     }
                     }
                     else
                     else
@@ -1057,15 +1029,15 @@ namespace Emby.Server.Implementations
                         metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
                         metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
 
 
                         int versionIndex = dir.LastIndexOf('_');
                         int versionIndex = dir.LastIndexOf('_');
-                        if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+                        if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
                         {
                         {
                             // Versioned folder.
                             // Versioned folder.
-                            versions.Add((ver, metafile, dir));
+                            versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
                         }
                         }
                         else
                         else
                         {
                         {
                             // Un-versioned folder - Add it under the path name and version 0.0.0.1.
                             // Un-versioned folder - Add it under the path name and version 0.0.0.1.
-                            versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+                            versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
                         }
                         }
                     }
                     }
                 }
                 }
@@ -1076,14 +1048,14 @@ namespace Emby.Server.Implementations
             }
             }
 
 
             string lastName = string.Empty;
             string lastName = string.Empty;
-            versions.Sort(VersionCompare);
+            versions.Sort(LocalPlugin.Compare);
             // Traverse backwards through the list.
             // Traverse backwards through the list.
             // The first item will be the latest version.
             // The first item will be the latest version.
             for (int x = versions.Count - 1; x >= 0; x--)
             for (int x = versions.Count - 1; x >= 0; x--)
             {
             {
                 if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
                 if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+                    versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
                     lastName = versions[x].Name;
                     lastName = versions[x].Name;
                     continue;
                     continue;
                 }
                 }
@@ -1091,6 +1063,7 @@ namespace Emby.Server.Implementations
                 if (!string.IsNullOrEmpty(lastName) && cleanup)
                 if (!string.IsNullOrEmpty(lastName) && cleanup)
                 {
                 {
                     // Attempt a cleanup of old folders.
                     // Attempt a cleanup of old folders.
+                    versions.RemoveAt(x);
                     try
                     try
                     {
                     {
                         Logger.LogDebug("Deleting {Path}", versions[x].Path);
                         Logger.LogDebug("Deleting {Path}", versions[x].Path);
@@ -1103,7 +1076,7 @@ namespace Emby.Server.Implementations
                 }
                 }
             }
             }
 
 
-            return dllList;
+            return versions;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1114,21 +1087,24 @@ namespace Emby.Server.Implementations
         {
         {
             if (Directory.Exists(ApplicationPaths.PluginsPath))
             if (Directory.Exists(ApplicationPaths.PluginsPath))
             {
             {
-                foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
+                foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
                 {
                 {
-                    Assembly plugAss;
-                    try
-                    {
-                        plugAss = Assembly.LoadFrom(file);
-                    }
-                    catch (FileLoadException ex)
+                    foreach (var file in plugin.DllFiles)
                     {
                     {
-                        Logger.LogError(ex, "Failed to load assembly {Path}", file);
-                        continue;
-                    }
+                        Assembly plugAss;
+                        try
+                        {
+                            plugAss = Assembly.LoadFrom(file);
+                        }
+                        catch (FileLoadException ex)
+                        {
+                            Logger.LogError(ex, "Failed to load assembly {Path}", file);
+                            continue;
+                        }
 
 
-                    Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
-                    yield return plugAss;
+                        Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
+                        yield return plugAss;
+                    }
                 }
                 }
             }
             }
 
 

+ 3 - 2
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 
 
@@ -19,9 +20,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
         public AuthorizationInfo Authenticate(HttpRequest request)
         public AuthorizationInfo Authenticate(HttpRequest request)
         {
         {
             var auth = _authorizationContext.GetAuthorizationInfo(request);
             var auth = _authorizationContext.GetAuthorizationInfo(request);
-            if (auth == null)
+            if (!auth.IsAuthenticated)
             {
             {
-                throw new SecurityException("Unauthenticated request.");
+                throw new AuthenticationException("Invalid token.");
             }
             }
 
 
             if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
             if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)

+ 12 - 13
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
         public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
         {
         {
             var auth = GetAuthorizationDictionary(requestContext);
             var auth = GetAuthorizationDictionary(requestContext);
-            var (authInfo, _) =
-                GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+            var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
             return authInfo;
             return authInfo;
         }
         }
 
 
@@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
         private AuthorizationInfo GetAuthorization(HttpContext httpReq)
         private AuthorizationInfo GetAuthorization(HttpContext httpReq)
         {
         {
             var auth = GetAuthorizationDictionary(httpReq);
             var auth = GetAuthorizationDictionary(httpReq);
-            var (authInfo, originalAuthInfo) =
-                GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
-
-            if (originalAuthInfo != null)
-            {
-                httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
-            }
+            var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
 
 
             httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             return authInfo;
             return authInfo;
         }
         }
 
 
-        private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+        private AuthorizationInfo GetAuthorizationInfoFromDictionary(
             in Dictionary<string, string> auth,
             in Dictionary<string, string> auth,
             in IHeaderDictionary headers,
             in IHeaderDictionary headers,
             in IQueryCollection queryString)
             in IQueryCollection queryString)
@@ -108,13 +101,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 Device = device,
                 Device = device,
                 DeviceId = deviceId,
                 DeviceId = deviceId,
                 Version = version,
                 Version = version,
-                Token = token
+                Token = token,
+                IsAuthenticated = false
             };
             };
 
 
             if (string.IsNullOrWhiteSpace(token))
             if (string.IsNullOrWhiteSpace(token))
             {
             {
                 // Request doesn't contain a token.
                 // Request doesn't contain a token.
-                return (null, null);
+                return authInfo;
             }
             }
 
 
             var result = _authRepo.Get(new AuthenticationInfoQuery
             var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -122,6 +116,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 AccessToken = token
                 AccessToken = token
             });
             });
 
 
+            if (result.Items.Count > 0)
+            {
+                authInfo.IsAuthenticated = true;
+            }
+
             var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
             var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
 
 
             if (originalAuthenticationInfo != null)
             if (originalAuthenticationInfo != null)
@@ -197,7 +196,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 }
                 }
             }
             }
 
 
-            return (authInfo, originalAuthenticationInfo);
+            return authInfo;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 8 - 2
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -15,6 +15,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
@@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
+        private readonly ICryptoProvider _cryptoProvider;
 
 
         public SchedulesDirect(
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
             ILogger<SchedulesDirect> logger,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
             IHttpClientFactory httpClientFactory,
             IHttpClientFactory httpClientFactory,
-            IApplicationHost appHost)
+            IApplicationHost appHost,
+            ICryptoProvider cryptoProvider)
         {
         {
             _logger = logger;
             _logger = logger;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _httpClientFactory = httpClientFactory;
             _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
+            _cryptoProvider = cryptoProvider;
         }
         }
 
 
         private string UserAgent => _appHost.ApplicationUserAgent;
         private string UserAgent => _appHost.ApplicationUserAgent;
@@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
             using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
             using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
-            options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
+            var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
+            string hashedPassword = Hex.Encode(hashedPasswordBytes);
+            options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
 
 
             using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
             using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

+ 3 - 1
Emby.Server.Implementations/Localization/Core/en-GB.json

@@ -113,5 +113,7 @@
     "TasksChannelsCategory": "Internet Channels",
     "TasksChannelsCategory": "Internet Channels",
     "TasksApplicationCategory": "Application",
     "TasksApplicationCategory": "Application",
     "TasksLibraryCategory": "Library",
     "TasksLibraryCategory": "Library",
-    "TasksMaintenanceCategory": "Maintenance"
+    "TasksMaintenanceCategory": "Maintenance",
+    "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
+    "TaskCleanActivityLog": "Clean Activity Log"
 }
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/es.json

@@ -113,5 +113,7 @@
     "TaskRefreshChannels": "Actualizar canales",
     "TaskRefreshChannels": "Actualizar canales",
     "TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
     "TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
     "TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
     "TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
-    "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos."
+    "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos.",
+    "TaskCleanActivityLogDescription": "Elimina todos los registros de actividad anteriores a la fecha configurada.",
+    "TaskCleanActivityLog": "Limpiar registro de actividad"
 }
 }

+ 23 - 6
Emby.Server.Implementations/Localization/Core/fil.json

@@ -1,7 +1,7 @@
 {
 {
     "VersionNumber": "Bersyon {0}",
     "VersionNumber": "Bersyon {0}",
     "ValueSpecialEpisodeName": "Espesyal - {0}",
     "ValueSpecialEpisodeName": "Espesyal - {0}",
-    "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library",
+    "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
     "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
     "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
     "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
     "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
     "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
     "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
@@ -61,8 +61,8 @@
     "Latest": "Pinakabago",
     "Latest": "Pinakabago",
     "LabelRunningTimeValue": "Oras: {0}",
     "LabelRunningTimeValue": "Oras: {0}",
     "LabelIpAddressValue": "Ang IP Address ay {0}",
     "LabelIpAddressValue": "Ang IP Address ay {0}",
-    "ItemRemovedWithName": "Naitanggal ang {0} sa library",
-    "ItemAddedWithName": "Naidagdag ang {0} sa library",
+    "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
+    "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
     "Inherit": "Manahin",
     "Inherit": "Manahin",
     "HeaderRecordingGroups": "Pagtatalang Grupo",
     "HeaderRecordingGroups": "Pagtatalang Grupo",
     "HeaderNextUp": "Susunod",
     "HeaderNextUp": "Susunod",
@@ -90,12 +90,29 @@
     "Application": "Aplikasyon",
     "Application": "Aplikasyon",
     "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
     "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
     "Albums": "Albums",
     "Albums": "Albums",
-    "TaskRefreshLibrary": "Suriin ang nasa librerya",
-    "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata",
+    "TaskRefreshLibrary": "Suriin and Librerya ng Medya",
+    "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
     "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
     "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
     "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
     "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
     "TasksChannelsCategory": "Palabas sa internet",
     "TasksChannelsCategory": "Palabas sa internet",
     "TasksLibraryCategory": "Librerya",
     "TasksLibraryCategory": "Librerya",
     "TasksMaintenanceCategory": "Pagpapanatili",
     "TasksMaintenanceCategory": "Pagpapanatili",
-    "HomeVideos": "Sariling pelikula"
+    "HomeVideos": "Sariling pelikula",
+    "TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
+    "TaskRefreshPeople": "I-refresh ang Tauhan",
+    "TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
+    "TaskDownloadMissingSubtitles": "I-download and nawawalang subtitles",
+    "TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
+    "TaskRefreshChannels": "I-refresh ang Channels",
+    "TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
+    "TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
+    "TaskUpdatePlugins": "I-update ang Plugins",
+    "TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
+    "TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
+    "TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
+    "TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
+    "TaskCleanCache": "Linisin and Direktoryo ng Cache",
+    "TasksApplicationCategory": "Application",
+    "TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
+    "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
 }
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/nl.json

@@ -113,5 +113,7 @@
     "TasksChannelsCategory": "Internet Kanalen",
     "TasksChannelsCategory": "Internet Kanalen",
     "TasksApplicationCategory": "Applicatie",
     "TasksApplicationCategory": "Applicatie",
     "TasksLibraryCategory": "Bibliotheek",
     "TasksLibraryCategory": "Bibliotheek",
-    "TasksMaintenanceCategory": "Onderhoud"
+    "TasksMaintenanceCategory": "Onderhoud",
+    "TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
+    "TaskCleanActivityLog": "Leeg activiteiten logboek"
 }
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/tr.json

@@ -114,5 +114,6 @@
     "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
     "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
     "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
     "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
     "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
     "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
-    "TaskCleanActivityLog": "İşlem Günlüğünü Temizle"
+    "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
+    "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi."
 }
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/vi.json

@@ -112,5 +112,7 @@
     "Books": "Sách",
     "Books": "Sách",
     "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
     "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
     "Application": "Ứng Dụng",
     "Application": "Ứng Dụng",
-    "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
+    "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
+    "TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
+    "TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động"
 }
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/zh-CN.json

@@ -114,5 +114,6 @@
     "TaskCleanCache": "清理缓存目录",
     "TaskCleanCache": "清理缓存目录",
     "TasksApplicationCategory": "应用程序",
     "TasksApplicationCategory": "应用程序",
     "TasksMaintenanceCategory": "维护",
     "TasksMaintenanceCategory": "维护",
-    "TaskCleanActivityLog": "清理程序日志"
+    "TaskCleanActivityLog": "清理程序日志",
+    "TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
 }
 }

+ 5 - 5
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -16,7 +16,7 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Common.Updates;
-using MediaBrowser.Common.System;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Controller.Events.Updates;
@@ -25,7 +25,6 @@ using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Updates;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.System;
 
 
 namespace Emby.Server.Implementations.Updates
 namespace Emby.Server.Implementations.Updates
 {
 {
@@ -49,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
         /// Gets the application host.
         /// Gets the application host.
         /// </summary>
         /// </summary>
         /// <value>The application host.</value>
         /// <value>The application host.</value>
-        private readonly IApplicationHost _applicationHost;
+        private readonly IServerApplicationHost _applicationHost;
 
 
         private readonly IZipClient _zipClient;
         private readonly IZipClient _zipClient;
 
 
@@ -67,7 +66,7 @@ namespace Emby.Server.Implementations.Updates
 
 
         public InstallationManager(
         public InstallationManager(
             ILogger<InstallationManager> logger,
             ILogger<InstallationManager> logger,
-            IApplicationHost appHost,
+            IServerApplicationHost appHost,
             IApplicationPaths appPaths,
             IApplicationPaths appPaths,
             IEventManager eventManager,
             IEventManager eventManager,
             IHttpClientFactory httpClientFactory,
             IHttpClientFactory httpClientFactory,
@@ -217,7 +216,8 @@ namespace Emby.Server.Implementations.Updates
 
 
         private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
         private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
         {
         {
-            foreach (var plugin in _applicationHost.Plugins)
+            var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
+            foreach (var plugin in plugins)
             {
             {
                 var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
                 var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
                 var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
                 var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);

+ 1 - 1
Jellyfin.Api/Auth/CustomAuthenticationHandler.cs

@@ -1,10 +1,10 @@
 using System.Globalization;
 using System.Globalization;
-using System.Security.Authentication;
 using System.Security.Claims;
 using System.Security.Claims;
 using System.Text.Encodings.Web;
 using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;

+ 8 - 10
Jellyfin.Api/Controllers/ArtistsController.cs

@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? years,
             [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? person,
             [FromQuery] string? person,
             [FromQuery] string? personIds,
             [FromQuery] string? personIds,
             [FromQuery] string? personTypes,
             [FromQuery] string? personTypes,
@@ -114,8 +114,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 
@@ -262,7 +261,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
@@ -297,7 +296,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
@@ -310,7 +309,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? years,
             [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? person,
             [FromQuery] string? person,
             [FromQuery] string? personIds,
             [FromQuery] string? personIds,
             [FromQuery] string? personTypes,
             [FromQuery] string? personTypes,
@@ -323,8 +322,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 6 - 8
Jellyfin.Api/Controllers/ChannelsController.cs

@@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
         /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <response code="200">Channel items returned.</response>
         /// <response code="200">Channel items returned.</response>
         /// <returns>
         /// <returns>
         /// A <see cref="Task"/> representing the request to get the channel items.
         /// A <see cref="Task"/> representing the request to get the channel items.
@@ -124,7 +124,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? sortOrder,
             [FromQuery] string? sortOrder,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] string? sortBy,
             [FromQuery] string? sortBy,
-            [FromQuery] string? fields)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
@@ -137,8 +137,7 @@ namespace Jellyfin.Api.Controllers
                 ChannelIds = new[] { channelId },
                 ChannelIds = new[] { channelId },
                 ParentId = folderId ?? Guid.Empty,
                 ParentId = folderId ?? Guid.Empty,
                 OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
                 OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
-                DtoOptions = new DtoOptions()
-                    .AddItemFields(fields)
+                DtoOptions = new DtoOptions { Fields = fields }
             };
             };
 
 
             foreach (var filter in filters)
             foreach (var filter in filters)
@@ -185,7 +184,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="filters">Optional. Specify additional filters to apply.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
         /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
         /// <response code="200">Latest channel items returned.</response>
         /// <response code="200">Latest channel items returned.</response>
         /// <returns>
         /// <returns>
@@ -198,7 +197,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? channelIds)
             [FromQuery] string? channelIds)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -214,8 +213,7 @@ namespace Jellyfin.Api.Controllers
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Select(i => new Guid(i))
                     .Select(i => new Guid(i))
                     .ToArray(),
                     .ToArray(),
-                DtoOptions = new DtoOptions()
-                    .AddItemFields(fields)
+                DtoOptions = new DtoOptions { Fields = fields }
             };
             };
 
 
             foreach (var filter in filters)
             foreach (var filter in filters)

+ 5 - 5
Jellyfin.Api/Controllers/GenresController.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="searchTerm">The search term.</param>
         /// <param name="searchTerm">The search term.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
@@ -72,12 +73,12 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] string? nameStartsWithOrGreater,
             [FromQuery] string? nameStartsWithOrGreater,
             [FromQuery] string? nameStartsWith,
             [FromQuery] string? nameStartsWith,
@@ -85,8 +86,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 
 

+ 29 - 35
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -55,7 +56,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -68,18 +69,17 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -105,18 +105,17 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var album = _libraryManager.GetItemById(id);
             var album = _libraryManager.GetItemById(id);
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
@@ -129,7 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -142,18 +141,17 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var playlist = (Playlist)_libraryManager.GetItemById(id);
             var playlist = (Playlist)_libraryManager.GetItemById(id);
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
@@ -166,7 +164,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="name">The genre name.</param>
         /// <param name="name">The genre name.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -179,17 +177,16 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
@@ -202,7 +199,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -215,18 +212,17 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@@ -239,7 +235,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -252,18 +248,17 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@@ -276,7 +271,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -289,18 +284,17 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);

+ 7 - 9
Jellyfin.Api/Controllers/ItemsController.cs

@@ -180,13 +180,13 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? sortOrder,
             [FromQuery] string? sortOrder,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? mediaTypes,
-            [FromQuery] ImageType[] imageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
             [FromQuery] string? sortBy,
             [FromQuery] string? sortBy,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? isPlayed,
             [FromQuery] string? genres,
             [FromQuery] string? genres,
@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? years,
             [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? person,
             [FromQuery] string? person,
             [FromQuery] string? personIds,
             [FromQuery] string? personIds,
             [FromQuery] string? personTypes,
             [FromQuery] string? personTypes,
@@ -234,8 +234,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 
@@ -533,11 +532,11 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? mediaTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool enableTotalRecordCount = true,
             [FromQuery] bool enableTotalRecordCount = true,
@@ -545,8 +544,7 @@ namespace Jellyfin.Api.Controllers
         {
         {
             var user = _userManager.GetUserById(userId);
             var user = _userManager.GetUserById(userId);
             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 2 - 1
Jellyfin.Api/Controllers/LibraryController.cs

@@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
@@ -693,7 +694,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? excludeArtistIds,
             [FromQuery] string? excludeArtistIds,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
         {
         {
             var item = itemId.Equals(Guid.Empty)
             var item = itemId.Equals(Guid.Empty)
                 ? (!userId.Equals(Guid.Empty)
                 ? (!userId.Equals(Guid.Empty)

+ 22 - 26
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
@@ -118,7 +119,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">"Optional. The image types to include in the output.</param>
         /// <param name="enableImageTypes">"Optional. The image types to include in the output.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="sortBy">Optional. Key to sort by.</param>
         /// <param name="sortBy">Optional. Key to sort by.</param>
         /// <param name="sortOrder">Optional. Sort order.</param>
         /// <param name="sortOrder">Optional. Sort order.</param>
@@ -146,16 +147,15 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isDisliked,
             [FromQuery] bool? isDisliked,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] string? sortBy,
             [FromQuery] string? sortBy,
             [FromQuery] SortOrder? sortOrder,
             [FromQuery] SortOrder? sortOrder,
             [FromQuery] bool enableFavoriteSorting = false,
             [FromQuery] bool enableFavoriteSorting = false,
             [FromQuery] bool addCurrentProgram = true)
             [FromQuery] bool addCurrentProgram = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 
@@ -239,7 +239,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="isMovie">Optional. Filter for movies.</param>
         /// <param name="isMovie">Optional. Filter for movies.</param>
         /// <param name="isSeries">Optional. Filter for series.</param>
         /// <param name="isSeries">Optional. Filter for series.</param>
@@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? seriesTimerId,
             [FromQuery] string? seriesTimerId,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSeries,
             [FromQuery] bool? isSeries,
@@ -274,8 +274,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isLibraryItem,
             [FromQuery] bool? isLibraryItem,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 
@@ -296,7 +295,7 @@ namespace Jellyfin.Api.Controllers
                 IsKids = isKids,
                 IsKids = isKids,
                 IsSports = isSports,
                 IsSports = isSports,
                 IsLibraryItem = isLibraryItem,
                 IsLibraryItem = isLibraryItem,
-                Fields = RequestHelpers.GetItemFields(fields),
+                Fields = fields,
                 ImageTypeLimit = imageTypeLimit,
                 ImageTypeLimit = imageTypeLimit,
                 EnableImages = enableImages
                 EnableImages = enableImages
             }, dtoOptions);
             }, dtoOptions);
@@ -316,7 +315,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
         /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
         /// <response code="200">Live tv recordings returned.</response>
         /// <response code="200">Live tv recordings returned.</response>
@@ -350,8 +349,8 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? seriesTimerId,
             [FromQuery] string? seriesTimerId,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
@@ -530,7 +529,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
         /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
         /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
         /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
         /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
         /// <response code="200">Live tv epgs returned.</response>
         /// <response code="200">Live tv epgs returned.</response>
         /// <returns>
         /// <returns>
@@ -561,11 +560,11 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? genreIds,
             [FromQuery] string? genreIds,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] string? seriesTimerId,
             [FromQuery] string? seriesTimerId,
             [FromQuery] Guid? librarySeriesId,
             [FromQuery] Guid? librarySeriesId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -606,8 +605,7 @@ namespace Jellyfin.Api.Controllers
                 }
                 }
             }
             }
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
             return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
@@ -662,8 +660,7 @@ namespace Jellyfin.Api.Controllers
                 }
                 }
             }
             }
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(body.Fields)
+            var dtoOptions = new DtoOptions { Fields = body.Fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
                 .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
             return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
             return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
@@ -685,7 +682,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="genreIds">The genres to return guide information for.</param>
         /// <param name="genreIds">The genres to return guide information for.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableUserData">Optional. include user data.</param>
         /// <param name="enableUserData">Optional. include user data.</param>
         /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
         /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
         /// <response code="200">Recommended epgs returned.</response>
         /// <response code="200">Recommended epgs returned.</response>
@@ -705,9 +702,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isSports,
             [FromQuery] bool? isSports,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? genreIds,
             [FromQuery] string? genreIds,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
@@ -729,8 +726,7 @@ namespace Jellyfin.Api.Controllers
                 GenreIds = RequestHelpers.GetGuids(genreIds)
                 GenreIds = RequestHelpers.GetGuids(genreIds)
             };
             };
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
             return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);

+ 3 - 3
Jellyfin.Api/Controllers/MoviesController.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
@@ -65,15 +66,14 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
         public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] int categoryLimit = 5,
             [FromQuery] int categoryLimit = 5,
             [FromQuery] int itemLimit = 8)
             [FromQuery] int itemLimit = 8)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request);
                 .AddClientFields(Request);
 
 
             var categories = new List<RecommendationDto>();
             var categories = new List<RecommendationDto>();

+ 5 - 5
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="searchTerm">The search term.</param>
         /// <param name="searchTerm">The search term.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
@@ -72,12 +73,12 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] string? nameStartsWithOrGreater,
             [FromQuery] string? nameStartsWithOrGreater,
             [FromQuery] string? nameStartsWith,
             [FromQuery] string? nameStartsWith,
@@ -85,8 +86,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 
 

+ 5 - 6
Jellyfin.Api/Controllers/PersonsController.cs

@@ -53,8 +53,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// </summary>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="searchTerm">The search term.</param>
         /// <param name="searchTerm">The search term.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
-        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
         /// <param name="enableUserData">Optional, include user data.</param>
         /// <param name="enableUserData">Optional, include user data.</param>
         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
@@ -71,20 +71,19 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetPersons(
         public ActionResult<QueryResult<BaseItemDto>> GetPersons(
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? excludePersonTypes,
             [FromQuery] string? excludePersonTypes,
             [FromQuery] string? personTypes,
             [FromQuery] string? personTypes,
             [FromQuery] string? appearsInItemId,
             [FromQuery] string? appearsInItemId,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] bool? enableImages = true)
             [FromQuery] bool? enableImages = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 5 - 5
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.PlaylistDtos;
 using Jellyfin.Api.Models.PlaylistDtos;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -134,7 +135,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">User id.</param>
         /// <param name="userId">User id.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -148,11 +149,11 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, Required] Guid userId,
             [FromQuery, Required] Guid userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
         {
         {
             var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
             var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
             if (playlist == null)
             if (playlist == null)
@@ -176,8 +177,7 @@ namespace Jellyfin.Api.Controllers
                 items = items.Take(limit.Value).ToArray();
                 items = items.Take(limit.Value).ToArray();
             }
             }
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 5 - 5
Jellyfin.Api/Controllers/StudiosController.cs

@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -49,7 +50,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="searchTerm">Optional. Search term.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
@@ -71,13 +72,13 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] string? nameStartsWithOrGreater,
             [FromQuery] string? nameStartsWithOrGreater,
             [FromQuery] string? nameStartsWith,
             [FromQuery] string? nameStartsWith,
@@ -85,8 +86,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 97 - 0
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -12,6 +12,8 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.SubtitleDtos;
 using Jellyfin.Api.Models.SubtitleDtos;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
@@ -22,6 +24,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Subtitles;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
@@ -35,6 +38,7 @@ namespace Jellyfin.Api.Controllers
     [Route("")]
     [Route("")]
     public class SubtitleController : BaseJellyfinApiController
     public class SubtitleController : BaseJellyfinApiController
     {
     {
+        private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ISubtitleManager _subtitleManager;
         private readonly ISubtitleManager _subtitleManager;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly ISubtitleEncoder _subtitleEncoder;
@@ -47,6 +51,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SubtitleController"/> class.
         /// Initializes a new instance of the <see cref="SubtitleController"/> class.
         /// </summary>
         /// </summary>
+        /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
         /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
         /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
         /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
@@ -56,6 +61,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
         /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
         public SubtitleController(
         public SubtitleController(
+            IServerConfigurationManager serverConfigurationManager,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             ISubtitleManager subtitleManager,
             ISubtitleManager subtitleManager,
             ISubtitleEncoder subtitleEncoder,
             ISubtitleEncoder subtitleEncoder,
@@ -65,6 +71,7 @@ namespace Jellyfin.Api.Controllers
             IAuthorizationContext authContext,
             IAuthorizationContext authContext,
             ILogger<SubtitleController> logger)
             ILogger<SubtitleController> logger)
         {
         {
+            _serverConfigurationManager = serverConfigurationManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _subtitleManager = subtitleManager;
             _subtitleManager = subtitleManager;
             _subtitleEncoder = subtitleEncoder;
             _subtitleEncoder = subtitleEncoder;
@@ -379,5 +386,95 @@ namespace Jellyfin.Api.Controllers
                 copyTimestamps,
                 copyTimestamps,
                 CancellationToken.None);
                 CancellationToken.None);
         }
         }
+
+        /// <summary>
+        /// Gets a list of available fallback font files.
+        /// </summary>
+        /// <response code="200">Information retrieved.</response>
+        /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
+        [HttpGet("FallbackFont/Fonts")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public IEnumerable<FontFile> GetFallbackFontList()
+        {
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+            var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+            if (!string.IsNullOrEmpty(fallbackFontPath))
+            {
+                var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
+                var fontFiles = files
+                    .Select(i => new FontFile
+                    {
+                        Name = i.Name,
+                        Size = i.Length,
+                        DateCreated = _fileSystem.GetCreationTimeUtc(i),
+                        DateModified = _fileSystem.GetLastWriteTimeUtc(i)
+                    })
+                    .OrderBy(i => i.Size)
+                    .ThenBy(i => i.Name)
+                    .ThenByDescending(i => i.DateModified)
+                    .ThenByDescending(i => i.DateCreated);
+                // max total size 20M
+                const int MaxSize = 20971520;
+                var sizeCounter = 0L;
+                foreach (var fontFile in fontFiles)
+                {
+                    sizeCounter += fontFile.Size;
+                    if (sizeCounter >= MaxSize)
+                    {
+                        _logger.LogWarning("Some fonts will not be sent due to size limitations");
+                        yield break;
+                    }
+
+                    yield return fontFile;
+                }
+            }
+            else
+            {
+                _logger.LogWarning("The path of fallback font folder has not been set");
+                encodingOptions.EnableFallbackFont = false;
+            }
+        }
+
+        /// <summary>
+        /// Gets a fallback font file.
+        /// </summary>
+        /// <param name="name">The name of the fallback font file to get.</param>
+        /// <response code="200">Fallback font file retrieved.</response>
+        /// <returns>The fallback font file.</returns>
+        [HttpGet("FallbackFont/Fonts/{name}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult GetFallbackFont([FromRoute, Required] string name)
+        {
+            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+            var fallbackFontPath = encodingOptions.FallbackFontPath;
+
+            if (!string.IsNullOrEmpty(fallbackFontPath))
+            {
+                var fontFile = _fileSystem.GetFiles(fallbackFontPath)
+                    .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+                var fileSize = fontFile?.Length;
+
+                if (fontFile != null && fileSize != null && fileSize > 0)
+                {
+                    _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
+                    return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
+                }
+                else
+                {
+                    _logger.LogWarning("The selected font is null or empty");
+                }
+            }
+            else
+            {
+                _logger.LogWarning("The path of fallback font folder has not been set");
+                encodingOptions.EnableFallbackFont = false;
+            }
+
+            // returning HTTP 204 will break the SubtitlesOctopus
+            return Ok();
+        }
     }
     }
 }
 }

+ 3 - 3
Jellyfin.Api/Controllers/TrailersController.cs

@@ -146,12 +146,12 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? searchTerm,
             [FromQuery] string? sortOrder,
             [FromQuery] string? sortOrder,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? isFavorite,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? mediaTypes,
-            [FromQuery] ImageType[] imageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
             [FromQuery] string? sortBy,
             [FromQuery] string? sortBy,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? isPlayed,
             [FromQuery] string? genres,
             [FromQuery] string? genres,
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? years,
             [FromQuery] string? years,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? person,
             [FromQuery] string? person,
             [FromQuery] string? personIds,
             [FromQuery] string? personIds,
             [FromQuery] string? personTypes,
             [FromQuery] string? personTypes,

+ 15 - 18
Jellyfin.Api/Controllers/TvShowsController.cs

@@ -5,6 +5,7 @@ using System.Globalization;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
@@ -58,7 +59,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user id of the user to get the next up episodes for.</param>
         /// <param name="userId">The user id of the user to get the next up episodes for.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="seriesId">Optional. Filter by series id.</param>
         /// <param name="seriesId">Optional. Filter by series id.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="enableImges">Optional. Include image information in output.</param>
         /// <param name="enableImges">Optional. Include image information in output.</param>
@@ -73,17 +74,16 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? seriesId,
             [FromQuery] string? seriesId,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
             [FromQuery] bool? enableImges,
             [FromQuery] bool? enableImges,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
             [FromQuery] bool enableTotalRecordCount = true)
         {
         {
-            var options = new DtoOptions()
-                .AddItemFields(fields!)
+            var options = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
 
 
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userId">The user id of the user to get the upcoming episodes for.</param>
         /// <param name="userId">The user id of the user to get the upcoming episodes for.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="enableImges">Optional. Include image information in output.</param>
         /// <param name="enableImges">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -131,11 +131,11 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
             [FromQuery] bool? enableImges,
             [FromQuery] bool? enableImges,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData)
             [FromQuery] bool? enableUserData)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -146,8 +146,7 @@ namespace Jellyfin.Api.Controllers
 
 
             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
 
 
-            var options = new DtoOptions()
-                .AddItemFields(fields!)
+            var options = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
 
 
@@ -197,7 +196,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
             [FromRoute, Required] string seriesId,
             [FromRoute, Required] string seriesId,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] int? season,
             [FromQuery] int? season,
             [FromQuery] string? seasonId,
             [FromQuery] string? seasonId,
             [FromQuery] bool? isMissing,
             [FromQuery] bool? isMissing,
@@ -207,7 +206,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] string? sortBy)
             [FromQuery] string? sortBy)
         {
         {
@@ -217,8 +216,7 @@ namespace Jellyfin.Api.Controllers
 
 
             List<BaseItem> episodes;
             List<BaseItem> episodes;
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields!)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 
 
@@ -320,13 +318,13 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
             [FromRoute, Required] string seriesId,
             [FromRoute, Required] string seriesId,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,
             [FromQuery] bool? isMissing,
             [FromQuery] string? adjacentTo,
             [FromQuery] string? adjacentTo,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData)
             [FromQuery] bool? enableUserData)
         {
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -345,8 +343,7 @@ namespace Jellyfin.Api.Controllers
                 AdjacentTo = adjacentTo
                 AdjacentTo = adjacentTo
             });
             });
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 
 

+ 5 - 5
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -251,7 +252,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// </summary>
         /// <param name="userId">User id.</param>
         /// <param name="userId">User id.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
         /// <param name="isPlayed">Filter by items that are played, or not.</param>
         /// <param name="isPlayed">Filter by items that are played, or not.</param>
         /// <param name="enableImages">Optional. include image information in output.</param>
         /// <param name="enableImages">Optional. include image information in output.</param>
@@ -267,12 +268,12 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
         public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
             [FromRoute, Required] Guid userId,
             [FromRoute, Required] Guid userId,
             [FromQuery] Guid? parentId,
             [FromQuery] Guid? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int limit = 20,
             [FromQuery] int limit = 20,
             [FromQuery] bool groupItems = true)
             [FromQuery] bool groupItems = true)
@@ -287,8 +288,7 @@ namespace Jellyfin.Api.Controllers
                 }
                 }
             }
             }
 
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 5 - 5
Jellyfin.Api/Controllers/YearsController.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
         /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
@@ -71,20 +72,19 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] int? limit,
             [FromQuery] string? sortOrder,
             [FromQuery] string? sortOrder,
             [FromQuery] string? parentId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? mediaTypes,
             [FromQuery] string? sortBy,
             [FromQuery] string? sortBy,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
             [FromQuery] int? imageTypeLimit,
-            [FromQuery] ImageType[] enableImageTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] bool recursive = true,
             [FromQuery] bool recursive = true,
             [FromQuery] bool? enableImages = true)
             [FromQuery] bool? enableImages = true)
         {
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
 

+ 0 - 36
Jellyfin.Api/Extensions/DtoExtensions.cs

@@ -13,42 +13,6 @@ namespace Jellyfin.Api.Extensions
     /// </summary>
     /// </summary>
     public static class DtoExtensions
     public static class DtoExtensions
     {
     {
-        /// <summary>
-        /// Add Dto Item fields.
-        /// </summary>
-        /// <remarks>
-        /// Converted from IHasItemFields.
-        /// Legacy order: 1.
-        /// </remarks>
-        /// <param name="dtoOptions">DtoOptions object.</param>
-        /// <param name="fields">Comma delimited string of fields.</param>
-        /// <returns>Modified DtoOptions object.</returns>
-        internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields)
-        {
-            if (string.IsNullOrEmpty(fields))
-            {
-                dtoOptions.Fields = Array.Empty<ItemFields>();
-            }
-            else
-            {
-                dtoOptions.Fields = fields.Split(',')
-                    .Select(v =>
-                    {
-                        if (Enum.TryParse(v, true, out ItemFields value))
-                        {
-                            return (ItemFields?)value;
-                        }
-
-                        return null;
-                    })
-                    .Where(i => i.HasValue)
-                    .Select(i => i!.Value)
-                    .ToArray();
-            }
-
-            return dtoOptions;
-        }
-
         /// <summary>
         /// <summary>
         /// Add additional fields depending on client.
         /// Add additional fields depending on client.
         /// </summary>
         /// </summary>

+ 0 - 27
Jellyfin.Api/Helpers/RequestHelpers.cs

@@ -165,33 +165,6 @@ namespace Jellyfin.Api.Helpers
                 .ToArray();
                 .ToArray();
         }
         }
 
 
-        /// <summary>
-        /// Gets the item fields.
-        /// </summary>
-        /// <param name="imageTypes">The image types string.</param>
-        /// <returns>IEnumerable{ItemFields}.</returns>
-        internal static ImageType[] GetImageTypes(string? imageTypes)
-        {
-            if (string.IsNullOrEmpty(imageTypes))
-            {
-                return Array.Empty<ImageType>();
-            }
-
-            return Split(imageTypes, ',', true)
-                .Select(v =>
-                {
-                    if (Enum.TryParse(v, true, out ImageType value))
-                    {
-                        return (ImageType?)value;
-                    }
-
-                    return null;
-                })
-                .Where(i => i.HasValue)
-                .Select(i => i!.Value)
-                .ToArray();
-        }
-
         internal static QueryResult<BaseItemDto> CreateQueryResult(
         internal static QueryResult<BaseItemDto> CreateQueryResult(
             QueryResult<(BaseItem, ItemCounts)> result,
             QueryResult<(BaseItem, ItemCounts)> result,
             DtoOptions dtoOptions,
             DtoOptions dtoOptions,

+ 4 - 1
Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs

@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization;
 using MediaBrowser.Common.Json.Converters;
 using MediaBrowser.Common.Json.Converters;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 
 
 namespace Jellyfin.Api.Models.LiveTvDtos
 namespace Jellyfin.Api.Models.LiveTvDtos
 {
 {
@@ -167,6 +168,8 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
         /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
         /// Optional.
         /// Optional.
         /// </summary>
         /// </summary>
-        public string? Fields { get; set; }
+        [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "Fields", Justification = "Imported from ServiceStack")]
+        public ItemFields[] Fields { get; set; } = Array.Empty<ItemFields>();
     }
     }
 }
 }

+ 1 - 1
Jellyfin.Server/Jellyfin.Server.csproj

@@ -50,7 +50,7 @@
     <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
     <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
     <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
     <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
     <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
-    <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.1" />
+    <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
     <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
     <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
     <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
     <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
   </ItemGroup>
   </ItemGroup>

+ 2 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -23,7 +23,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.MigrateUserDb),
             typeof(Routines.MigrateUserDb),
             typeof(Routines.ReaddDefaultPluginRepository),
             typeof(Routines.ReaddDefaultPluginRepository),
-            typeof(Routines.MigrateDisplayPreferencesDb)
+            typeof(Routines.MigrateDisplayPreferencesDb),
+            typeof(Routines.RemoveDownloadImagesInAdvance)
         };
         };
 
 
         /// <summary>
         /// <summary>

+ 46 - 0
Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs

@@ -0,0 +1,46 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
+    /// </summary>
+    internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
+    {
+        private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
+        private readonly ILibraryManager _libraryManager;
+
+        public RemoveDownloadImagesInAdvance(ILogger<RemoveDownloadImagesInAdvance> logger, ILibraryManager libraryManager)
+        {
+            _logger = logger;
+            _libraryManager = libraryManager;
+        }
+
+        /// <inheritdoc/>
+        public Guid Id => Guid.Parse("{A81F75E0-8F43-416F-A5E8-516CCAB4D8CC}");
+
+        /// <inheritdoc/>
+        public string Name => "RemoveDownloadImagesInAdvance";
+
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
+        /// <inheritdoc/>
+        public void Perform()
+        {
+            var virtualFolders = _libraryManager.GetVirtualFolders(false);
+            _logger.LogInformation("Removing 'RemoveDownloadImagesInAdvance' settings in all the libraries");
+            foreach (var virtualFolder in virtualFolders)
+            {
+                var libraryOptions = virtualFolder.LibraryOptions;
+                var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(virtualFolder.ItemId);
+                // The property no longer exists in LibraryOptions, so we just re-save the options to get old data removed.
+                collectionFolder.UpdateLibraryOptions(libraryOptions);
+                _logger.LogInformation("Removed from '{VirtualFolder}'", virtualFolder.Name);
+            }
+        }
+    }
+}

+ 113 - 0
MediaBrowser.Common/Plugins/LocalPlugin.cs

@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace MediaBrowser.Common.Plugins
+{
+    /// <summary>
+    /// Local plugin struct.
+    /// </summary>
+    public class LocalPlugin : IEquatable<LocalPlugin>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalPlugin"/> class.
+        /// </summary>
+        /// <param name="id">The plugin id.</param>
+        /// <param name="name">The plugin name.</param>
+        /// <param name="version">The plugin version.</param>
+        /// <param name="path">The plugin path.</param>
+        public LocalPlugin(Guid id, string name, Version version, string path)
+        {
+            Id = id;
+            Name = name;
+            Version = version;
+            Path = path;
+            DllFiles = new List<string>();
+        }
+
+        /// <summary>
+        /// Gets the plugin id.
+        /// </summary>
+        public Guid Id { get; }
+
+        /// <summary>
+        /// Gets the plugin name.
+        /// </summary>
+        public string Name { get; }
+
+        /// <summary>
+        /// Gets the plugin version.
+        /// </summary>
+        public Version Version { get; }
+
+        /// <summary>
+        /// Gets the plugin path.
+        /// </summary>
+        public string Path { get; }
+
+        /// <summary>
+        /// Gets the list of dll files for this plugin.
+        /// </summary>
+        public List<string> DllFiles { get; }
+
+        /// <summary>
+        /// == operator.
+        /// </summary>
+        /// <param name="left">Left item.</param>
+        /// <param name="right">Right item.</param>
+        /// <returns>Comparison result.</returns>
+        public static bool operator ==(LocalPlugin left, LocalPlugin right)
+        {
+            return left.Equals(right);
+        }
+
+        /// <summary>
+        /// != operator.
+        /// </summary>
+        /// <param name="left">Left item.</param>
+        /// <param name="right">Right item.</param>
+        /// <returns>Comparison result.</returns>
+        public static bool operator !=(LocalPlugin left, LocalPlugin right)
+        {
+            return !left.Equals(right);
+        }
+
+        /// <summary>
+        /// Compare two <see cref="LocalPlugin"/>.
+        /// </summary>
+        /// <param name="a">The first item.</param>
+        /// <param name="b">The second item.</param>
+        /// <returns>Comparison result.</returns>
+        public static int Compare(LocalPlugin a, LocalPlugin b)
+        {
+            var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+            // Id is not equal but name is.
+            if (a.Id != b.Id && compare == 0)
+            {
+                compare = a.Id.CompareTo(b.Id);
+            }
+
+            return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
+        }
+
+        /// <inheritdoc />
+        public override bool Equals(object obj)
+        {
+            return obj is LocalPlugin other && this.Equals(other);
+        }
+
+        /// <inheritdoc />
+        public override int GetHashCode()
+        {
+            return Name.GetHashCode(StringComparison.OrdinalIgnoreCase);
+        }
+
+        /// <inheritdoc />
+        public bool Equals(LocalPlugin other)
+        {
+            return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
+                   && Id.Equals(other.Id);
+        }
+    }
+}

+ 9 - 1
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -6,8 +6,8 @@ using System.Net;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
+using MediaBrowser.Common.Plugins;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
-using Microsoft.AspNetCore.Http;
 
 
 namespace MediaBrowser.Controller
 namespace MediaBrowser.Controller
 {
 {
@@ -120,5 +120,13 @@ namespace MediaBrowser.Controller
         string ExpandVirtualPath(string path);
         string ExpandVirtualPath(string path);
 
 
         string ReverseVirtualPath(string path);
         string ReverseVirtualPath(string path);
+
+        /// <summary>
+        /// Gets the list of local plugins.
+        /// </summary>
+        /// <param name="path">Plugin base directory.</param>
+        /// <param name="cleanup">Cleanup old plugins.</param>
+        /// <returns>Enumerable of local plugins.</returns>
+        IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
     }
     }
 }
 }

+ 5 - 0
MediaBrowser.Controller/Net/AuthorizationInfo.cs

@@ -53,5 +53,10 @@ namespace MediaBrowser.Controller.Net
         /// Gets or sets the user making the request.
         /// Gets or sets the user making the request.
         /// </summary>
         /// </summary>
         public User User { get; set; }
         public User User { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the token is authenticated.
+        /// </summary>
+        public bool IsAuthenticated { get; set; }
     }
     }
 }
 }

+ 5 - 0
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Configuration
 
 
         public string TranscodingTempPath { get; set; }
         public string TranscodingTempPath { get; set; }
 
 
+        public string FallbackFontPath { get; set; }
+
+        public bool EnableFallbackFont { get; set; }
+
         public double DownMixAudioBoost { get; set; }
         public double DownMixAudioBoost { get; set; }
 
 
         public int MaxMuxingQueueSize { get; set; }
         public int MaxMuxingQueueSize { get; set; }
@@ -69,6 +73,7 @@ namespace MediaBrowser.Model.Configuration
 
 
         public EncodingOptions()
         public EncodingOptions()
         {
         {
+            EnableFallbackFont = false;
             DownMixAudioBoost = 2;
             DownMixAudioBoost = 2;
             MaxMuxingQueueSize = 2048;
             MaxMuxingQueueSize = 2048;
             EnableThrottling = false;
             EnableThrottling = false;

+ 0 - 2
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -17,8 +17,6 @@ namespace MediaBrowser.Model.Configuration
 
 
         public bool ExtractChapterImagesDuringLibraryScan { get; set; }
         public bool ExtractChapterImagesDuringLibraryScan { get; set; }
 
 
-        public bool DownloadImagesInAdvance { get; set; }
-
         public MediaPathInfo[] PathInfos { get; set; }
         public MediaPathInfo[] PathInfos { get; set; }
 
 
         public bool SaveLocalMetadata { get; set; }
         public bool SaveLocalMetadata { get; set; }

+ 34 - 0
MediaBrowser.Model/Subtitles/FontFile.cs

@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Model.Subtitles
+{
+    /// <summary>
+    /// Class FontFile.
+    /// </summary>
+    public class FontFile
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the size.
+        /// </summary>
+        /// <value>The size.</value>
+        public long Size { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date created.
+        /// </summary>
+        /// <value>The date created.</value>
+        public DateTime DateCreated { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date modified.
+        /// </summary>
+        /// <value>The date modified.</value>
+        public DateTime DateModified { get; set; }
+    }
+}

+ 2 - 7
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -517,13 +517,8 @@ namespace MediaBrowser.Providers.Manager
                     return true;
                     return true;
                 }
                 }
             }
             }
-
-            if (libraryOptions.DownloadImagesInAdvance)
-            {
-                return false;
-            }
-
-            return true;
+            // We always want to use prefetched images
+            return false;
         }
         }
 
 
         private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls)
         private void SaveImageStub(BaseItem item, ImageType imageType, IEnumerable<string> urls)

+ 7 - 25
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -252,7 +252,13 @@ namespace MediaBrowser.Providers.Manager
 
 
                     if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
                     if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
                     {
                     {
-                        await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false);
+                        personEntity.SetImage(
+                            new ItemImageInfo
+                            {
+                                Path = person.ImageUrl,
+                                Type = ImageType.Primary
+                            },
+                            0);
 
 
                         saveEntity = true;
                         saveEntity = true;
                         updateType |= ItemUpdateType.ImageUpdate;
                         updateType |= ItemUpdateType.ImageUpdate;
@@ -266,30 +272,6 @@ namespace MediaBrowser.Providers.Manager
             }
             }
         }
         }
 
 
-        private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken)
-        {
-            if (libraryOptions.DownloadImagesInAdvance)
-            {
-                try
-                {
-                    await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
-                    return;
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error in AddPersonImage");
-                }
-            }
-
-            personEntity.SetImage(
-                new ItemImageInfo
-                {
-                    Path = imageUrl,
-                    Type = ImageType.Primary
-                },
-                0);
-        }
-
         protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
         protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
         {
         {
             item.AfterMetadataRefresh();
             item.AfterMetadataRefresh();

+ 1 - 9
apiclient/templates/typescript/axios/generate.sh

@@ -1,14 +1,6 @@
 #!/bin/bash
 #!/bin/bash
 
 
 artifactsDirectory="${1}"
 artifactsDirectory="${1}"
-buildNumber="${2}"
-if [[ -n ${buildNumber} ]]; then
-    # Unstable build
-    additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/"
-else
-    # Stable build
-    additionalProperties=""
-fi
 
 
 java -jar openapi-generator-cli.jar generate \
 java -jar openapi-generator-cli.jar generate \
     --input-spec ${artifactsDirectory}/openapispec/openapi.json \
     --input-spec ${artifactsDirectory}/openapispec/openapi.json \
@@ -16,4 +8,4 @@ java -jar openapi-generator-cli.jar generate \
     --output ./apiclient/generated/typescript/axios  \
     --output ./apiclient/generated/typescript/axios  \
     --template-dir ./apiclient/templates/typescript/axios \
     --template-dir ./apiclient/templates/typescript/axios \
     --ignore-file-override ./apiclient/.openapi-generator-ignore \
     --ignore-file-override ./apiclient/.openapi-generator-ignore \
-    --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"${additionalProperties}
+    --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"

+ 3 - 2
tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs

@@ -8,6 +8,7 @@ using Jellyfin.Api.Auth;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
@@ -68,14 +69,14 @@ namespace Jellyfin.Api.Tests.Auth
         }
         }
 
 
         [Fact]
         [Fact]
-        public async Task HandleAuthenticateAsyncShouldFailOnSecurityException()
+        public async Task HandleAuthenticateAsyncShouldFailOnAuthenticationException()
         {
         {
             var errorMessage = _fixture.Create<string>();
             var errorMessage = _fixture.Create<string>();
 
 
             _jellyfinAuthServiceMock.Setup(
             _jellyfinAuthServiceMock.Setup(
                     a => a.Authenticate(
                     a => a.Authenticate(
                         It.IsAny<HttpRequest>()))
                         It.IsAny<HttpRequest>()))
-                .Throws(new SecurityException(errorMessage));
+                .Throws(new AuthenticationException(errorMessage));
 
 
             var authenticateResult = await _sut.AuthenticateAsync();
             var authenticateResult = await _sut.AuthenticateAsync();
 
 

+ 1 - 1
tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj

@@ -18,7 +18,7 @@
     <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

+ 1 - 1
tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj

@@ -13,7 +13,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

+ 1 - 1
tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj

@@ -13,7 +13,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

+ 1 - 1
tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj

@@ -19,7 +19,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

+ 1 - 1
tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj

@@ -13,7 +13,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

+ 1 - 1
tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj

@@ -16,7 +16,7 @@
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="AutoFixture" Version="4.14.0" />
     <PackageReference Include="AutoFixture" Version="4.14.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.14.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="Moq" Version="4.14.7" />
     <PackageReference Include="Moq" Version="4.14.7" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />