Browse Source

Merge remote-tracking branch 'upstream/master' into dotnet-5

crobibero 4 years ago
parent
commit
dae4541bad
74 changed files with 525 additions and 772 deletions
  1. 1 1
      DvdLib/Ifo/Dvd.cs
  2. 1 1
      Emby.Dlna/Didl/Filter.cs
  3. 1 1
      Emby.Dlna/Eventing/DlnaEventManager.cs
  4. 27 0
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  5. 40 33
      Emby.Server.Implementations/ApplicationHost.cs
  6. 35 17
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  7. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  8. 3 3
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  9. 1 1
      Emby.Server.Implementations/Library/LibraryManager.cs
  10. 1 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  11. 1 1
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  12. 2 2
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  13. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  14. 3 3
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  15. 3 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  16. 23 6
      Emby.Server.Implementations/Localization/Core/fil.json
  17. 3 1
      Emby.Server.Implementations/Localization/Core/hu.json
  18. 3 1
      Emby.Server.Implementations/Localization/Core/nl.json
  19. 3 1
      Emby.Server.Implementations/Localization/Core/ro.json
  20. 3 1
      Emby.Server.Implementations/Localization/Core/sr.json
  21. 3 1
      Emby.Server.Implementations/Localization/Core/ta.json
  22. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  23. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  24. 0 135
      Jellyfin.Api/Controllers/AlbumsController.cs
  25. 6 8
      Jellyfin.Api/Controllers/ArtistsController.cs
  26. 6 8
      Jellyfin.Api/Controllers/ChannelsController.cs
  27. 3 3
      Jellyfin.Api/Controllers/FilterController.cs
  28. 3 4
      Jellyfin.Api/Controllers/GenresController.cs
  29. 21 28
      Jellyfin.Api/Controllers/InstantMixController.cs
  30. 4 6
      Jellyfin.Api/Controllers/ItemsController.cs
  31. 65 95
      Jellyfin.Api/Controllers/LibraryController.cs
  32. 16 21
      Jellyfin.Api/Controllers/LiveTvController.cs
  33. 3 3
      Jellyfin.Api/Controllers/MoviesController.cs
  34. 3 4
      Jellyfin.Api/Controllers/MusicGenresController.cs
  35. 2 1
      Jellyfin.Api/Controllers/PackageController.cs
  36. 4 5
      Jellyfin.Api/Controllers/PersonsController.cs
  37. 3 4
      Jellyfin.Api/Controllers/PlaylistsController.cs
  38. 1 1
      Jellyfin.Api/Controllers/RemoteImageController.cs
  39. 3 4
      Jellyfin.Api/Controllers/StudiosController.cs
  40. 1 1
      Jellyfin.Api/Controllers/TrailersController.cs
  41. 10 14
      Jellyfin.Api/Controllers/TvShowsController.cs
  42. 3 4
      Jellyfin.Api/Controllers/UserLibraryController.cs
  43. 1 1
      Jellyfin.Api/Controllers/VideoAttachmentsController.cs
  44. 3 4
      Jellyfin.Api/Controllers/YearsController.cs
  45. 6 40
      Jellyfin.Api/Extensions/DtoExtensions.cs
  46. 1 1
      Jellyfin.Api/Helpers/RequestHelpers.cs
  47. 0 182
      Jellyfin.Api/Helpers/SimilarItemsHelper.cs
  48. 1 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  49. 48 17
      Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
  50. 6 9
      Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
  51. 6 9
      Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
  52. 3 3
      Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
  53. 5 3
      Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
  54. 3 4
      Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
  55. 18 2
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  56. 7 0
      MediaBrowser.Common/Configuration/IConfigurationManager.cs
  57. 26 5
      MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
  58. 7 10
      MediaBrowser.Common/Plugins/BasePlugin.cs
  59. 0 12
      MediaBrowser.Common/Plugins/IPlugin.cs
  60. 19 0
      MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs
  61. 3 2
      MediaBrowser.Controller/Dto/DtoOptions.cs
  62. 5 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  63. 1 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  64. 4 4
      MediaBrowser.Controller/MediaEncoding/JobLogger.cs
  65. 2 2
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  66. 1 1
      MediaBrowser.Model/Dlna/ContainerProfile.cs
  67. 1 1
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  68. 1 1
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  69. 2 1
      MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
  70. 1 1
      MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
  71. 1 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
  72. 4 4
      MediaBrowser.Providers/Subtitles/SubtitleManager.cs
  73. 4 4
      RSSDP/HttpParserBase.cs
  74. 17 16
      tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs

+ 1 - 1
DvdLib/Ifo/Dvd.cs

@@ -31,7 +31,7 @@ namespace DvdLib.Ifo
                         continue;
                     }
 
-                    var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
+                    var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
                     if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
                     {
                         ReadVTS(ifoNumber, ifo.FullName);

+ 1 - 1
Emby.Dlna/Didl/Filter.cs

@@ -18,7 +18,7 @@ namespace Emby.Dlna.Didl
         {
             _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
 
-            _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+            _fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
         }
 
         public bool Contains(string field)

+ 1 - 1
Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -83,7 +83,7 @@ namespace Emby.Dlna.Eventing
             if (!string.IsNullOrEmpty(header))
             {
                 // Starts with SECOND-
-                header = header.Split('-').Last();
+                header = header.Split('-')[^1];
 
                 if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
                 {

+ 27 - 0
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -133,6 +133,33 @@ namespace Emby.Server.Implementations.AppBase
             }
         }
 
+        /// <summary>
+        /// Manually pre-loads a factory so that it is available pre system initialisation.
+        /// </summary>
+        /// <typeparam name="T">Class to register.</typeparam>
+        public virtual void RegisterConfiguration<T>()
+            where T : IConfigurationFactory
+        {
+            IConfigurationFactory factory = Activator.CreateInstance<T>();
+
+            if (_configurationFactories == null)
+            {
+                _configurationFactories = new[] { factory };
+            }
+            else
+            {
+                var oldLen = _configurationFactories.Length;
+                var arr = new IConfigurationFactory[oldLen + 1];
+                _configurationFactories.CopyTo(arr, 0);
+                arr[oldLen] = factory;
+                _configurationFactories = arr;
+            }
+
+            _configurationStores = _configurationFactories
+                .SelectMany(i => i.GetConfigurations())
+                .ToArray();
+        }
+
         /// <summary>
         /// Adds parts.
         /// </summary>

+ 40 - 33
Emby.Server.Implementations/ApplicationHost.cs

@@ -126,7 +126,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private IHttpClientFactory _httpClientFactory;
-
         private string[] _urlPrefixes;
 
         /// <summary>
@@ -497,24 +496,11 @@ namespace Emby.Server.Implementations
                 HttpsPort = ServerConfiguration.DefaultHttpsPort;
             }
 
-            if (Plugins != null)
-            {
-                var pluginBuilder = new StringBuilder();
-
-                foreach (var plugin in Plugins)
-                {
-                    pluginBuilder.Append(plugin.Name)
-                        .Append(' ')
-                        .Append(plugin.Version)
-                        .AppendLine();
-                }
-
-                Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
-            }
-
             DiscoverTypes();
 
             RegisterServices();
+
+            RegisterPluginServices();
         }
 
         /// <summary>
@@ -779,10 +765,24 @@ namespace Emby.Server.Implementations
 
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
             _plugins = GetExports<IPlugin>()
-                        .Select(LoadPlugin)
                         .Where(i => i != null)
                         .ToArray();
 
+            if (Plugins != null)
+            {
+                var pluginBuilder = new StringBuilder();
+
+                foreach (var plugin in Plugins)
+                {
+                    pluginBuilder.Append(plugin.Name)
+                        .Append(' ')
+                        .Append(plugin.Version)
+                        .AppendLine();
+                }
+
+                Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
+            }
+
             _urlPrefixes = GetUrlPrefixes().ToArray();
 
             Resolve<ILibraryManager>().AddParts(
@@ -812,21 +812,6 @@ namespace Emby.Server.Implementations
             Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
         }
 
-        private IPlugin LoadPlugin(IPlugin plugin)
-        {
-            try
-            {
-                plugin.RegisterServices(ServiceCollection);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
-                return null;
-            }
-
-            return plugin;
-        }
-
         /// <summary>
         /// Discovers the types.
         /// </summary>
@@ -837,6 +822,22 @@ namespace Emby.Server.Implementations
             _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
         }
 
+        private void RegisterPluginServices()
+        {
+            foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
+            {
+                try
+                {
+                    var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
+                    instance.RegisterServices(ServiceCollection);
+                }
+                catch (Exception ex)
+                {
+                    Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
+                }
+            }
+        }
+
         private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
         {
             foreach (var ass in assemblies)
@@ -996,6 +997,12 @@ namespace Emby.Server.Implementations
         {
             var minimumVersion = new Version(0, 0, 0, 1);
             var versions = new List<LocalPlugin>();
+            if (!Directory.Exists(path))
+            {
+                // Plugin path doesn't exist, don't try to enumerate subfolders.
+                return Enumerable.Empty<LocalPlugin>();
+            }
+
             var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
 
             foreach (var dir in directories)
@@ -1026,7 +1033,7 @@ namespace Emby.Server.Implementations
                     else
                     {
                         // No metafile, so lets see if the folder is versioned.
-                        metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+                        metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
 
                         int versionIndex = dir.LastIndexOf('_');
                         if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))

+ 35 - 17
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1007,7 +1007,7 @@ namespace Emby.Server.Implementations.Data
                 return;
             }
 
-            var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+            var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
 
             foreach (var part in parts)
             {
@@ -1057,7 +1057,7 @@ namespace Emby.Server.Implementations.Data
                 return;
             }
 
-            var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+            var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
             var list = new List<ItemImageInfo>();
             foreach (var part in parts)
             {
@@ -1096,7 +1096,7 @@ namespace Emby.Server.Implementations.Data
 
         public ItemImageInfo ItemImageInfoFromValueString(string value)
         {
-            var parts = value.Split(new[] { '*' }, StringSplitOptions.None);
+            var parts = value.Split('*', StringSplitOptions.None);
 
             if (parts.Length < 3)
             {
@@ -1532,7 +1532,7 @@ namespace Emby.Server.Implementations.Data
             {
                 if (!reader.IsDBNull(index))
                 {
-                    item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
 
                 index++;
@@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Data
                 {
                     IEnumerable<MetadataField> GetLockedFields(string s)
                     {
-                        foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
+                        foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
                         {
                             if (Enum.TryParse(i, true, out MetadataField parsedValue))
                             {
@@ -1612,7 +1612,7 @@ namespace Emby.Server.Implementations.Data
             {
                 if (!reader.IsDBNull(index))
                 {
-                    item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
 
                 index++;
@@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data
             {
                 if (!reader.IsDBNull(index))
                 {
-                    item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
 
                 index++;
@@ -1636,7 +1636,7 @@ namespace Emby.Server.Implementations.Data
                     {
                         IEnumerable<TrailerType> GetTrailerTypes(string s)
                         {
-                            foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
+                            foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
                             {
                                 if (Enum.TryParse(i, true, out TrailerType parsedValue))
                                 {
@@ -1811,7 +1811,7 @@ namespace Emby.Server.Implementations.Data
             {
                 if (!reader.IsDBNull(index))
                 {
-                    item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
+                    item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
                 }
 
                 index++;
@@ -1848,14 +1848,14 @@ namespace Emby.Server.Implementations.Data
             {
                 if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
                 {
-                    hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
 
                 index++;
 
                 if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
                 {
-                    hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
 
                 index++;
@@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
 
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 {
-                    builder.Append("((OfficialRating is null) * 10)");
+                    builder.Append("(OfficialRating is null * 10)");
                 }
                 else
                 {
-                    builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
+                    builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
                 }
 
                 if (item.ProductionYear.HasValue)
@@ -2416,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
                     builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
                 }
 
-                //// genres, tags
-                builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)");
+                // genres, tags, studios, person, year?
+                builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
+
+                if (item is MusicArtist)
+                {
+                    // Match albums where the artist is AlbumArtist against other albums.
+                    // It is assumed that similar albums => similar artists.
+                    builder.Append(
+                        @"+ (WITH artistValues AS (
+	                            SELECT DISTINCT albumValues.CleanValue
+	                            FROM ItemValues albumValues
+	                            INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
+	                            INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
+                            ), similarArtist AS (
+	                            SELECT albumValues.ItemId
+	                            FROM ItemValues albumValues
+	                            INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
+	                            INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
+                            ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
+                }
 
                 builder.Append(") as SimilarityScore");
 
@@ -5052,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             CheckDisposed();
 
-            var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People";
+            var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
 
             var whereClauses = GetPeopleWhereClauses(query, null);
 
@@ -5593,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 return counts;
             }
 
-            var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
+            var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
                 .ToLookup(x => x);
 
             foreach (var type in allTypes)

+ 1 - 1
Emby.Server.Implementations/Dto/DtoService.cs

@@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Dto
                     continue;
                 }
 
-                var containers = container.Split(new[] { ',' });
+                var containers = container.Split(',');
                 if (containers.Length < 2)
                 {
                     continue;

+ 3 - 3
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -245,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 return null;
             }
 
-            var parts = authorizationHeader.Split(new[] { ' ' }, 2);
+            var parts = authorizationHeader.Split(' ', 2);
 
             // There should be at least to parts
             if (parts.Length != 2)
@@ -269,11 +269,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             foreach (var item in parts)
             {
-                var param = item.Trim().Split(new[] { '=' }, 2);
+                var param = item.Trim().Split('=', 2);
 
                 if (param.Length == 2)
                 {
-                    var value = NormalizeValue(param[1].Trim(new[] { '"' }));
+                    var value = NormalizeValue(param[1].Trim('"'));
                     result[param[0]] = value;
                 }
             }

+ 1 - 1
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -2705,7 +2705,7 @@ namespace Emby.Server.Implementations.Library
 
             var videos = videoListResolver.Resolve(fileSystemChildren);
 
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 
             if (currentVideo != null)
             {

+ 1 - 1
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -849,7 +849,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentException("Key can't be empty.", nameof(key));
             }
 
-            var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
+            var keys = key.Split(LiveStreamIdDelimeter, 2);
 
             var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
 

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -201,7 +201,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                     continue;
                 }
 
-                var firstMedia = resolvedItem.Files.First();
+                var firstMedia = resolvedItem.Files[0];
 
                 var libraryItem = new T
                 {

+ 2 - 2
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1429,7 +1429,7 @@ namespace Emby.Server.Implementations.LiveTv
             return result;
         }
 
-        public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, ItemFields[] fields, User user = null)
+        public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
         {
             var programTuples = new List<Tuple<BaseItemDto, string, string>>();
             var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
@@ -2208,7 +2208,7 @@ namespace Emby.Server.Implementations.LiveTv
         /// <returns>Task.</returns>
         public Task ResetTuner(string id, CancellationToken cancellationToken)
         {
-            var parts = id.Split(new[] { '_' }, 2);
+            var parts = id.Split('_', 2);
 
             var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase));
 

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             if (string.IsNullOrEmpty(currentFile))
             {
-                return (files.Last(), true);
+                return (files[^1], true);
             }
 
             var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;

+ 3 - 3
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
         {
-            var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+            var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
             var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
 
             string numberString = null;
@@ -273,8 +273,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
         {
-            var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-            var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null;
+            var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries);
+            var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null;
 
             // Check for channel number with the format from SatIp
             // #EXTINF:0,84. VOX Schweiz

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

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

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

@@ -1,7 +1,7 @@
 {
     "VersionNumber": "Bersyon {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}",
     "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
     "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
@@ -61,8 +61,8 @@
     "Latest": "Pinakabago",
     "LabelRunningTimeValue": "Oras: {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",
     "HeaderRecordingGroups": "Pagtatalang Grupo",
     "HeaderNextUp": "Susunod",
@@ -90,12 +90,29 @@
     "Application": "Aplikasyon",
     "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
     "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",
     "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
     "TasksChannelsCategory": "Palabas sa internet",
     "TasksLibraryCategory": "Librerya",
     "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/hu.json

@@ -113,5 +113,7 @@
     "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
     "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
     "TaskRefreshChannels": "Csatornák frissítése",
-    "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat."
+    "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
+    "TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
+    "TaskCleanActivityLog": "Tevékenységnapló törlése"
 }

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

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

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

@@ -112,5 +112,7 @@
     "TasksChannelsCategory": "Canale de pe Internet",
     "TasksApplicationCategory": "Aplicație",
     "TasksLibraryCategory": "Librărie",
-    "TasksMaintenanceCategory": "Mentenanță"
+    "TasksMaintenanceCategory": "Mentenanță",
+    "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
+    "TaskCleanActivityLog": "Curăță Jurnalul de Activitate"
 }

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

@@ -112,5 +112,7 @@
     "TasksChannelsCategory": "Интернет канали",
     "TasksApplicationCategory": "Апликација",
     "TasksLibraryCategory": "Библиотека",
-    "TasksMaintenanceCategory": "Одржавање"
+    "TasksMaintenanceCategory": "Одржавање",
+    "TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
+    "TaskCleanActivityLog": "Очисти историју активности"
 }

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

@@ -112,5 +112,7 @@
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
     "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
-    "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
+    "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது",
+    "TaskCleanActivityLogDescription": "உள்ளமைக்கப்பட்ட வயதை விட பழைய செயல்பாட்டு பதிவு உள்ளீடுகளை நீக்குகிறது.",
+    "TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி"
 }

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -653,7 +653,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                     try
                     {
                         _logger.LogInformation(Name + ": Waiting on Task");
-                        var exited = Task.WaitAll(new[] { task }, 2000);
+                        var exited = task.Wait(2000);
 
                         if (exited)
                         {

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 try
                 {
                     previouslyFailedImages = File.ReadAllText(failHistoryPath)
-                        .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
+                        .Split('|', StringSplitOptions.RemoveEmptyEntries)
                         .ToList();
                 }
                 catch (IOException)

+ 0 - 135
Jellyfin.Api/Controllers/AlbumsController.cs

@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers
-{
-    /// <summary>
-    /// The albums controller.
-    /// </summary>
-    [Route("")]
-    public class AlbumsController : BaseJellyfinApiController
-    {
-        private readonly IUserManager _userManager;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IDtoService _dtoService;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AlbumsController"/> class.
-        /// </summary>
-        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
-        /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
-        public AlbumsController(
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService)
-        {
-            _userManager = userManager;
-            _libraryManager = libraryManager;
-            _dtoService = dtoService;
-        }
-
-        /// <summary>
-        /// Finds albums similar to a given album.
-        /// </summary>
-        /// <param name="albumId">The album id.</param>
-        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
-        /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
-        /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <response code="200">Similar albums returned.</response>
-        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
-        [HttpGet("Albums/{albumId}/Similar")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
-            [FromRoute, Required] string albumId,
-            [FromQuery] Guid? userId,
-            [FromQuery] string? excludeArtistIds,
-            [FromQuery] int? limit)
-        {
-            var dtoOptions = new DtoOptions().AddClientFields(Request);
-
-            return SimilarItemsHelper.GetSimilarItemsResult(
-                dtoOptions,
-                _userManager,
-                _libraryManager,
-                _dtoService,
-                userId,
-                albumId,
-                excludeArtistIds,
-                limit,
-                new[] { typeof(MusicAlbum) },
-                GetAlbumSimilarityScore);
-        }
-
-        /// <summary>
-        /// Finds artists similar to a given artist.
-        /// </summary>
-        /// <param name="artistId">The artist id.</param>
-        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
-        /// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
-        /// <param name="limit">Optional. The maximum number of records to return.</param>
-        /// <response code="200">Similar artists returned.</response>
-        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
-        [HttpGet("Artists/{artistId}/Similar")]
-        [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
-            [FromRoute, Required] string artistId,
-            [FromQuery] Guid? userId,
-            [FromQuery] string? excludeArtistIds,
-            [FromQuery] int? limit)
-        {
-            var dtoOptions = new DtoOptions().AddClientFields(Request);
-
-            return SimilarItemsHelper.GetSimilarItemsResult(
-                dtoOptions,
-                _userManager,
-                _libraryManager,
-                _dtoService,
-                userId,
-                artistId,
-                excludeArtistIds,
-                limit,
-                new[] { typeof(MusicArtist) },
-                SimilarItemsHelper.GetSimiliarityScore);
-        }
-
-        /// <summary>
-        /// Gets a similairty score of two albums.
-        /// </summary>
-        /// <param name="item1">The first item.</param>
-        /// <param name="item1People">The item1 people.</param>
-        /// <param name="allPeople">All people.</param>
-        /// <param name="item2">The second item.</param>
-        /// <returns>System.Int32.</returns>
-        private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
-        {
-            var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
-
-            var album1 = (MusicAlbum)item1;
-            var album2 = (MusicAlbum)item2;
-
-            var artists1 = album1
-                .GetAllArtists()
-                .DistinctNames()
-                .ToList();
-
-            var artists2 = new HashSet<string>(
-                album2.GetAllArtists().DistinctNames(),
-                StringComparer.OrdinalIgnoreCase);
-
-            return points + artists1.Where(artists2.Contains).Sum(i => 5);
-        }
-    }
-}

+ 6 - 8
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="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="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="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>
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
@@ -114,8 +114,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .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="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="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="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>
@@ -297,7 +296,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
@@ -323,8 +322,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .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="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="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>
         /// <returns>
         /// A <see cref="Task"/> representing the request to get the channel items.
@@ -124,7 +124,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? sortOrder,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] string? sortBy,
-            [FromQuery] string? fields)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
@@ -137,8 +137,7 @@ namespace Jellyfin.Api.Controllers
                 ChannelIds = new[] { channelId },
                 ParentId = folderId ?? Guid.Empty,
                 OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
-                DtoOptions = new DtoOptions()
-                    .AddItemFields(fields)
+                DtoOptions = new DtoOptions { Fields = fields }
             };
 
             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="limit">Optional. The maximum number of records to return.</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>
         /// <response code="200">Latest channel items returned.</response>
         /// <returns>
@@ -198,7 +197,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? channelIds)
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -214,8 +213,7 @@ namespace Jellyfin.Api.Controllers
                     .Where(i => !string.IsNullOrWhiteSpace(i))
                     .Select(i => new Guid(i))
                     .ToArray(),
-                DtoOptions = new DtoOptions()
-                    .AddItemFields(fields)
+                DtoOptions = new DtoOptions { Fields = fields }
             };
 
             foreach (var filter in filters)

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

@@ -78,8 +78,8 @@ namespace Jellyfin.Api.Controllers
             var query = new InternalItemsQuery
             {
                 User = user,
-                MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
-                IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
+                IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
                 Recursive = true,
                 EnableTotalRecordCount = false,
                 DtoOptions = new DtoOptions
@@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers
             var genreQuery = new InternalItemsQuery(user)
             {
                 IncludeItemTypes =
-                    (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+                    (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
                 DtoOptions = new DtoOptions
                 {
                     Fields = Array.Empty<ItemFields>(),

+ 3 - 4
Jellyfin.Api/Controllers/GenresController.cs

@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</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="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="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>
@@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isFavorite,
@@ -86,8 +86,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 

+ 21 - 28
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -56,7 +56,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -79,8 +79,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@@ -93,7 +92,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -106,7 +105,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -116,8 +115,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
@@ -130,7 +128,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -143,7 +141,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -153,8 +151,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
@@ -167,7 +164,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="name">The genre name.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -180,7 +177,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -189,8 +186,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
@@ -203,7 +199,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -216,7 +212,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -226,8 +222,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@@ -240,7 +235,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -253,7 +248,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -263,8 +258,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@@ -277,7 +271,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="id">The item id.</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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -290,7 +284,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -300,8 +294,7 @@ namespace Jellyfin.Api.Controllers
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
             var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);

+ 4 - 6
Jellyfin.Api/Controllers/ItemsController.cs

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

+ 65 - 95
Jellyfin.Api/Controllers/LibraryController.cs

@@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Progress;
@@ -680,12 +681,12 @@ namespace Jellyfin.Api.Controllers
         /// <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, TrailerUrls.</param>
         /// <response code="200">Similar items returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
-        [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
+        [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
         [HttpGet("Items/{itemId}/Similar")]
-        [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
-        [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
-        [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
-        [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
+        [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
+        [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
+        [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
+        [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@@ -693,7 +694,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? excludeArtistIds,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
-            [FromQuery] string? fields)
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
         {
             var item = itemId.Equals(Guid.Empty)
                 ? (!userId.Equals(Guid.Empty)
@@ -701,33 +702,71 @@ namespace Jellyfin.Api.Controllers
                     : _libraryManager.RootFolder)
                 : _libraryManager.GetItemById(itemId);
 
+            if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
+            {
+                return new QueryResult<BaseItemDto>();
+            }
+
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
+            var dtoOptions = new DtoOptions { Fields = fields }
+                .AddClientFields(Request);
+
             var program = item as IHasProgramAttributes;
-            var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
-            if (program != null && program.IsSeries)
+            bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
+            bool? isSeries = item is Series || (program != null && program.IsSeries);
+
+            var includeItemTypes = new List<string>();
+            if (isMovie.Value)
             {
-                return GetSimilarItemsResult(
-                    item,
-                    excludeArtistIds,
-                    userId,
-                    limit,
-                    fields,
-                    new[] { nameof(Series) },
-                    false);
+                includeItemTypes.Add(nameof(Movie));
+                if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+                {
+                    includeItemTypes.Add(nameof(Trailer));
+                    includeItemTypes.Add(nameof(LiveTvProgram));
+                }
+            }
+            else if (isSeries.Value)
+            {
+                includeItemTypes.Add(nameof(Series));
+            }
+            else
+            {
+                // For non series and movie types these columns are typically null
+                isSeries = null;
+                isMovie = null;
+                includeItemTypes.Add(item.GetType().Name);
             }
 
-            if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
+            var query = new InternalItemsQuery(user)
             {
-                return new QueryResult<BaseItemDto>();
+                Limit = limit,
+                IncludeItemTypes = includeItemTypes.ToArray(),
+                IsMovie = isMovie,
+                IsSeries = isSeries,
+                SimilarTo = item,
+                DtoOptions = dtoOptions,
+                EnableTotalRecordCount = !isMovie ?? true,
+                EnableGroupByMetadataKey = isMovie ?? false,
+                MinSimilarityScore = 2 // A remnant from album/artist scoring
+            };
+
+            // ExcludeArtistIds
+            if (!string.IsNullOrEmpty(excludeArtistIds))
+            {
+                query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
             }
 
-            return GetSimilarItemsResult(
-                item,
-                excludeArtistIds,
-                userId,
-                limit,
-                fields,
-                new[] { item.GetType().Name },
-                isMovie);
+            List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+
+            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = returnList,
+                TotalRecordCount = itemsResult.Count
+            };
         }
 
         /// <summary>
@@ -880,75 +919,6 @@ namespace Jellyfin.Api.Controllers
             }
         }
 
-        private QueryResult<BaseItemDto> GetSimilarItemsResult(
-            BaseItem item,
-            string? excludeArtistIds,
-            Guid? userId,
-            int? limit,
-            string? fields,
-            string[] includeItemTypes,
-            bool isMovie)
-        {
-            var user = userId.HasValue && !userId.Equals(Guid.Empty)
-                ? _userManager.GetUserById(userId.Value)
-                : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
-                .AddClientFields(Request);
-
-            var query = new InternalItemsQuery(user)
-            {
-                Limit = limit,
-                IncludeItemTypes = includeItemTypes,
-                IsMovie = isMovie,
-                SimilarTo = item,
-                DtoOptions = dtoOptions,
-                EnableTotalRecordCount = !isMovie,
-                EnableGroupByMetadataKey = isMovie
-            };
-
-            // ExcludeArtistIds
-            if (!string.IsNullOrEmpty(excludeArtistIds))
-            {
-                query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
-            }
-
-            List<BaseItem> itemsResult;
-
-            if (isMovie)
-            {
-                var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
-                if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
-                {
-                    itemTypes.Add(nameof(Trailer));
-                    itemTypes.Add(nameof(LiveTvProgram));
-                }
-
-                query.IncludeItemTypes = itemTypes.ToArray();
-                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
-            }
-            else if (item is MusicArtist)
-            {
-                query.IncludeItemTypes = Array.Empty<string>();
-
-                itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
-            }
-            else
-            {
-                itemsResult = _libraryManager.GetItemList(query);
-            }
-
-            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
-
-            var result = new QueryResult<BaseItemDto>
-            {
-                Items = returnList,
-                TotalRecordCount = itemsResult.Count
-            };
-
-            return result;
-        }
-
         private static string[] GetRepresentativeItemTypes(string? contentType)
         {
             return contentType switch

+ 16 - 21
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -119,7 +119,7 @@ namespace Jellyfin.Api.Controllers
         /// <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="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="sortBy">Optional. Key to sort by.</param>
         /// <param name="sortOrder">Optional. Sort order.</param>
@@ -148,15 +148,14 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] string? sortBy,
             [FromQuery] SortOrder? sortOrder,
             [FromQuery] bool enableFavoriteSorting = false,
             [FromQuery] bool addCurrentProgram = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
@@ -240,7 +239,7 @@ namespace Jellyfin.Api.Controllers
         /// <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="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="isMovie">Optional. Filter for movies.</param>
         /// <param name="isSeries">Optional. Filter for series.</param>
@@ -265,7 +264,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool? isMovie,
             [FromQuery] bool? isSeries,
@@ -275,8 +274,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? isLibraryItem,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
@@ -297,7 +295,7 @@ namespace Jellyfin.Api.Controllers
                 IsKids = isKids,
                 IsSports = isSports,
                 IsLibraryItem = isLibraryItem,
-                Fields = RequestHelpers.GetItemFields(fields),
+                Fields = fields,
                 ImageTypeLimit = imageTypeLimit,
                 EnableImages = enableImages
             }, dtoOptions);
@@ -317,7 +315,7 @@ namespace Jellyfin.Api.Controllers
         /// <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="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="enableTotalRecordCount">Optional. Return total record count.</param>
         /// <response code="200">Live tv recordings returned.</response>
@@ -352,7 +350,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -531,7 +529,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableUserData">Optional. Include user data.</param>
         /// <param name="seriesTimerId">Optional. Filter by series timer 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>
         /// <response code="200">Live tv epgs returned.</response>
         /// <returns>
@@ -566,7 +564,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableUserData,
             [FromQuery] string? seriesTimerId,
             [FromQuery] Guid? librarySeriesId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool enableTotalRecordCount = true)
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -607,8 +605,7 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
@@ -663,8 +660,7 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(body.Fields)
+            var dtoOptions = new DtoOptions { Fields = body.Fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
             return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
@@ -686,7 +682,7 @@ namespace Jellyfin.Api.Controllers
         /// <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="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="enableTotalRecordCount">Retrieve total record count.</param>
         /// <response code="200">Recommended epgs returned.</response>
@@ -708,7 +704,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] string? genreIds,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
         {
@@ -730,8 +726,7 @@ namespace Jellyfin.Api.Controllers
                 GenreIds = RequestHelpers.GetGuids(genreIds)
             };
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
             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 Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Extensions;
@@ -65,15 +66,14 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
             [FromQuery] Guid? userId,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] int categoryLimit = 5,
             [FromQuery] int itemLimit = 8)
         {
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request);
 
             var categories = new List<RecommendationDto>();

+ 3 - 4
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</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="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="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>
@@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isFavorite,
@@ -86,8 +86,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 

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

@@ -154,12 +154,13 @@ namespace Jellyfin.Api.Controllers
         /// <param name="repositoryInfos">The list of package repositories.</param>
         /// <response code="204">Package repositories saved.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpOptions("Repositories")]
+        [HttpPost("Repositories")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
         {
             _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
+            _serverConfigurationManager.SaveConfiguration();
             return NoContent();
         }
     }

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

@@ -53,8 +53,8 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <param name="limit">Optional. The maximum number of records to return.</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="enableUserData">Optional, include user data.</param>
         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetPersons(
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,
             [FromQuery] bool? enableUserData,
@@ -83,8 +83,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] bool? enableImages = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 

+ 3 - 4
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -135,7 +135,7 @@ namespace Jellyfin.Api.Controllers
         /// <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="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="enableUserData">Optional. Include user data.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -149,7 +149,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, Required] Guid userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? enableImages,
             [FromQuery] bool? enableUserData,
             [FromQuery] int? imageTypeLimit,
@@ -177,8 +177,7 @@ namespace Jellyfin.Api.Controllers
                 items = items.Take(limit.Value).ToArray();
             }
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 

+ 1 - 1
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
                 throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
             }
 
-            var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
+            var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
             var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");

+ 3 - 4
Jellyfin.Api/Controllers/StudiosController.cs

@@ -50,7 +50,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</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="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="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>
@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] string? searchTerm,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isFavorite,
@@ -86,8 +86,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableImages = true,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 

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

@@ -146,7 +146,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? searchTerm,
             [FromQuery] string? sortOrder,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
             [FromQuery] bool? isFavorite,

+ 10 - 14
Jellyfin.Api/Controllers/TvShowsController.cs

@@ -59,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="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="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="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>
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? seriesId,
             [FromQuery] string? parentId,
             [FromQuery] bool? enableImges,
@@ -83,8 +83,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? enableUserData,
             [FromQuery] bool enableTotalRecordCount = true)
         {
-            var options = new DtoOptions()
-                .AddItemFields(fields!)
+            var options = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
 
@@ -119,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="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="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="enableImges">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@@ -132,7 +131,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] Guid? userId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? parentId,
             [FromQuery] bool? enableImges,
             [FromQuery] int? imageTypeLimit,
@@ -147,8 +146,7 @@ namespace Jellyfin.Api.Controllers
 
             var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
 
-            var options = new DtoOptions()
-                .AddItemFields(fields!)
+            var options = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
 
@@ -198,7 +196,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
             [FromRoute, Required] string seriesId,
             [FromQuery] Guid? userId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] int? season,
             [FromQuery] string? seasonId,
             [FromQuery] bool? isMissing,
@@ -218,8 +216,7 @@ namespace Jellyfin.Api.Controllers
 
             List<BaseItem> episodes;
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields!)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 
@@ -321,7 +318,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
             [FromRoute, Required] string seriesId,
             [FromQuery] Guid? userId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,
             [FromQuery] string? adjacentTo,
@@ -346,8 +343,7 @@ namespace Jellyfin.Api.Controllers
                 AdjacentTo = adjacentTo
             });
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
 

+ 3 - 4
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <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="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="isPlayed">Filter by items that are played, or not.</param>
         /// <param name="enableImages">Optional. include image information in output.</param>
@@ -268,7 +268,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
             [FromRoute, Required] Guid userId,
             [FromQuery] Guid? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? includeItemTypes,
             [FromQuery] bool? isPlayed,
             [FromQuery] bool? enableImages,
@@ -288,8 +288,7 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 

+ 1 - 1
Jellyfin.Api/Controllers/VideoAttachmentsController.cs

@@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public async Task<ActionResult<FileStreamResult>> GetAttachment(
+        public async Task<ActionResult> GetAttachment(
             [FromRoute, Required] Guid videoId,
             [FromRoute, Required] string mediaSourceId,
             [FromRoute, Required] int index)

+ 3 - 4
Jellyfin.Api/Controllers/YearsController.cs

@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</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="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="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>
@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery] string? sortOrder,
             [FromQuery] string? parentId,
-            [FromQuery] string? fields,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? excludeItemTypes,
             [FromQuery] string? includeItemTypes,
             [FromQuery] string? mediaTypes,
@@ -84,8 +84,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool recursive = true,
             [FromQuery] bool? enableImages = true)
         {
-            var dtoOptions = new DtoOptions()
-                .AddItemFields(fields)
+            var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 

+ 6 - 40
Jellyfin.Api/Extensions/DtoExtensions.cs

@@ -1,6 +1,8 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -13,42 +15,6 @@ namespace Jellyfin.Api.Extensions
     /// </summary>
     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>
         /// Add additional fields depending on client.
         /// </summary>
@@ -79,7 +45,7 @@ namespace Jellyfin.Api.Extensions
                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    int oldLen = dtoOptions.Fields.Length;
+                    int oldLen = dtoOptions.Fields.Count;
                     var arr = new ItemFields[oldLen + 1];
                     dtoOptions.Fields.CopyTo(arr, 0);
                     arr[oldLen] = ItemFields.RecursiveItemCount;
@@ -97,7 +63,7 @@ namespace Jellyfin.Api.Extensions
                     client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
                     client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    int oldLen = dtoOptions.Fields.Length;
+                    int oldLen = dtoOptions.Fields.Count;
                     var arr = new ItemFields[oldLen + 1];
                     dtoOptions.Fields.CopyTo(arr, 0);
                     arr[oldLen] = ItemFields.ChildCount;
@@ -126,7 +92,7 @@ namespace Jellyfin.Api.Extensions
             bool? enableImages,
             bool? enableUserData,
             int? imageTypeLimit,
-            ImageType[] enableImageTypes)
+            IReadOnlyList<ImageType> enableImageTypes)
         {
             dtoOptions.EnableImages = enableImages ?? true;
 
@@ -140,7 +106,7 @@ namespace Jellyfin.Api.Extensions
                 dtoOptions.EnableUserData = enableUserData.Value;
             }
 
-            if (enableImageTypes.Length != 0)
+            if (enableImageTypes.Count != 0)
             {
                 dtoOptions.ImageTypes = enableImageTypes;
             }

+ 1 - 1
Jellyfin.Api/Helpers/RequestHelpers.cs

@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Helpers
             }
 
             return removeEmpty
-                ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
+                ? value.Split(separator, StringSplitOptions.RemoveEmptyEntries)
                 : value.Split(separator);
         }
 

+ 0 - 182
Jellyfin.Api/Helpers/SimilarItemsHelper.cs

@@ -1,182 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
-
-namespace Jellyfin.Api.Helpers
-{
-    /// <summary>
-    /// The similar items helper class.
-    /// </summary>
-    public static class SimilarItemsHelper
-    {
-        internal static QueryResult<BaseItemDto> GetSimilarItemsResult(
-            DtoOptions dtoOptions,
-            IUserManager userManager,
-            ILibraryManager libraryManager,
-            IDtoService dtoService,
-            Guid? userId,
-            string id,
-            string? excludeArtistIds,
-            int? limit,
-            Type[] includeTypes,
-            Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
-        {
-            var user = userId.HasValue && !userId.Equals(Guid.Empty)
-                ? userManager.GetUserById(userId.Value)
-                : null;
-
-            var item = string.IsNullOrEmpty(id) ?
-                (!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
-                libraryManager.RootFolder) : libraryManager.GetItemById(id);
-
-            var query = new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
-                Recursive = true,
-                DtoOptions = dtoOptions,
-                ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds)
-            };
-
-            var inputItems = libraryManager.GetItemList(query);
-
-            var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)
-                .ToList();
-
-            var returnItems = items;
-
-            if (limit.HasValue && limit < returnItems.Count)
-            {
-                returnItems = returnItems.GetRange(0, limit.Value);
-            }
-
-            var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
-
-            return new QueryResult<BaseItemDto>
-            {
-                Items = dtos,
-                TotalRecordCount = items.Count
-            };
-        }
-
-        /// <summary>
-        /// Gets the similaritems.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="inputItems">The input items.</param>
-        /// <param name="getSimilarityScore">The get similarity score.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        private static IEnumerable<BaseItem> GetSimilaritems(
-            BaseItem item,
-            ILibraryManager libraryManager,
-            IEnumerable<BaseItem> inputItems,
-            Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
-        {
-            var itemId = item.Id;
-            inputItems = inputItems.Where(i => i.Id != itemId);
-            var itemPeople = libraryManager.GetPeople(item);
-            var allPeople = libraryManager.GetPeople(new InternalPeopleQuery
-            {
-                AppearsInItemId = item.Id
-            });
-
-            return inputItems.Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, itemPeople, allPeople, i)))
-                .Where(i => i.Item2 > 2)
-                .OrderByDescending(i => i.Item2)
-                .Select(i => i.Item1);
-        }
-
-        private static IEnumerable<string> GetTags(BaseItem item)
-        {
-            return item.Tags;
-        }
-
-        /// <summary>
-        /// Gets the similiarity score.
-        /// </summary>
-        /// <param name="item1">The item1.</param>
-        /// <param name="item1People">The item1 people.</param>
-        /// <param name="allPeople">All people.</param>
-        /// <param name="item2">The item2.</param>
-        /// <returns>System.Int32.</returns>
-        internal static int GetSimiliarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
-        {
-            var points = 0;
-
-            if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
-            {
-                points += 10;
-            }
-
-            // Find common genres
-            points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-
-            // Find common tags
-            points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
-
-            // Find common studios
-            points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
-
-            var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id)
-                .Select(i => i.Name)
-                .Where(i => !string.IsNullOrWhiteSpace(i))
-                .DistinctNames()
-                .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
-
-            points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
-            {
-                if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 5;
-                }
-
-                if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-
-                if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-
-                if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-
-                if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 2;
-                }
-
-                return 1;
-            });
-
-            if (item1.ProductionYear.HasValue && item2.ProductionYear.HasValue)
-            {
-                var diff = Math.Abs(item1.ProductionYear.Value - item2.ProductionYear.Value);
-
-                // Add if they came out within the same decade
-                if (diff < 10)
-                {
-                    points += 2;
-                }
-
-                // And more if within five years
-                if (diff < 5)
-                {
-                    points += 2;
-                }
-            }
-
-            return points;
-        }
-    }
-}

+ 1 - 1
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Helpers
                 throw new ResourceNotFoundException(nameof(httpRequest.Path));
             }
 
-            var url = httpRequest.Path.Value.Split('.').Last();
+            var url = httpRequest.Path.Value.Split('.')[^1];
 
             if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
             {

+ 48 - 17
Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs

@@ -1,7 +1,9 @@
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.ModelBinders
 {
@@ -11,6 +13,17 @@ namespace Jellyfin.Api.ModelBinders
     /// </summary>
     public class CommaDelimitedArrayModelBinder : IModelBinder
     {
+        private readonly ILogger<CommaDelimitedArrayModelBinder> _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
+        /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
+        public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
+        {
+            _logger = logger;
+        }
+
         /// <inheritdoc/>
         public Task BindModelAsync(ModelBindingContext bindingContext)
         {
@@ -20,16 +33,8 @@ namespace Jellyfin.Api.ModelBinders
 
             if (valueProviderResult.Length > 1)
             {
-                var result = Array.CreateInstance(elementType, valueProviderResult.Length);
-
-                for (int i = 0; i < valueProviderResult.Length; i++)
-                {
-                    var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim());
-
-                    result.SetValue(value, i);
-                }
-
-                bindingContext.Result = ModelBindingResult.Success(result);
+                var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
+                bindingContext.Result = ModelBindingResult.Success(typedValues);
             }
             else
             {
@@ -37,13 +42,8 @@ namespace Jellyfin.Api.ModelBinders
 
                 if (value != null)
                 {
-                    var values = Array.ConvertAll(
-                        value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries),
-                        x => converter.ConvertFromString(x?.Trim()));
-
-                    var typedValues = Array.CreateInstance(elementType, values.Length);
-                    values.CopyTo(typedValues, 0);
-
+                    var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
+                    var typedValues = GetParsedResult(splitValues, elementType, converter);
                     bindingContext.Result = ModelBindingResult.Success(typedValues);
                 }
                 else
@@ -55,5 +55,36 @@ namespace Jellyfin.Api.ModelBinders
 
             return Task.CompletedTask;
         }
+
+        private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
+        {
+            var parsedValues = new object?[values.Count];
+            var convertedCount = 0;
+            for (var i = 0; i < values.Count; i++)
+            {
+                try
+                {
+                    parsedValues[i] = converter.ConvertFromString(values[i].Trim());
+                    convertedCount++;
+                }
+                catch (FormatException e)
+                {
+                    _logger.LogWarning(e, "Error converting value.");
+                }
+            }
+
+            var typedValues = Array.CreateInstance(elementType, convertedCount);
+            var typedValueIndex = 0;
+            for (var i = 0; i < parsedValues.Length; i++)
+            {
+                if (parsedValues[i] != null)
+                {
+                    typedValues.SetValue(parsedValues[i], typedValueIndex);
+                    typedValueIndex++;
+                }
+            }
+
+            return typedValues;
+        }
     }
 }

+ 6 - 9
Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs

@@ -1,4 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
+using System;
+using System.Collections.Generic;
 
 namespace Jellyfin.Api.Models.LibraryDtos
 {
@@ -10,25 +11,21 @@ namespace Jellyfin.Api.Models.LibraryDtos
         /// <summary>
         /// Gets or sets the metadata savers.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataSavers", Justification = "Imported from ServiceStack")]
-        public LibraryOptionInfoDto[] MetadataSavers { get; set; } = null!;
+        public IReadOnlyList<LibraryOptionInfoDto> MetadataSavers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
 
         /// <summary>
         /// Gets or sets the metadata readers.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataReaders", Justification = "Imported from ServiceStack")]
-        public LibraryOptionInfoDto[] MetadataReaders { get; set; } = null!;
+        public IReadOnlyList<LibraryOptionInfoDto> MetadataReaders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
 
         /// <summary>
         /// Gets or sets the subtitle fetchers.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SubtitleFetchers", Justification = "Imported from ServiceStack")]
-        public LibraryOptionInfoDto[] SubtitleFetchers { get; set; } = null!;
+        public IReadOnlyList<LibraryOptionInfoDto> SubtitleFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
 
         /// <summary>
         /// Gets or sets the type options.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "TypeOptions", Justification = "Imported from ServiceStack")]
-        public LibraryTypeOptionsDto[] TypeOptions { get; set; } = null!;
+        public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
     }
 }

+ 6 - 9
Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs

@@ -1,4 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
+using System;
+using System.Collections.Generic;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 
@@ -17,25 +18,21 @@ namespace Jellyfin.Api.Models.LibraryDtos
         /// <summary>
         /// Gets or sets the metadata fetchers.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataFetchers", Justification = "Imported from ServiceStack")]
-        public LibraryOptionInfoDto[] MetadataFetchers { get; set; } = null!;
+        public IReadOnlyList<LibraryOptionInfoDto> MetadataFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
 
         /// <summary>
         /// Gets or sets the image fetchers.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "ImageFetchers", Justification = "Imported from ServiceStack")]
-        public LibraryOptionInfoDto[] ImageFetchers { get; set; } = null!;
+        public IReadOnlyList<LibraryOptionInfoDto> ImageFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
 
         /// <summary>
         /// Gets or sets the supported image types.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SupportedImageTypes", Justification = "Imported from ServiceStack")]
-        public ImageType[] SupportedImageTypes { get; set; } = null!;
+        public IReadOnlyList<ImageType> SupportedImageTypes { get; set; } = Array.Empty<ImageType>();
 
         /// <summary>
         /// Gets or sets the default image options.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "DefaultImageOptions", Justification = "Imported from ServiceStack")]
-        public ImageOption[] DefaultImageOptions { get; set; } = null!;
+        public IReadOnlyList<ImageOption> DefaultImageOptions { get; set; } = Array.Empty<ImageOption>();
     }
 }

+ 3 - 3
Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
@@ -25,8 +26,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// <summary>
         /// Gets or sets list of mappings.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "Mappings", Justification = "Imported from ServiceStack")]
-        public NameValuePair[] Mappings { get; set; } = null!;
+        public IReadOnlyList<NameValuePair> Mappings { get; set; } = Array.Empty<NameValuePair>();
 
         /// <summary>
         /// Gets or sets provider name.

+ 5 - 3
Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs

@@ -1,8 +1,10 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Text.Json.Serialization;
 using MediaBrowser.Common.Json.Converters;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 
 namespace Jellyfin.Api.Models.LiveTvDtos
 {
@@ -142,8 +144,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// Optional.
         /// </summary>
         [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "EnableImageTypes", Justification = "Imported from ServiceStack")]
-        public ImageType[] EnableImageTypes { get; set; } = Array.Empty<ImageType>();
+        public IReadOnlyList<ImageType> EnableImageTypes { get; set; } = Array.Empty<ImageType>();
 
         /// <summary>
         /// Gets or sets include user data.
@@ -167,6 +168,7 @@ 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.
         /// Optional.
         /// </summary>
-        public string? Fields { get; set; }
+        [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+        public IReadOnlyList<ItemFields> Fields { get; set; } = Array.Empty<ItemFields>();
     }
 }

+ 3 - 4
Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs

@@ -1,4 +1,5 @@
-using System.Diagnostics.CodeAnalysis;
+using System;
+using System.Collections.Generic;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.MediaInfo;
 
@@ -17,8 +18,6 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
         /// <summary>
         /// Gets or sets the device play protocols.
         /// </summary>
-        [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
-        [SuppressMessage("Microsoft.Performance", "SA1011:ClosingBracketsSpace", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
-        public MediaProtocol[]? DirectPlayProtocols { get; set; }
+        public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; } = Array.Empty<MediaProtocol>();
     }
 }

+ 18 - 2
Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs

@@ -9,6 +9,7 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Server.Implementations;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
@@ -26,6 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
         private readonly IServerApplicationPaths _paths;
         private readonly JellyfinDbProvider _provider;
         private readonly JsonSerializerOptions _jsonOptions;
+        private readonly IUserManager _userManager;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
@@ -33,11 +35,17 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <param name="logger">The logger.</param>
         /// <param name="paths">The server application paths.</param>
         /// <param name="provider">The database provider.</param>
-        public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
+        /// <param name="userManager">The user manager.</param>
+        public MigrateDisplayPreferencesDb(
+            ILogger<MigrateDisplayPreferencesDb> logger,
+            IServerApplicationPaths paths,
+            JellyfinDbProvider provider,
+            IUserManager userManager)
         {
             _logger = logger;
             _paths = paths;
             _provider = provider;
+            _userManager = userManager;
             _jsonOptions = new JsonSerializerOptions();
             _jsonOptions.Converters.Add(new JsonStringEnumConverter());
         }
@@ -86,11 +94,19 @@ namespace Jellyfin.Server.Migrations.Routines
                         continue;
                     }
 
+                    var dtoUserId = new Guid(result[1].ToBlob());
+                    var existingUser = _userManager.GetUserById(dtoUserId);
+                    if (existingUser == null)
+                    {
+                        _logger.LogWarning("User with ID {UserId} does not exist in the database, skipping migration.", dtoUserId);
+                        continue;
+                    }
+
                     var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
                         ? chromecastDict[version]
                         : ChromecastVersion.Stable;
 
-                    var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())
+                    var displayPreferences = new DisplayPreferences(dtoUserId, result[2].ToString())
                     {
                         IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
                         ShowBackdrop = dto.ShowBackdrop,

+ 7 - 0
MediaBrowser.Common/Configuration/IConfigurationManager.cs

@@ -46,6 +46,13 @@ namespace MediaBrowser.Common.Configuration
         /// <param name="newConfiguration">The new configuration.</param>
         void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
 
+        /// <summary>
+        /// Manually pre-loads a factory so that it is available pre system initialisation.
+        /// </summary>
+        /// <typeparam name="T">Class to register.</typeparam>
+        void RegisterConfiguration<T>()
+            where T : IConfigurationFactory;
+
         /// <summary>
         /// Gets the configuration.
         /// </summary>

+ 26 - 5
MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs

@@ -26,19 +26,40 @@ namespace MediaBrowser.Common.Json.Converters
         {
             if (reader.TokenType == JsonTokenType.String)
             {
-                var stringEntries = reader.GetString()?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+                var stringEntries = reader.GetString()?.Split(',', StringSplitOptions.RemoveEmptyEntries);
                 if (stringEntries == null || stringEntries.Length == 0)
                 {
                     return Array.Empty<T>();
                 }
 
-                var entries = new T[stringEntries.Length];
+                var parsedValues = new object[stringEntries.Length];
+                var convertedCount = 0;
                 for (var i = 0; i < stringEntries.Length; i++)
                 {
-                    entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim());
+                    try
+                    {
+                        parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
+                        convertedCount++;
+                    }
+                    catch (FormatException)
+                    {
+                        // TODO log when upgraded to .Net5
+                        // _logger.LogWarning(e, "Error converting value.");
+                    }
                 }
 
-                return entries;
+                var typedValues = new T[convertedCount];
+                var typedValueIndex = 0;
+                for (var i = 0; i < stringEntries.Length; i++)
+                {
+                    if (parsedValues[i] != null)
+                    {
+                        typedValues.SetValue(parsedValues[i], typedValueIndex);
+                        typedValueIndex++;
+                    }
+                }
+
+                return typedValues;
             }
 
             return JsonSerializer.Deserialize<T[]>(ref reader, options);
@@ -50,4 +71,4 @@ namespace MediaBrowser.Common.Json.Converters
             JsonSerializer.Serialize(writer, value, options);
         }
     }
-}
+}

+ 7 - 10
MediaBrowser.Common/Plugins/BasePlugin.cs

@@ -83,16 +83,6 @@ namespace MediaBrowser.Common.Plugins
         {
         }
 
-        /// <inheritdoc />
-        public virtual void RegisterServices(IServiceCollection serviceCollection)
-        {
-        }
-
-        /// <inheritdoc />
-        public virtual void UnregisterServices(IServiceCollection serviceCollection)
-        {
-        }
-
         /// <inheritdoc />
         public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
         {
@@ -185,6 +175,11 @@ namespace MediaBrowser.Common.Plugins
         /// <value>The type of the configuration.</value>
         public Type ConfigurationType => typeof(TConfigurationType);
 
+        /// <summary>
+        /// Gets or sets the event handler that is triggered when this configuration changes.
+        /// </summary>
+        public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
+
         /// <summary>
         /// Gets the name the assembly file.
         /// </summary>
@@ -280,6 +275,8 @@ namespace MediaBrowser.Common.Plugins
             Configuration = (TConfigurationType)configuration;
 
             SaveConfiguration();
+
+            ConfigurationChanged.Invoke(this, configuration);
         }
 
         /// <inheritdoc />

+ 0 - 12
MediaBrowser.Common/Plugins/IPlugin.cs

@@ -62,18 +62,6 @@ namespace MediaBrowser.Common.Plugins
         /// Called when just before the plugin is uninstalled from the server.
         /// </summary>
         void OnUninstalling();
-
-        /// <summary>
-        /// Registers the plugin's services to the service collection.
-        /// </summary>
-        /// <param name="serviceCollection">The service collection.</param>
-        void RegisterServices(IServiceCollection serviceCollection);
-
-        /// <summary>
-        /// Unregisters the plugin's services from the service collection.
-        /// </summary>
-        /// <param name="serviceCollection">The service collection.</param>
-        void UnregisterServices(IServiceCollection serviceCollection);
     }
 
     public interface IHasPluginConfiguration

+ 19 - 0
MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs

@@ -0,0 +1,19 @@
+namespace MediaBrowser.Common.Plugins
+{
+    using Microsoft.Extensions.DependencyInjection;
+
+    /// <summary>
+    /// Defines the <see cref="IPluginServiceRegistrator" />.
+    /// </summary>
+    public interface IPluginServiceRegistrator
+    {
+        /// <summary>
+        /// Registers the plugin's services with the service collection.
+        /// </summary>
+        /// <remarks>
+        /// This interface is only used for service registration and requires a parameterless constructor.
+        /// </remarks>
+        /// <param name="serviceCollection">The service collection.</param>
+        void RegisterServices(IServiceCollection serviceCollection);
+    }
+}

+ 3 - 2
MediaBrowser.Controller/Dto/DtoOptions.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -15,9 +16,9 @@ namespace MediaBrowser.Controller.Dto
             ItemFields.RefreshState
         };
 
-        public ItemFields[] Fields { get; set; }
+        public IReadOnlyList<ItemFields> Fields { get; set; }
 
-        public ImageType[] ImageTypes { get; set; }
+        public IReadOnlyList<ImageType> ImageTypes { get; set; }
 
         public int ImageTypeLimit { get; set; }
 

+ 5 - 1
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -87,6 +87,8 @@ namespace MediaBrowser.Controller.Entities
         public const string InterviewFolderName = "interviews";
         public const string SceneFolderName = "scenes";
         public const string SampleFolderName = "samples";
+        public const string ShortsFolderName = "shorts";
+        public const string FeaturettesFolderName = "featurettes";
 
         public static readonly string[] AllExtrasTypesFolderNames = {
             ExtrasFolderName,
@@ -94,7 +96,9 @@ namespace MediaBrowser.Controller.Entities
             DeletedScenesFolderName,
             InterviewFolderName,
             SceneFolderName,
-            SampleFolderName
+            SampleFolderName,
+            ShortsFolderName,
+            FeaturettesFolderName
         };
 
         [JsonIgnore]

+ 1 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -225,7 +225,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="fields">The fields.</param>
         /// <param name="user">The user.</param>
         /// <returns>Task.</returns>
-        Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, User user = null);
+        Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
 
         /// <summary>
         /// Saves the tuner host.

+ 4 - 4
MediaBrowser.Controller/MediaEncoding/JobLogger.cs

@@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase))
                 {
-                    var rate = part.Split(new[] { '=' }, 2)[^1];
+                    var rate = part.Split('=', 2)[^1];
 
                     if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
                     {
@@ -103,7 +103,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 else if (state.RunTimeTicks.HasValue &&
                     part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
                 {
-                    var time = part.Split(new[] { '=' }, 2).Last();
+                    var time = part.Split('=', 2)[^1];
 
                     if (TimeSpan.TryParse(time, _usCulture, out var val))
                     {
@@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
                 {
-                    var size = part.Split(new[] { '=' }, 2).Last();
+                    var size = part.Split('=', 2)[^1];
 
                     int? scale = null;
                     if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
@@ -135,7 +135,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
                 {
-                    var rate = part.Split(new[] { '=' }, 2).Last();
+                    var rate = part.Split('=', 2)[^1];
 
                     int? scale = null;
                     if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)

+ 2 - 2
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -149,7 +149,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                 var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC");
                 if (!string.IsNullOrWhiteSpace(iTunEXTC))
                 {
-                    var parts = iTunEXTC.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    var parts = iTunEXTC.Split('|', StringSplitOptions.RemoveEmptyEntries);
                     // Example
                     // mpaa|G|100|For crude humor
                     if (parts.Length > 1)
@@ -1139,7 +1139,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                 return null;
             }
 
-            return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
+            return value.Split('/', StringSplitOptions.RemoveEmptyEntries)
                 .Select(i => i.Trim())
                 .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
         }

+ 1 - 1
MediaBrowser.Model/Dlna/ContainerProfile.cs

@@ -34,7 +34,7 @@ namespace MediaBrowser.Model.Dlna
                 return Array.Empty<string>();
             }
 
-            return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+            return value.Split(',', StringSplitOptions.RemoveEmptyEntries);
         }
 
         public bool ContainsContainer(string container)

+ 1 - 1
MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs

@@ -186,7 +186,7 @@ namespace MediaBrowser.Model.Dlna
 
             if (mediaProfile != null && !string.IsNullOrEmpty(mediaProfile.OrgPn))
             {
-                orgPnValues.AddRange(mediaProfile.OrgPn.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
+                orgPnValues.AddRange(mediaProfile.OrgPn.Split(',', StringSplitOptions.RemoveEmptyEntries));
             }
             else
             {

+ 1 - 1
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -1647,7 +1647,7 @@ namespace MediaBrowser.Model.Dlna
 
                                 // strip spaces to avoid having to encode
                                 var values = value
-                                    .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                                    .Split('|', StringSplitOptions.RemoveEmptyEntries);
 
                                 if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
                                 {

+ 2 - 1
MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs

@@ -2,6 +2,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 using MediaBrowser.Model.Dlna;
 
 namespace MediaBrowser.Model.MediaInfo
@@ -55,6 +56,6 @@ namespace MediaBrowser.Model.MediaInfo
 
         public bool EnableDirectStream { get; set; }
 
-        public MediaProtocol[] DirectPlayProtocols { get; set; }
+        public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; }
     }
 }

+ 1 - 1
MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

@@ -391,7 +391,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
                 item.Genres = Array.Empty<string>();
 
                 foreach (var genre in result.Genre
-                    .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                    .Split(',', StringSplitOptions.RemoveEmptyEntries)
                     .Select(i => i.Trim())
                     .Where(i => !string.IsNullOrWhiteSpace(i)))
                 {

+ 1 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs

@@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                 _logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id);
             }
 
-            return result?.Data.First().Id.ToString();
+            return result?.Data[0].Id.ToString(CultureInfo.InvariantCulture);
         }
 
         /// <summary>

+ 4 - 4
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -147,7 +147,7 @@ namespace MediaBrowser.Providers.Subtitles
             string subtitleId,
             CancellationToken cancellationToken)
         {
-            var parts = subtitleId.Split(new[] { '_' }, 2);
+            var parts = subtitleId.Split('_', 2);
             var provider = GetProvider(parts[0]);
 
             try
@@ -329,7 +329,7 @@ namespace MediaBrowser.Providers.Subtitles
                 Index = index,
                 ItemId = item.Id,
                 Type = MediaStreamType.Subtitle
-            }).First();
+            })[0];
 
             var path = stream.Path;
             _monitor.ReportFileSystemChangeBeginning(path);
@@ -349,10 +349,10 @@ namespace MediaBrowser.Providers.Subtitles
         /// <inheritdoc />
         public Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken)
         {
-            var parts = id.Split(new[] { '_' }, 2);
+            var parts = id.Split('_', 2);
 
             var provider = GetProvider(parts[0]);
-            id = parts.Last();
+            id = parts[^1];
 
             return provider.GetSubtitles(id, cancellationToken);
         }

+ 4 - 4
RSSDP/HttpParserBase.cs

@@ -119,7 +119,7 @@ namespace Rssdp.Infrastructure
             }
             else
             {
-                headersToAddTo.TryAddWithoutValidation(headerName, values.First());
+                headersToAddTo.TryAddWithoutValidation(headerName, values[0]);
             }
         }
 
@@ -151,7 +151,7 @@ namespace Rssdp.Infrastructure
             return lineIndex;
         }
 
-        private IList<string> ParseValues(string headerValue)
+        private List<string> ParseValues(string headerValue)
         {
             // This really should be better and match the HTTP 1.1 spec,
             // but this should actually be good enough for SSDP implementations
@@ -160,7 +160,7 @@ namespace Rssdp.Infrastructure
 
             if (headerValue == "\"\"")
             {
-                values.Add(String.Empty);
+                values.Add(string.Empty);
                 return values;
             }
 
@@ -172,7 +172,7 @@ namespace Rssdp.Infrastructure
             else
             {
                 var segments = headerValue.Split(SeparatorCharacters);
-                if (headerValue.Contains("\""))
+                if (headerValue.Contains('"'))
                 {
                     for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
                     {

+ 17 - 16
tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.ModelBinders;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.Extensions.Primitives;
 using Moq;
 using Xunit;
@@ -21,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
             var queryParamString = "lol,xd";
             var queryParamType = typeof(string[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
                     new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -46,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
             var queryParamString = "42,0";
             var queryParamType = typeof(int[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
                     new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -71,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
             var queryParamString = "How,Much";
             var queryParamType = typeof(TestType[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
                     new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -96,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
             var queryParamString = "How,,Much";
             var queryParamType = typeof(TestType[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
                     new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -122,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
             var queryParamString2 = "Much";
             var queryParamType = typeof(TestType[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
 
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
@@ -150,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
             IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
             var queryParamType = typeof(TestType[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
 
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
@@ -172,13 +173,13 @@ namespace Jellyfin.Api.Tests.ModelBinders
         }
 
         [Fact]
-        public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid()
+        public async Task BindModelAsync_EnumArrayQuery_BindValidOnly()
         {
             var queryParamName = "test";
             var queryParamString = "🔥,😢";
             var queryParamType = typeof(TestType[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
                     new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -189,20 +190,20 @@ namespace Jellyfin.Api.Tests.ModelBinders
             bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
             bindingContextMock.SetupProperty(b => b.Result);
 
-            Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
-
-            await Assert.ThrowsAsync<FormatException>(act);
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Empty((TestType[])bindingContextMock.Object.Result.Model);
         }
 
         [Fact]
-        public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2()
+        public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2()
         {
             var queryParamName = "test";
             var queryParamString1 = "How";
             var queryParamString2 = "😱";
             var queryParamType = typeof(TestType[]);
 
-            var modelBinder = new CommaDelimitedArrayModelBinder();
+            var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
 
             var valueProvider = new QueryStringValueProvider(
                     new BindingSource(string.Empty, string.Empty, false, false),
@@ -217,9 +218,9 @@ namespace Jellyfin.Api.Tests.ModelBinders
             bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
             bindingContextMock.SetupProperty(b => b.Result);
 
-            Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
-
-            await Assert.ThrowsAsync<FormatException>(act);
+            await modelBinder.BindModelAsync(bindingContextMock.Object);
+            Assert.True(bindingContextMock.Object.Result.IsModelSet);
+            Assert.Single((TestType[])bindingContextMock.Object.Result.Model);
         }
     }
 }