浏览代码

Merge remote-tracking branch 'upstream/master' into bye-tvdb

crobibero 4 年之前
父节点
当前提交
4b15284324
共有 100 个文件被更改,包括 487 次插入290 次删除
  1. 1 1
      .ci/azure-pipelines-abi.yml
  2. 1 1
      .ci/azure-pipelines-main.yml
  3. 2 2
      .ci/azure-pipelines-test.yml
  4. 1 1
      .ci/azure-pipelines.yml
  5. 2 2
      Dockerfile
  6. 2 2
      Dockerfile.arm
  7. 2 2
      Dockerfile.arm64
  8. 1 1
      DvdLib/DvdLib.csproj
  9. 1 1
      DvdLib/Ifo/Dvd.cs
  10. 1 1
      Emby.Dlna/Didl/Filter.cs
  11. 1 1
      Emby.Dlna/Emby.Dlna.csproj
  12. 1 1
      Emby.Dlna/Eventing/DlnaEventManager.cs
  13. 1 1
      Emby.Drawing/Emby.Drawing.csproj
  14. 1 1
      Emby.Naming/Emby.Naming.csproj
  15. 1 1
      Emby.Notifications/Emby.Notifications.csproj
  16. 5 5
      Emby.Notifications/NotificationEntryPoint.cs
  17. 1 1
      Emby.Photos/Emby.Photos.csproj
  18. 27 0
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  19. 4 2
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  20. 40 33
      Emby.Server.Implementations/ApplicationHost.cs
  21. 2 1
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  22. 12 12
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  23. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  24. 8 6
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  25. 3 3
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  26. 1 1
      Emby.Server.Implementations/Library/LibraryManager.cs
  27. 1 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  28. 1 1
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  29. 2 2
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  30. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  31. 3 3
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  32. 3 1
      Emby.Server.Implementations/Localization/Core/hu.json
  33. 3 1
      Emby.Server.Implementations/Localization/Core/ro.json
  34. 3 1
      Emby.Server.Implementations/Localization/Core/sr.json
  35. 3 1
      Emby.Server.Implementations/Localization/Core/ta.json
  36. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  37. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  38. 3 2
      Emby.Server.Implementations/Session/WebSocketController.cs
  39. 5 3
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  40. 6 0
      Jellyfin.Api/Controllers/EnvironmentController.cs
  41. 3 3
      Jellyfin.Api/Controllers/FilterController.cs
  42. 1 1
      Jellyfin.Api/Controllers/GenresController.cs
  43. 3 1
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  44. 1 1
      Jellyfin.Api/Controllers/ImageByNameController.cs
  45. 2 1
      Jellyfin.Api/Controllers/ImageController.cs
  46. 10 2
      Jellyfin.Api/Controllers/ItemLookupController.cs
  47. 8 8
      Jellyfin.Api/Controllers/LibraryController.cs
  48. 1 1
      Jellyfin.Api/Controllers/LiveTvController.cs
  49. 2 2
      Jellyfin.Api/Controllers/MusicGenresController.cs
  50. 7 1
      Jellyfin.Api/Controllers/PackageController.cs
  51. 14 6
      Jellyfin.Api/Controllers/RemoteImageController.cs
  52. 1 1
      Jellyfin.Api/Controllers/SearchController.cs
  53. 1 1
      Jellyfin.Api/Controllers/VideoAttachmentsController.cs
  54. 3 1
      Jellyfin.Api/Controllers/VideoHlsController.cs
  55. 6 4
      Jellyfin.Api/Extensions/DtoExtensions.cs
  56. 8 1
      Jellyfin.Api/Helpers/AudioHelper.cs
  57. 8 3
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  58. 2 2
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  59. 3 3
      Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
  60. 5 0
      Jellyfin.Api/Helpers/HlsHelpers.cs
  61. 6 0
      Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
  62. 1 1
      Jellyfin.Api/Helpers/RequestHelpers.cs
  63. 5 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  64. 10 9
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  65. 5 3
      Jellyfin.Api/Jellyfin.Api.csproj
  66. 48 17
      Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
  67. 6 9
      Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
  68. 6 9
      Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
  69. 3 3
      Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs
  70. 3 4
      Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
  71. 3 4
      Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
  72. 2 2
      Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
  73. 1 1
      Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
  74. 1 1
      Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
  75. 3 3
      Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
  76. 7 7
      Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
  77. 3 3
      Jellyfin.Data/Jellyfin.Data.csproj
  78. 1 1
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  79. 7 4
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  80. 4 4
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  81. 2 1
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  82. 4 4
      Jellyfin.Server/Filters/FileResponseFilter.cs
  83. 2 1
      Jellyfin.Server/Formatters/CssOutputFormatter.cs
  84. 2 1
      Jellyfin.Server/Formatters/XmlOutputFormatter.cs
  85. 5 5
      Jellyfin.Server/Jellyfin.Server.csproj
  86. 18 2
      Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
  87. 7 0
      MediaBrowser.Common/Configuration/IConfigurationManager.cs
  88. 26 5
      MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
  89. 3 3
      MediaBrowser.Common/MediaBrowser.Common.csproj
  90. 7 10
      MediaBrowser.Common/Plugins/BasePlugin.cs
  91. 0 12
      MediaBrowser.Common/Plugins/IPlugin.cs
  92. 19 0
      MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs
  93. 3 2
      MediaBrowser.Controller/Dto/DtoOptions.cs
  94. 5 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  95. 1 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  96. 3 3
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  97. 4 4
      MediaBrowser.Controller/MediaEncoding/JobLogger.cs
  98. 1 1
      MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
  99. 1 1
      MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
  100. 1 1
      MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

+ 1 - 1
.ci/azure-pipelines-abi.yml

@@ -7,7 +7,7 @@ parameters:
   default: "ubuntu-latest"
   default: "ubuntu-latest"
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
   type: string
   type: string
-  default: 3.1.100
+  default: 5.0.100
 
 
 jobs:
 jobs:
   - job: CompatibilityCheck
   - job: CompatibilityCheck

+ 1 - 1
.ci/azure-pipelines-main.yml

@@ -1,7 +1,7 @@
 parameters:
 parameters:
   LinuxImage: 'ubuntu-latest'
   LinuxImage: 'ubuntu-latest'
   RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
   RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
-  DotNetSdkVersion: 3.1.100
+  DotNetSdkVersion: 5.0.100
 
 
 jobs:
 jobs:
   - job: Build
   - job: Build

+ 2 - 2
.ci/azure-pipelines-test.yml

@@ -10,7 +10,7 @@ parameters:
   default: "tests/**/*Tests.csproj"
   default: "tests/**/*Tests.csproj"
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
   type: string
   type: string
-  default: 3.1.100
+  default: 5.0.100
 
 
 jobs:
 jobs:
   - job: Test
   - job: Test
@@ -94,5 +94,5 @@ jobs:
         displayName: 'Publish OpenAPI Artifact'
         displayName: 'Publish OpenAPI Artifact'
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
         inputs:
         inputs:
-          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/netcoreapp3.1/openapi.json"
+          targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json"
           artifactName: 'OpenAPI Spec'
           artifactName: 'OpenAPI Spec'

+ 1 - 1
.ci/azure-pipelines.yml

@@ -6,7 +6,7 @@ variables:
 - name: RestoreBuildProjects
 - name: RestoreBuildProjects
   value: 'Jellyfin.Server/Jellyfin.Server.csproj'
   value: 'Jellyfin.Server/Jellyfin.Server.csproj'
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
-  value: 3.1.100
+  value: 5.0.100
 
 
 pr:
 pr:
   autoCancel: true
   autoCancel: true

+ 2 - 2
Dockerfile

@@ -1,4 +1,4 @@
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
 
 
 FROM node:alpine as web-builder
 FROM node:alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 ARG JELLYFIN_WEB_VERSION=master
@@ -8,7 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && yarn install \
  && yarn install \
  && mv dist /dist
  && mv dist /dist
 
 
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster as builder
 WORKDIR /repo
 WORKDIR /repo
 COPY . .
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

+ 2 - 2
Dockerfile.arm

@@ -2,7 +2,7 @@
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
 
 
 
 
 FROM node:alpine as web-builder
 FROM node:alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && mv dist /dist
  && mv dist /dist
 
 
 
 
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 WORKDIR /repo
 COPY . .
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

+ 2 - 2
Dockerfile.arm64

@@ -2,7 +2,7 @@
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=3.1
+ARG DOTNET_VERSION=5.0
 
 
 
 
 FROM node:alpine as web-builder
 FROM node:alpine as web-builder
@@ -14,7 +14,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && mv dist /dist
  && mv dist /dist
 
 
 
 
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 WORKDIR /repo
 COPY . .
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1

+ 1 - 1
DvdLib/DvdLib.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
DvdLib/Ifo/Dvd.cs

@@ -31,7 +31,7 @@ namespace DvdLib.Ifo
                         continue;
                         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))
                     if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
                     {
                     {
                         ReadVTS(ifoNumber, ifo.FullName);
                         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);
             _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)
         public bool Contains(string field)

+ 1 - 1
Emby.Dlna/Emby.Dlna.csproj

@@ -17,7 +17,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

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

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

+ 1 - 1
Emby.Drawing/Emby.Drawing.csproj

@@ -6,7 +6,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
Emby.Naming/Emby.Naming.csproj

@@ -6,7 +6,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
Emby.Notifications/Emby.Notifications.csproj

@@ -6,7 +6,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 5 - 5
Emby.Notifications/NotificationEntryPoint.cs

@@ -83,7 +83,7 @@ namespace Emby.Notifications
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
-        private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
+        private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
         {
         {
             var type = NotificationType.ServerRestartRequired.ToString();
             var type = NotificationType.ServerRestartRequired.ToString();
 
 
@@ -99,7 +99,7 @@ namespace Emby.Notifications
             await SendNotification(notification, null).ConfigureAwait(false);
             await SendNotification(notification, null).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
         {
         {
             var entry = e.Argument;
             var entry = e.Argument;
 
 
@@ -132,7 +132,7 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
         }
 
 
-        private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
+        private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
         {
         {
             if (!_appHost.HasUpdateAvailable)
             if (!_appHost.HasUpdateAvailable)
             {
             {
@@ -151,7 +151,7 @@ namespace Emby.Notifications
             await SendNotification(notification, null).ConfigureAwait(false);
             await SendNotification(notification, null).ConfigureAwait(false);
         }
         }
 
 
-        private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
         {
         {
             if (!FilterItem(e.Item))
             if (!FilterItem(e.Item))
             {
             {
@@ -197,7 +197,7 @@ namespace Emby.Notifications
             return item.SourceType == SourceType.Library;
             return item.SourceType == SourceType.Library;
         }
         }
 
 
-        private async void LibraryUpdateTimerCallback(object state)
+        private async void LibraryUpdateTimerCallback(object? state)
         {
         {
             List<BaseItem> items;
             List<BaseItem> items;
 
 

+ 1 - 1
Emby.Photos/Emby.Photos.csproj

@@ -19,7 +19,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 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>
         /// <summary>
         /// Adds parts.
         /// Adds parts.
         /// </summary>
         /// </summary>

+ 4 - 2
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -3,6 +3,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 
 
 namespace Emby.Server.Implementations.AppBase
 namespace Emby.Server.Implementations.AppBase
@@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.AppBase
             }
             }
             catch (Exception)
             catch (Exception)
             {
             {
-                configuration = Activator.CreateInstance(type);
+                configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
             }
             }
 
 
             using var stream = new MemoryStream(buffer?.Length ?? 0);
             using var stream = new MemoryStream(buffer?.Length ?? 0);
@@ -48,8 +49,9 @@ namespace Emby.Server.Implementations.AppBase
             // If the file didn't exist before, or if something has changed, re-save
             // If the file didn't exist before, or if something has changed, re-save
             if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
             if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
             {
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
+                var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
 
 
+                Directory.CreateDirectory(directory);
                 // Save it after load in case we got new items
                 // Save it after load in case we got new items
                 using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
                 using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
                 {
                 {

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

@@ -125,7 +125,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder _mediaEncoder;
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private ISessionManager _sessionManager;
         private IHttpClientFactory _httpClientFactory;
         private IHttpClientFactory _httpClientFactory;
-
         private string[] _urlPrefixes;
         private string[] _urlPrefixes;
 
 
         /// <summary>
         /// <summary>
@@ -496,24 +495,11 @@ namespace Emby.Server.Implementations
                 HttpsPort = ServerConfiguration.DefaultHttpsPort;
                 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();
             DiscoverTypes();
 
 
             RegisterServices();
             RegisterServices();
+
+            RegisterPluginServices();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -777,10 +763,24 @@ namespace Emby.Server.Implementations
 
 
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
             _plugins = GetExports<IPlugin>()
             _plugins = GetExports<IPlugin>()
-                        .Select(LoadPlugin)
                         .Where(i => i != null)
                         .Where(i => i != null)
                         .ToArray();
                         .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();
             _urlPrefixes = GetUrlPrefixes().ToArray();
 
 
             Resolve<ILibraryManager>().AddParts(
             Resolve<ILibraryManager>().AddParts(
@@ -810,21 +810,6 @@ namespace Emby.Server.Implementations
             Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
             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>
         /// <summary>
         /// Discovers the types.
         /// Discovers the types.
         /// </summary>
         /// </summary>
@@ -835,6 +820,22 @@ namespace Emby.Server.Implementations
             _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
             _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)
         private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
         {
         {
             foreach (var ass in assemblies)
             foreach (var ass in assemblies)
@@ -994,6 +995,12 @@ namespace Emby.Server.Implementations
         {
         {
             var minimumVersion = new Version(0, 0, 0, 1);
             var minimumVersion = new Version(0, 0, 0, 1);
             var versions = new List<LocalPlugin>();
             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);
             var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
 
 
             foreach (var dir in directories)
             foreach (var dir in directories)
@@ -1024,7 +1031,7 @@ namespace Emby.Server.Implementations
                     else
                     else
                     {
                     {
                         // No metafile, so lets see if the folder is versioned.
                         // 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('_');
                         int versionIndex = dir.LastIndexOf('_');
                         if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
                         if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))

+ 2 - 1
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -3,6 +3,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Cryptography;
 using static MediaBrowser.Common.Cryptography.Constants;
 using static MediaBrowser.Common.Cryptography.Constants;
 
 
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Cryptography
                 throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
                 throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
             }
             }
 
 
-            using var h = HashAlgorithm.Create(hashMethod);
+            using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
             if (salt.Length == 0)
             if (salt.Length == 0)
             {
             {
                 return h.ComputeHash(bytes);
                 return h.ComputeHash(bytes);

+ 12 - 12
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1007,7 +1007,7 @@ namespace Emby.Server.Implementations.Data
                 return;
                 return;
             }
             }
 
 
-            var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+            var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
 
 
             foreach (var part in parts)
             foreach (var part in parts)
             {
             {
@@ -1057,7 +1057,7 @@ namespace Emby.Server.Implementations.Data
                 return;
                 return;
             }
             }
 
 
-            var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+            var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries);
             var list = new List<ItemImageInfo>();
             var list = new List<ItemImageInfo>();
             foreach (var part in parts)
             foreach (var part in parts)
             {
             {
@@ -1096,7 +1096,7 @@ namespace Emby.Server.Implementations.Data
 
 
         public ItemImageInfo ItemImageInfoFromValueString(string value)
         public ItemImageInfo ItemImageInfoFromValueString(string value)
         {
         {
-            var parts = value.Split(new[] { '*' }, StringSplitOptions.None);
+            var parts = value.Split('*', StringSplitOptions.None);
 
 
             if (parts.Length < 3)
             if (parts.Length < 3)
             {
             {
@@ -1532,7 +1532,7 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 if (!reader.IsDBNull(index))
                 if (!reader.IsDBNull(index))
                 {
                 {
-                    item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
                 }
 
 
                 index++;
                 index++;
@@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Data
                 {
                 {
                     IEnumerable<MetadataField> GetLockedFields(string s)
                     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))
                             if (Enum.TryParse(i, true, out MetadataField parsedValue))
                             {
                             {
@@ -1612,7 +1612,7 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 if (!reader.IsDBNull(index))
                 if (!reader.IsDBNull(index))
                 {
                 {
-                    item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
                 }
 
 
                 index++;
                 index++;
@@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 if (!reader.IsDBNull(index))
                 if (!reader.IsDBNull(index))
                 {
                 {
-                    item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                    item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries);
                 }
                 }
 
 
                 index++;
                 index++;
@@ -1636,7 +1636,7 @@ namespace Emby.Server.Implementations.Data
                     {
                     {
                         IEnumerable<TrailerType> GetTrailerTypes(string s)
                         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))
                                 if (Enum.TryParse(i, true, out TrailerType parsedValue))
                                 {
                                 {
@@ -1811,7 +1811,7 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 if (!reader.IsDBNull(index))
                 if (!reader.IsDBNull(index))
                 {
                 {
-                    item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
+                    item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray();
                 }
                 }
 
 
                 index++;
                 index++;
@@ -1848,14 +1848,14 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 if (item is IHasArtist hasArtists && !reader.IsDBNull(index))
                 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++;
                 index++;
 
 
                 if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(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++;
                 index++;
@@ -5611,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 return counts;
                 return counts;
             }
             }
 
 
-            var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
+            var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries)
                 .ToLookup(x => x);
                 .ToLookup(x => x);
 
 
             foreach (var type in allTypes)
             foreach (var type in allTypes)

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

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

+ 8 - 6
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -32,13 +32,13 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
     <PackageReference Include="Mono.Nat" Version="3.0.0" />
     <PackageReference Include="Mono.Nat" Version="3.0.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
-    <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
+    <PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" />
     <PackageReference Include="sharpcompress" Version="0.26.0" />
     <PackageReference Include="sharpcompress" Version="0.26.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.0" />
@@ -49,10 +49,12 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+    <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
+    <NoWarn>AD0001</NoWarn>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

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

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

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

@@ -1429,7 +1429,7 @@ namespace Emby.Server.Implementations.LiveTv
             return result;
             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 programTuples = new List<Tuple<BaseItemDto, string, string>>();
             var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
             var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
@@ -2208,7 +2208,7 @@ namespace Emby.Server.Implementations.LiveTv
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public Task ResetTuner(string id, CancellationToken cancellationToken)
         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));
             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))
             if (string.IsNullOrEmpty(currentFile))
             {
             {
-                return (files.Last(), true);
+                return (files[^1], true);
             }
             }
 
 
             var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
             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)
         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;
             var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
 
 
             string numberString = null;
             string numberString = null;
@@ -273,8 +273,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
         private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
         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
             // Check for channel number with the format from SatIp
             // #EXTINF:0,84. VOX Schweiz
             // #EXTINF:0,84. VOX Schweiz

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

@@ -113,5 +113,7 @@
     "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
     "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
     "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
     "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
     "TaskRefreshChannels": "Csatornák frissítése",
     "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/ro.json

@@ -112,5 +112,7 @@
     "TasksChannelsCategory": "Canale de pe Internet",
     "TasksChannelsCategory": "Canale de pe Internet",
     "TasksApplicationCategory": "Aplicație",
     "TasksApplicationCategory": "Aplicație",
     "TasksLibraryCategory": "Librărie",
     "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": "Интернет канали",
     "TasksChannelsCategory": "Интернет канали",
     "TasksApplicationCategory": "Апликација",
     "TasksApplicationCategory": "Апликација",
     "TasksLibraryCategory": "Библиотека",
     "TasksLibraryCategory": "Библиотека",
-    "TasksMaintenanceCategory": "Одржавање"
+    "TasksMaintenanceCategory": "Одржавање",
+    "TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.",
+    "TaskCleanActivityLog": "Очисти историју активности"
 }
 }

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

@@ -112,5 +112,7 @@
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
     "HomeVideos": "முகப்பு வீடியோக்கள்",
     "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
     "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
                     try
                     {
                     {
                         _logger.LogInformation(Name + ": Waiting on Task");
                         _logger.LogInformation(Name + ": Waiting on Task");
-                        var exited = Task.WaitAll(new[] { task }, 2000);
+                        var exited = task.Wait(2000);
 
 
                         if (exited)
                         if (exited)
                         {
                         {

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

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

+ 3 - 2
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -8,6 +8,7 @@ using System.Linq;
 using System.Net.WebSockets;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
@@ -55,9 +56,9 @@ namespace Emby.Server.Implementations.Session
             connection.Closed += OnConnectionClosed;
             connection.Closed += OnConnectionClosed;
         }
         }
 
 
-        private void OnConnectionClosed(object sender, EventArgs e)
+        private void OnConnectionClosed(object? sender, EventArgs e)
         {
         {
-            var connection = (IWebSocketConnection)sender;
+            var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
             _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
             _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
             _sockets.Remove(connection);
             _sockets.Remove(connection);
             connection.Closed -= OnConnectionClosed;
             connection.Closed -= OnConnectionClosed;

+ 5 - 3
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -14,6 +14,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
@@ -1347,7 +1348,9 @@ namespace Jellyfin.Api.Controllers
 
 
             var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
             var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
 
 
-            var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
+            var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+
+            var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
 
 
             var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
             var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
             if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
@@ -1565,8 +1568,7 @@ namespace Jellyfin.Api.Controllers
 
 
         private string GetSegmentPath(StreamState state, string playlist, int index)
         private string GetSegmentPath(StreamState state, string playlist, int index)
         {
         {
-            var folder = Path.GetDirectoryName(playlist);
-
+            var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
             var filename = Path.GetFileNameWithoutExtension(playlist);
             var filename = Path.GetFileNameWithoutExtension(playlist);
 
 
             return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));
             return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));

+ 6 - 0
Jellyfin.Api/Controllers/EnvironmentController.cs

@@ -5,6 +5,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.EnvironmentDtos;
 using Jellyfin.Api.Models.EnvironmentDtos;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
@@ -103,6 +104,11 @@ namespace Jellyfin.Api.Controllers
 
 
                 if (validatePathDto.ValidateWritable)
                 if (validatePathDto.ValidateWritable)
                 {
                 {
+                    if (validatePathDto.Path == null)
+                    {
+                        throw new ResourceNotFoundException(nameof(validatePathDto.Path));
+                    }
+
                     var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
                     var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
                     try
                     try
                     {
                     {

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

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

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

@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
             return _dtoService.GetBaseItemDto(item, dtoOptions);
             return _dtoService.GetBaseItemDto(item, dtoOptions);
         }
         }
 
 
-        private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
             where T : BaseItem, new()
             where T : BaseItem, new()
         {
         {
             var result = libraryManager.GetItemList(new InternalItemsQuery
             var result = libraryManager.GetItemList(new InternalItemsQuery

+ 3 - 1
Jellyfin.Api/Controllers/HlsSegmentController.cs

@@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -134,7 +135,8 @@ namespace Jellyfin.Api.Controllers
             var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
             var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
                 .FirstOrDefault(i =>
                 .FirstOrDefault(i =>
                     string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
                     string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
-                    && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
+                    && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
+                ?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid.");
 
 
             return GetFileResult(file, playlistPath);
             return GetFileResult(file, playlistPath);
         }
         }

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

@@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="theme">Theme to search.</param>
         /// <param name="theme">Theme to search.</param>
         /// <param name="name">File name to search for.</param>
         /// <param name="name">File name to search for.</param>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
-        private ActionResult GetImageFile(string basePath, string? theme, string? name)
+        private ActionResult GetImageFile(string basePath, string theme, string? name)
         {
         {
             var themeFolder = Path.Combine(basePath, theme);
             var themeFolder = Path.Combine(basePath, theme);
             if (Directory.Exists(themeFolder))
             if (Directory.Exists(themeFolder))

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

@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Net.Mime;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
@@ -1268,7 +1269,7 @@ namespace Jellyfin.Api.Controllers
                 Response.Headers.Add(key, value);
                 Response.Headers.Add(key, value);
             }
             }
 
 
-            Response.ContentType = imageContentType;
+            Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
             Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
             Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
             Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
             Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
 
 

+ 10 - 2
Jellyfin.Api/Controllers/ItemLookupController.cs

@@ -334,10 +334,16 @@ namespace Jellyfin.Api.Controllers
         private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
         private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
         {
         {
             using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
             using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
+            if (result.Content.Headers.ContentType?.MediaType == null)
+            {
+                throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
+            }
+
             var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
             var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
 
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+            var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
+            Directory.CreateDirectory(directory);
             using (var stream = result.Content)
             using (var stream = result.Content)
             {
             {
                 await using var fileStream = new FileStream(
                 await using var fileStream = new FileStream(
@@ -351,7 +357,9 @@ namespace Jellyfin.Api.Controllers
                 await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                 await stream.CopyToAsync(fileStream).ConfigureAwait(false);
             }
             }
 
 
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+            var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
+
+            Directory.CreateDirectory(pointerCacheDirectory);
             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
         }
         }
 
 

+ 8 - 8
Jellyfin.Api/Controllers/LibraryController.cs

@@ -456,7 +456,7 @@ namespace Jellyfin.Api.Controllers
                 : null;
                 : null;
 
 
             var dtoOptions = new DtoOptions().AddClientFields(Request);
             var dtoOptions = new DtoOptions().AddClientFields(Request);
-            BaseItem parent = item.GetParent();
+            BaseItem? parent = item.GetParent();
 
 
             while (parent != null)
             while (parent != null)
             {
             {
@@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers
 
 
                 baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
                 baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
 
 
-                parent = parent.GetParent();
+                parent = parent?.GetParent();
             }
             }
 
 
             return baseItemDtos;
             return baseItemDtos;
@@ -681,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>
         /// <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>
         /// <response code="200">Similar items returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
-        [HttpGet("Artists/{itemId}/Similar")]
+        [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
         [HttpGet("Items/{itemId}/Similar")]
         [HttpGet("Items/{itemId}/Similar")]
-        [HttpGet("Albums/{itemId}/Similar")]
-        [HttpGet("Shows/{itemId}/Similar")]
-        [HttpGet("Movies/{itemId}/Similar")]
-        [HttpGet("Trailers/{itemId}/Similar")]
+        [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)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@@ -893,7 +893,7 @@ namespace Jellyfin.Api.Controllers
             return _libraryManager.GetItemsResult(query).TotalRecordCount;
             return _libraryManager.GetItemsResult(query).TotalRecordCount;
         }
         }
 
 
-        private BaseItem TranslateParentItem(BaseItem item, User user)
+        private BaseItem? TranslateParentItem(BaseItem item, User user)
         {
         {
             return item.GetParent() is AggregateFolder
             return item.GetParent() is AggregateFolder
                 ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
                 ? _libraryManager.GetUserRootFolder().GetChildren(user, true)

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

@@ -1073,7 +1073,7 @@ namespace Jellyfin.Api.Controllers
             var client = _httpClientFactory.CreateClient(NamedClient.Default);
             var client = _httpClientFactory.CreateClient(NamedClient.Default);
             // https://json.schedulesdirect.org/20141201/available/countries
             // https://json.schedulesdirect.org/20141201/available/countries
             // Can't dispose the response as it's required up the call chain.
             // Can't dispose the response as it's required up the call chain.
-            var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")
+            var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
 
 
             return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
             return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);

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

@@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
         {
         {
             var dtoOptions = new DtoOptions().AddClientFields(Request);
             var dtoOptions = new DtoOptions().AddClientFields(Request);
 
 
-            MusicGenre item;
+            MusicGenre? item;
 
 
             if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
             if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
             {
             {
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
             return _dtoService.GetBaseItemDto(item, dtoOptions);
             return _dtoService.GetBaseItemDto(item, dtoOptions);
         }
         }
 
 
-        private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
             where T : BaseItem, new()
             where T : BaseItem, new()
         {
         {
             var result = libraryManager.GetItemList(new InternalItemsQuery
             var result = libraryManager.GetItemList(new InternalItemsQuery

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

@@ -54,6 +54,11 @@ namespace Jellyfin.Api.Controllers
                     string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
                     string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
                 .FirstOrDefault();
                 .FirstOrDefault();
 
 
+            if (result == null)
+            {
+                return NotFound();
+            }
+
             return result;
             return result;
         }
         }
 
 
@@ -149,12 +154,13 @@ namespace Jellyfin.Api.Controllers
         /// <param name="repositoryInfos">The list of package repositories.</param>
         /// <param name="repositoryInfos">The list of package repositories.</param>
         /// <response code="204">Package repositories saved.</response>
         /// <response code="204">Package repositories saved.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpOptions("Repositories")]
+        [HttpPost("Repositories")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
         public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
         {
         {
             _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
             _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
+            _serverConfigurationManager.SaveConfiguration();
             return NoContent();
             return NoContent();
         }
         }
     }
     }

+ 14 - 6
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -157,9 +157,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesImageFile]
         [ProducesImageFile]
-        public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
+        public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
         {
         {
-            var urlHash = imageUrl.GetMD5();
+            var urlHash = imageUrl.ToString().GetMD5();
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
             var pointerCachePath = GetFullCachePath(urlHash.ToString());
 
 
             string? contentPath = null;
             string? contentPath = null;
@@ -245,17 +245,25 @@ namespace Jellyfin.Api.Controllers
         /// <param name="urlHash">The URL hash.</param>
         /// <param name="urlHash">The URL hash.</param>
         /// <param name="pointerCachePath">The pointer cache path.</param>
         /// <param name="pointerCachePath">The pointer cache path.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
+        private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
         {
         {
             var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
             using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
-            var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
+            if (response.Content.Headers.ContentType?.MediaType == null)
+            {
+                throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
+            }
+
+            var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
             var fullCachePath = GetFullCachePath(urlHash + "." + ext);
 
 
-            Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
+            var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
+            Directory.CreateDirectory(fullCacheDirectory);
             await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
             await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
             await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
             await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
-            Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
+
+            var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
+            Directory.CreateDirectory(pointerCacheDirectory);
             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
             await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
         }
         }

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

@@ -260,7 +260,7 @@ namespace Jellyfin.Api.Controllers
             }
             }
         }
         }
 
 
-        private T GetParentWithImage<T>(BaseItem item, ImageType type)
+        private T? GetParentWithImage<T>(BaseItem item, ImageType type)
             where T : BaseItem
             where T : BaseItem
         {
         {
             return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
             return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));

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

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

+ 3 - 1
Jellyfin.Api/Controllers/VideoHlsController.cs

@@ -11,6 +11,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
@@ -361,7 +362,8 @@ namespace Jellyfin.Api.Controllers
             var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
             var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
             var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
             var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
             var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
             var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
-            var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
+            var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+            var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
 
 
             var segmentFormat = format.TrimStart('.');
             var segmentFormat = format.TrimStart('.');
             if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))

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

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

+ 8 - 1
Jellyfin.Api/Helpers/AudioHelper.cs

@@ -1,8 +1,10 @@
-using System.Net.Http;
+using System;
+using System.Net.Http;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Devices;
@@ -98,6 +100,11 @@ namespace Jellyfin.Api.Helpers
             TranscodingJobType transcodingJobType,
             TranscodingJobType transcodingJobType,
             StreamingRequestDto streamingRequest)
             StreamingRequestDto streamingRequest)
         {
         {
+            if (_httpContextAccessor.HttpContext == null)
+            {
+                throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+            }
+
             bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
             bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
             var cancellationTokenSource = new CancellationTokenSource();
             var cancellationTokenSource = new CancellationTokenSource();
 
 

+ 8 - 3
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -113,7 +113,7 @@ namespace Jellyfin.Api.Helpers
             StreamingRequestDto streamingRequest,
             StreamingRequestDto streamingRequest,
             bool enableAdaptiveBitrateStreaming)
             bool enableAdaptiveBitrateStreaming)
         {
         {
-            var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head;
+            var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
             var cancellationTokenSource = new CancellationTokenSource();
             var cancellationTokenSource = new CancellationTokenSource();
             return await GetMasterPlaylistInternal(
             return await GetMasterPlaylistInternal(
                 streamingRequest,
                 streamingRequest,
@@ -130,6 +130,11 @@ namespace Jellyfin.Api.Helpers
             TranscodingJobType transcodingJobType,
             TranscodingJobType transcodingJobType,
             CancellationTokenSource cancellationTokenSource)
             CancellationTokenSource cancellationTokenSource)
         {
         {
+            if (_httpContextAccessor.HttpContext == null)
+            {
+                throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+            }
+
             using var state = await StreamingHelpers.GetStreamingState(
             using var state = await StreamingHelpers.GetStreamingState(
                     streamingRequest,
                     streamingRequest,
                     _httpContextAccessor.HttpContext.Request,
                     _httpContextAccessor.HttpContext.Request,
@@ -487,14 +492,14 @@ namespace Jellyfin.Api.Helpers
 
 
             if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
             {
             {
-                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+                string? profile = state.GetRequestedProfiles("h264").FirstOrDefault();
                 return HlsCodecStringHelpers.GetH264String(profile, level);
                 return HlsCodecStringHelpers.GetH264String(profile, level);
             }
             }
 
 
             if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
             if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
                 || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
             {
             {
-                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
+                string? profile = state.GetRequestedProfiles("h265").FirstOrDefault();
 
 
                 return HlsCodecStringHelpers.GetH265String(profile, level);
                 return HlsCodecStringHelpers.GetH265String(profile, level);
             }
             }

+ 2 - 2
Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs

@@ -37,8 +37,8 @@ namespace Jellyfin.Api.Helpers
             }
             }
 
 
             // Can't dispose the response as it's required up the call chain.
             // Can't dispose the response as it's required up the call chain.
-            var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
-            var contentType = response.Content.Headers.ContentType.ToString();
+            var response = await httpClient.GetAsync(new Uri(state.MediaPath)).ConfigureAwait(false);
+            var contentType = response.Content.Headers.ContentType?.ToString();
 
 
             httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
             httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
 
 

+ 3 - 3
Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs

@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// </summary>
         /// <param name="profile">AAC profile.</param>
         /// <param name="profile">AAC profile.</param>
         /// <returns>AAC codec string.</returns>
         /// <returns>AAC codec string.</returns>
-        public static string GetAACString(string profile)
+        public static string GetAACString(string? profile)
         {
         {
             StringBuilder result = new StringBuilder("mp4a", 9);
             StringBuilder result = new StringBuilder("mp4a", 9);
 
 
@@ -46,7 +46,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="profile">H.264 profile.</param>
         /// <param name="profile">H.264 profile.</param>
         /// <param name="level">H.264 level.</param>
         /// <param name="level">H.264 level.</param>
         /// <returns>H.264 string.</returns>
         /// <returns>H.264 string.</returns>
-        public static string GetH264String(string profile, int level)
+        public static string GetH264String(string? profile, int level)
         {
         {
             StringBuilder result = new StringBuilder("avc1", 11);
             StringBuilder result = new StringBuilder("avc1", 11);
 
 
@@ -80,7 +80,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="profile">H.265 profile.</param>
         /// <param name="profile">H.265 profile.</param>
         /// <param name="level">H.265 level.</param>
         /// <param name="level">H.265 level.</param>
         /// <returns>H.265 string.</returns>
         /// <returns>H.265 string.</returns>
-        public static string GetH265String(string profile, int level)
+        public static string GetH265String(string? profile, int level)
         {
         {
             // The h265 syntax is a bit of a mystery at the time this comment was written.
             // The h265 syntax is a bit of a mystery at the time this comment was written.
             // This is what I've found through various sources:
             // This is what I've found through various sources:

+ 5 - 0
Jellyfin.Api/Helpers/HlsHelpers.cs

@@ -45,6 +45,11 @@ namespace Jellyfin.Api.Helpers
                         while (!reader.EndOfStream)
                         while (!reader.EndOfStream)
                         {
                         {
                             var line = await reader.ReadLineAsync().ConfigureAwait(false);
                             var line = await reader.ReadLineAsync().ConfigureAwait(false);
+                            if (line == null)
+                            {
+                                // Nothing currently in buffer.
+                                break;
+                            }
 
 
                             if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
                             if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
                             {
                             {

+ 6 - 0
Jellyfin.Api/Helpers/ProgressiveFileCopier.cs

@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 
 
@@ -90,6 +91,11 @@ namespace Jellyfin.Api.Helpers
                     allowAsyncFileRead = true;
                     allowAsyncFileRead = true;
                 }
                 }
 
 
+                if (_path == null)
+                {
+                    throw new ResourceNotFoundException(nameof(_path));
+                }
+
                 await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
                 await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
 
 
                 var eofCount = 0;
                 var eofCount = 0;

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

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

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

@@ -83,8 +83,12 @@ namespace Jellyfin.Api.Helpers
             }
             }
 
 
             streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
             streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
+            if (httpRequest.Path.Value == null)
+            {
+                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))
             if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
             {
             {

+ 10 - 9
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -12,6 +12,7 @@ using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
@@ -102,7 +103,7 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// </summary>
         /// <param name="playSessionId">Playback session id.</param>
         /// <param name="playSessionId">Playback session id.</param>
         /// <returns>The transcoding job.</returns>
         /// <returns>The transcoding job.</returns>
-        public TranscodingJobDto GetTranscodingJob(string playSessionId)
+        public TranscodingJobDto? GetTranscodingJob(string playSessionId)
         {
         {
             lock (_activeTranscodingJobs)
             lock (_activeTranscodingJobs)
             {
             {
@@ -116,7 +117,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="path">Path to the transcoding file.</param>
         /// <param name="path">Path to the transcoding file.</param>
         /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
         /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
         /// <returns>The transcoding job.</returns>
         /// <returns>The transcoding job.</returns>
-        public TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
+        public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
         {
         {
             lock (_activeTranscodingJobs)
             lock (_activeTranscodingJobs)
             {
             {
@@ -193,10 +194,9 @@ namespace Jellyfin.Api.Helpers
         /// Called when [transcode kill timer stopped].
         /// Called when [transcode kill timer stopped].
         /// </summary>
         /// </summary>
         /// <param name="state">The state.</param>
         /// <param name="state">The state.</param>
-        private async void OnTranscodeKillTimerStopped(object state)
+        private async void OnTranscodeKillTimerStopped(object? state)
         {
         {
-            var job = (TranscodingJobDto)state;
-
+            var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
             if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
             if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
             {
             {
                 var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
                 var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
@@ -489,7 +489,8 @@ namespace Jellyfin.Api.Helpers
             CancellationTokenSource cancellationTokenSource,
             CancellationTokenSource cancellationTokenSource,
             string? workingDirectory = null)
             string? workingDirectory = null)
         {
         {
-            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+            var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+            Directory.CreateDirectory(directory);
 
 
             await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
             await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 
 
@@ -523,7 +524,7 @@ namespace Jellyfin.Api.Helpers
                     RedirectStandardInput = true,
                     RedirectStandardInput = true,
                     FileName = _mediaEncoder.EncoderPath,
                     FileName = _mediaEncoder.EncoderPath,
                     Arguments = commandLineArguments,
                     Arguments = commandLineArguments,
-                    WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
+                    WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
                     ErrorDialog = false
                     ErrorDialog = false
                 },
                 },
                 EnableRaisingEvents = true
                 EnableRaisingEvents = true
@@ -827,7 +828,7 @@ namespace Jellyfin.Api.Helpers
         {
         {
             lock (_transcodingLocks)
             lock (_transcodingLocks)
             {
             {
-                if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
+                if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
                 {
                 {
                     result = new SemaphoreSlim(1, 1);
                     result = new SemaphoreSlim(1, 1);
                     _transcodingLocks[outputPath] = result;
                     _transcodingLocks[outputPath] = result;
@@ -837,7 +838,7 @@ namespace Jellyfin.Api.Helpers
             }
             }
         }
         }
 
 
-        private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
         {
         {
             if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
             if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
             {
             {

+ 5 - 3
Jellyfin.Api/Jellyfin.Api.csproj

@@ -6,17 +6,19 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
+    <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
+    <NoWarn>AD0001</NoWarn>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
   </ItemGroup>
   </ItemGroup>

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

@@ -1,7 +1,9 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.ComponentModel;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
 
 
 namespace Jellyfin.Api.ModelBinders
 namespace Jellyfin.Api.ModelBinders
 {
 {
@@ -11,6 +13,17 @@ namespace Jellyfin.Api.ModelBinders
     /// </summary>
     /// </summary>
     public class CommaDelimitedArrayModelBinder : IModelBinder
     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/>
         /// <inheritdoc/>
         public Task BindModelAsync(ModelBindingContext bindingContext)
         public Task BindModelAsync(ModelBindingContext bindingContext)
         {
         {
@@ -20,16 +33,8 @@ namespace Jellyfin.Api.ModelBinders
 
 
             if (valueProviderResult.Length > 1)
             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
             else
             {
             {
@@ -37,13 +42,8 @@ namespace Jellyfin.Api.ModelBinders
 
 
                 if (value != null)
                 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);
                     bindingContext.Result = ModelBindingResult.Success(typedValues);
                 }
                 }
                 else
                 else
@@ -55,5 +55,36 @@ namespace Jellyfin.Api.ModelBinders
 
 
             return Task.CompletedTask;
             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
 namespace Jellyfin.Api.Models.LibraryDtos
 {
 {
@@ -10,25 +11,21 @@ namespace Jellyfin.Api.Models.LibraryDtos
         /// <summary>
         /// <summary>
         /// Gets or sets the metadata savers.
         /// Gets or sets the metadata savers.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets the metadata readers.
         /// Gets or sets the metadata readers.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets the subtitle fetchers.
         /// Gets or sets the subtitle fetchers.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets the type options.
         /// Gets or sets the type options.
         /// </summary>
         /// </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.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
@@ -17,25 +18,21 @@ namespace Jellyfin.Api.Models.LibraryDtos
         /// <summary>
         /// <summary>
         /// Gets or sets the metadata fetchers.
         /// Gets or sets the metadata fetchers.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets the image fetchers.
         /// Gets or sets the image fetchers.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets the supported image types.
         /// Gets or sets the supported image types.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets the default image options.
         /// Gets or sets the default image options.
         /// </summary>
         /// </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 System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
@@ -25,8 +26,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// <summary>
         /// <summary>
         /// Gets or sets list of mappings.
         /// Gets or sets list of mappings.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets provider name.
         /// Gets or sets provider name.

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

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization;
 using MediaBrowser.Common.Json.Converters;
 using MediaBrowser.Common.Json.Converters;
@@ -143,8 +144,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// Optional.
         /// Optional.
         /// </summary>
         /// </summary>
         [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
         [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>
         /// <summary>
         /// Gets or sets include user data.
         /// Gets or sets include user data.
@@ -169,7 +169,6 @@ namespace Jellyfin.Api.Models.LiveTvDtos
         /// Optional.
         /// Optional.
         /// </summary>
         /// </summary>
         [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
         [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
-        [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "Fields", Justification = "Imported from ServiceStack")]
-        public ItemFields[] Fields { get; set; } = Array.Empty<ItemFields>();
+        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.Dlna;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 
 
@@ -17,8 +18,6 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
         /// <summary>
         /// <summary>
         /// Gets or sets the device play protocols.
         /// Gets or sets the device play protocols.
         /// </summary>
         /// </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>();
     }
     }
 }
 }

+ 2 - 2
Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs

@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
         /// Start kill timer.
         /// Start kill timer.
         /// </summary>
         /// </summary>
         /// <param name="callback">Callback action.</param>
         /// <param name="callback">Callback action.</param>
-        public void StartKillTimer(Action<object> callback)
+        public void StartKillTimer(Action<object?> callback)
         {
         {
             StartKillTimer(callback, PingTimeout);
             StartKillTimer(callback, PingTimeout);
         }
         }
@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
         /// </summary>
         /// </summary>
         /// <param name="callback">Callback action.</param>
         /// <param name="callback">Callback action.</param>
         /// <param name="intervalMs">Callback interval.</param>
         /// <param name="intervalMs">Callback interval.</param>
-        public void StartKillTimer(Action<object> callback, int intervalMs)
+        public void StartKillTimer(Action<object?> callback, int intervalMs)
         {
         {
             if (HasExited)
             if (HasExited)
             {
             {

+ 1 - 1
Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs

@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
             return _config.GetConfiguration<EncodingOptions>("encoding");
             return _config.GetConfiguration<EncodingOptions>("encoding");
         }
         }
 
 
-        private async void TimerCallback(object state)
+        private async void TimerCallback(object? state)
         {
         {
             if (_job.HasExited)
             if (_job.HasExited)
             {
             {

+ 1 - 1
Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs

@@ -56,7 +56,7 @@ namespace Jellyfin.Api.WebSocketListeners
             base.Dispose(dispose);
             base.Dispose(dispose);
         }
         }
 
 
-        private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
         {
         {
             SendData(true);
             SendData(true);
         }
         }

+ 3 - 3
Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs

@@ -64,19 +64,19 @@ namespace Jellyfin.Api.WebSocketListeners
             base.Dispose(dispose);
             base.Dispose(dispose);
         }
         }
 
 
-        private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+        private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
         {
         {
             SendData(true);
             SendData(true);
             e.Task.TaskProgress -= OnTaskProgress;
             e.Task.TaskProgress -= OnTaskProgress;
         }
         }
 
 
-        private void OnTaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
+        private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
         {
         {
             SendData(true);
             SendData(true);
             e.Argument.TaskProgress += OnTaskProgress;
             e.Argument.TaskProgress += OnTaskProgress;
         }
         }
 
 
-        private void OnTaskProgress(object sender, GenericEventArgs<double> e)
+        private void OnTaskProgress(object? sender, GenericEventArgs<double> e)
         {
         {
             SendData(false);
             SendData(false);
         }
         }

+ 7 - 7
Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs

@@ -66,37 +66,37 @@ namespace Jellyfin.Api.WebSocketListeners
             base.Dispose(dispose);
             base.Dispose(dispose);
         }
         }
 
 
-        private async void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
+        private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
         {
         {
             await SendData(false).ConfigureAwait(false);
             await SendData(false).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
+        private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
         {
         {
             await SendData(true).ConfigureAwait(false);
             await SendData(true).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
         {
         {
             await SendData(!e.IsAutomated).ConfigureAwait(false);
             await SendData(!e.IsAutomated).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
+        private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
         {
         {
             await SendData(true).ConfigureAwait(false);
             await SendData(true).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
+        private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
         {
         {
             await SendData(true).ConfigureAwait(false);
             await SendData(true).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
+        private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
         {
         {
             await SendData(true).ConfigureAwait(false);
             await SendData(true).ConfigureAwait(false);
         }
         }
 
 
-        private async void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
+        private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
         {
         {
             await SendData(true).ConfigureAwait(false);
             await SendData(true).ConfigureAwait(false);
         }
         }

+ 3 - 3
Jellyfin.Data/Jellyfin.Data.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -41,8 +41,8 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.9" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 1 - 1
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -6,7 +6,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 7 - 4
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using BlurHashSharp.SkiaSharp;
 using BlurHashSharp.SkiaSharp;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Drawing;
@@ -227,8 +228,8 @@ namespace Jellyfin.Drawing.Skia
             }
             }
 
 
             var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
             var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
-
-            Directory.CreateDirectory(Path.GetDirectoryName(tempPath));
+            var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
+            Directory.CreateDirectory(directory);
             File.Copy(path, tempPath, true);
             File.Copy(path, tempPath, true);
 
 
             return tempPath;
             return tempPath;
@@ -493,7 +494,8 @@ namespace Jellyfin.Drawing.Skia
             // If all we're doing is resizing then we can stop now
             // If all we're doing is resizing then we can stop now
             if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
             if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
             {
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+                var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+                Directory.CreateDirectory(outputDirectory);
                 using var outputStream = new SKFileWStream(outputPath);
                 using var outputStream = new SKFileWStream(outputPath);
                 using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
                 using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
                 resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
                 resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
@@ -540,7 +542,8 @@ namespace Jellyfin.Drawing.Skia
                 DrawIndicator(canvas, width, height, options);
                 DrawIndicator(canvas, width, height, options);
             }
             }
 
 
-            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+            var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+            Directory.CreateDirectory(directory);
             using (var outputStream = new SKFileWStream(outputPath))
             using (var outputStream = new SKFileWStream(outputPath))
             {
             {
                 using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
                 using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))

+ 4 - 4
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -24,12 +24,12 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="System.Linq.Async" Version="4.1.1" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.9">
+    <PackageReference Include="System.Linq.Async" Version="5.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>

+ 2 - 1
Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -57,7 +57,8 @@ namespace Jellyfin.Server.Implementations.Users
                 SerializablePasswordReset spr;
                 SerializablePasswordReset spr;
                 await using (var str = File.OpenRead(resetFile))
                 await using (var str = File.OpenRead(resetFile))
                 {
                 {
-                    spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
+                    spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
+                        ?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
                 }
                 }
 
 
                 if (spr.ExpirationDate < DateTime.UtcNow)
                 if (spr.ExpirationDate < DateTime.UtcNow)

+ 4 - 4
Jellyfin.Server/Filters/FileResponseFilter.cs

@@ -26,22 +26,22 @@ namespace Jellyfin.Server.Filters
                 if (attribute is ProducesFileAttribute producesFileAttribute)
                 if (attribute is ProducesFileAttribute producesFileAttribute)
                 {
                 {
                     // Get operation response values.
                     // Get operation response values.
-                    var (_, value) = operation.Responses
+                    var response = operation.Responses
                         .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
                         .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
 
 
                     // Operation doesn't have a response.
                     // Operation doesn't have a response.
-                    if (value == null)
+                    if (response.Value == null)
                     {
                     {
                         continue;
                         continue;
                     }
                     }
 
 
                     // Clear existing responses.
                     // Clear existing responses.
-                    value.Content.Clear();
+                    response.Value.Content.Clear();
 
 
                     // Add all content-types as file.
                     // Add all content-types as file.
                     foreach (var contentType in producesFileAttribute.GetContentTypes())
                     foreach (var contentType in producesFileAttribute.GetContentTypes())
                     {
                     {
-                        value.Content.Add(contentType, _openApiMediaType);
+                        response.Value.Content.Add(contentType, _openApiMediaType);
                     }
                     }
 
 
                     break;
                     break;

+ 2 - 1
Jellyfin.Server/Formatters/CssOutputFormatter.cs

@@ -30,7 +30,8 @@ namespace Jellyfin.Server.Formatters
         /// <returns>Write stream task.</returns>
         /// <returns>Write stream task.</returns>
         public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
         public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
         {
         {
-            return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+            var stringResponse = context.Object?.ToString();
+            return stringResponse == null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
         }
         }
     }
     }
 }
 }

+ 2 - 1
Jellyfin.Server/Formatters/XmlOutputFormatter.cs

@@ -26,7 +26,8 @@ namespace Jellyfin.Server.Formatters
         /// <inheritdoc />
         /// <inheritdoc />
         public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
         public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
         {
         {
-            return context.HttpContext.Response.WriteAsync(context.Object?.ToString());
+            var stringResponse = context.Object?.ToString();
+            return stringResponse == null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
         }
         }
     }
     }
 }
 }

+ 5 - 5
Jellyfin.Server/Jellyfin.Server.csproj

@@ -8,7 +8,7 @@
   <PropertyGroup>
   <PropertyGroup>
     <AssemblyName>jellyfin</AssemblyName>
     <AssemblyName>jellyfin</AssemblyName>
     <OutputType>Exe</OutputType>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -38,10 +38,10 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
     <PackageReference Include="prometheus-net" Version="4.0.0" />
     <PackageReference Include="prometheus-net" Version="4.0.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

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

@@ -9,6 +9,7 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 using SQLitePCL.pretty;
@@ -26,6 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
         private readonly IServerApplicationPaths _paths;
         private readonly IServerApplicationPaths _paths;
         private readonly JellyfinDbProvider _provider;
         private readonly JellyfinDbProvider _provider;
         private readonly JsonSerializerOptions _jsonOptions;
         private readonly JsonSerializerOptions _jsonOptions;
+        private readonly IUserManager _userManager;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
         /// 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="logger">The logger.</param>
         /// <param name="paths">The server application paths.</param>
         /// <param name="paths">The server application paths.</param>
         /// <param name="provider">The database provider.</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;
             _logger = logger;
             _paths = paths;
             _paths = paths;
             _provider = provider;
             _provider = provider;
+            _userManager = userManager;
             _jsonOptions = new JsonSerializerOptions();
             _jsonOptions = new JsonSerializerOptions();
             _jsonOptions.Converters.Add(new JsonStringEnumConverter());
             _jsonOptions.Converters.Add(new JsonStringEnumConverter());
         }
         }
@@ -86,11 +94,19 @@ namespace Jellyfin.Server.Migrations.Routines
                         continue;
                         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)
                     var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
                         ? chromecastDict[version]
                         ? chromecastDict[version]
                         : ChromecastVersion.Stable;
                         : 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,
                         IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
                         ShowBackdrop = dto.ShowBackdrop,
                         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>
         /// <param name="newConfiguration">The new configuration.</param>
         void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
         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>
         /// <summary>
         /// Gets the configuration.
         /// Gets the configuration.
         /// </summary>
         /// </summary>

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

@@ -26,19 +26,40 @@ namespace MediaBrowser.Common.Json.Converters
         {
         {
             if (reader.TokenType == JsonTokenType.String)
             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)
                 if (stringEntries == null || stringEntries.Length == 0)
                 {
                 {
                     return Array.Empty<T>();
                     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++)
                 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);
             return JsonSerializer.Deserialize<T[]>(ref reader, options);
@@ -50,4 +71,4 @@ namespace MediaBrowser.Common.Json.Converters
             JsonSerializer.Serialize(writer, value, options);
             JsonSerializer.Serialize(writer, value, options);
         }
         }
     }
     }
-}
+}

+ 3 - 3
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -18,8 +18,8 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
   </ItemGroup>
   </ItemGroup>
@@ -29,7 +29,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 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 />
         /// <inheritdoc />
         public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
         public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
         {
         {
@@ -185,6 +175,11 @@ namespace MediaBrowser.Common.Plugins
         /// <value>The type of the configuration.</value>
         /// <value>The type of the configuration.</value>
         public Type ConfigurationType => typeof(TConfigurationType);
         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>
         /// <summary>
         /// Gets the name the assembly file.
         /// Gets the name the assembly file.
         /// </summary>
         /// </summary>
@@ -280,6 +275,8 @@ namespace MediaBrowser.Common.Plugins
             Configuration = (TConfigurationType)configuration;
             Configuration = (TConfigurationType)configuration;
 
 
             SaveConfiguration();
             SaveConfiguration();
+
+            ConfigurationChanged.Invoke(this, configuration);
         }
         }
 
 
         /// <inheritdoc />
         /// <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.
         /// Called when just before the plugin is uninstalled from the server.
         /// </summary>
         /// </summary>
         void OnUninstalling();
         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
     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
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
@@ -15,9 +16,9 @@ namespace MediaBrowser.Controller.Dto
             ItemFields.RefreshState
             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; }
         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 InterviewFolderName = "interviews";
         public const string SceneFolderName = "scenes";
         public const string SceneFolderName = "scenes";
         public const string SampleFolderName = "samples";
         public const string SampleFolderName = "samples";
+        public const string ShortsFolderName = "shorts";
+        public const string FeaturettesFolderName = "featurettes";
 
 
         public static readonly string[] AllExtrasTypesFolderNames = {
         public static readonly string[] AllExtrasTypesFolderNames = {
             ExtrasFolderName,
             ExtrasFolderName,
@@ -94,7 +96,9 @@ namespace MediaBrowser.Controller.Entities
             DeletedScenesFolderName,
             DeletedScenesFolderName,
             InterviewFolderName,
             InterviewFolderName,
             SceneFolderName,
             SceneFolderName,
-            SampleFolderName
+            SampleFolderName,
+            ShortsFolderName,
+            FeaturettesFolderName
         };
         };
 
 
         [JsonIgnore]
         [JsonIgnore]

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

@@ -225,7 +225,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="fields">The fields.</param>
         /// <param name="fields">The fields.</param>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         /// <returns>Task.</returns>
         /// <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>
         /// <summary>
         /// Saves the tuner host.
         /// Saves the tuner host.

+ 3 - 3
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -14,8 +14,8 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
   </ItemGroup>
   </ItemGroup>
 
 
@@ -29,7 +29,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>

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

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

+ 1 - 1
MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -486,7 +486,7 @@ namespace MediaBrowser.LocalMetadata.Images
             return false;
             return false;
         }
         }
 
 
-        private FileSystemMetadata GetImage(IEnumerable<FileSystemMetadata> files, string name)
+        private FileSystemMetadata? GetImage(IEnumerable<FileSystemMetadata> files, string name)
         {
         {
             return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0);
             return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0);
         }
         }

+ 1 - 1
MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj

@@ -11,7 +11,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>netstandard2.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

@@ -683,7 +683,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 default:
                 default:
                 {
                 {
                     string readerName = reader.Name;
                     string readerName = reader.Name;
-                    if (_validProviderIds!.TryGetValue(readerName, out string providerIdValue))
+                    if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
                     {
                     {
                         var id = reader.ReadElementContentAsString();
                         var id = reader.ReadElementContentAsString();
                         if (!string.IsNullOrWhiteSpace(id))
                         if (!string.IsNullOrWhiteSpace(id))

部分文件因为文件数量过多而无法显示